Merge branch '4.x' into main

This commit is contained in:
Oleg Zhurakousky
2022-01-18 14:13:20 +01:00
67 changed files with 1972 additions and 6997 deletions

View File

@@ -176,20 +176,20 @@ You can also build and package your application into a boot jar (by using `./mvn
Now you have a working (albeit very basic) Spring Cloud Stream application.
[[spring-cloud-stream-preface-notable-deprecations]]
== Notable Deprecations
[[spel-and-streaming-data]]
- Annotation-based programming model. Basically the @EnableBInding, @StreamListener and all related annotations are now deprecated in
favor of the functional programming model. See <<Spring Cloud Function support>> for more details.
- _Reactive module_ (`spring-cloud-stream-reactive`) is discontinued and no longer distributed in favor of native support via spring-cloud-function.
For backward
compatibility you can still bring `spring-cloud-stream-reactive` from previous versions.
- _Test support binder_ `spring-cloud-stream-test-support` with MessageCollector in favor of a new test binder. See <<Testing>> for more details.
- _@StreamMessageConverter_ - deprecated as it is no longer required.
- The `original-content-type` header references have been removed after it's been deprecated in v2.0.
- The `BinderAwareChannelResolver` is deprecated in favor if providing `spring.cloud.stream.sendto.destination` property.
This is primarily for function-based programming model. For StreamListener it would still be required and thus will stay until we deprecate and eventually discontinue StreamListener
and annotation-based programming model.
== Spring Expression Language (SpEL) in the context of Streaming data
Throughout this reference manual you will encounter many features and examples where you can utilize Spring Expression Language (SpEL). It is important to understand certain limitations when it comes to using it.
SpEL gives you access to the current Message as well as the Application Context you are running in.
However it is important to understand what type of data SpEL can see especially in the context of the incoming Message.
From the broker, the message arrives in a form of a byte[]. It is then transformed to a `Message<byte[]>` by the binders where as you can see the payload of the message maintains its raw form. The headers of the message are `<String, Object>`, where values are typically another primitive or a collection/array of primitives, hence Object.
That is because binder does not know the required input type as it has no access to the user code (function). So effectively binder delivered an envelope with the payload and some readable meta-data in the form of message headers, just like the letter delivered by mail.
This means that while accessing payload of the message is possible you will only have access to it as raw data (i.e., byte[]). And while it may be very common for developers to ask for ability to have SpEL access to fields of a payload object as concrete type (e.g., Foo, Bar etc), you can see how difficult or even impossible would it be to achieve.
Here is one example to demonstrate the problem; Imagine you have a routing expression to route to different functions based on payload type. This requirement would imply payload conversion from byte[] to a specific type and then applying the SpEL. However, in order to perform such conversion we would need to know the actual type to pass to converter and that comes from function's signature which we dont know which one. A better approach to solve this requirement would be to pass the type information as message headers (e.g., `application/json;type=foo.bar.Baz` ). Youll get a clear readable String value that could be accessed and evaluated in a year and easy to read SpEL expression.
Additionally it is considered very bad practice to use payload for routing decisions, since the payload is considered to be privileged data - data only to be read by its final recipient. Again, using the mail delivery analogy you would not want the mailman to open your envelope and read the contents of the letter to make some delivery decisions. The same concept applies here, especially when it is relatively easy to include such information when generating a Message. It enforces certain level of discipline related to the design of data to be transmitted over the network and which pieces of such data can be considered as public and which are privileged.
[[spel-and-streaming-data]]

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>
<name>spring-cloud-stream-docs</name>

View File

@@ -166,7 +166,6 @@ compatibility you can still bring `spring-cloud-stream-reactive` from previous v
- _Test support binder_ `spring-cloud-stream-test-support` with MessageCollector in favor of a new test binder. See <<Testing>> for more details.
- _@StreamMessageConverter_ - deprecated as it is no longer required.
- The `original-content-type` header references have been removed after it's been deprecated in v2.0.
- The `BinderAwareChannelResolver` is deprecated in favor if providing `spring.cloud.stream.sendto.destination` property.
This is primarily for function-based programming model. For StreamListener it would still be required and thus will stay until we deprecate and eventually discontinue StreamListener
and annotation-based programming model.

View File

@@ -1398,44 +1398,6 @@ Aside from static destinations, Spring Cloud Stream lets applications send messa
This is useful, for example, when the target destination needs to be determined at runtime.
Applications can do so in one of two ways.
===== BinderAwareChannelResolver
The `BinderAwareChannelResolver` is a special bean registered automatically by the framework.
You can autowire this bean into your application and use it to resolve output destination at runtime
The 'spring.cloud.stream.dynamicDestinations' property can be used for restricting the dynamic destination names to a known set (that is, intentionally allowed values).
If this property is not set, any destination can be bound dynamically.
The following example demonstrates one of the common scenarios where REST controller uses a path variable to determine target destination:
[source,java]
----
@SpringBootApplication
@Controller
public class SourceWithDynamicDestination {
@Autowired
private BinderAwareChannelResolver resolver;
@RequestMapping(value="/{target}")
@ResponseStatus(HttpStatus.ACCEPTED)
public void send(@RequestBody String body, @PathVariable("target") String target){
resolver.resolveDestination(target).send(new GenericMessage<String>(body));
}
}
----
Now consider what happens when we start the application on the default port (8080) and make the following requests with CURL:
----
curl -H "Content-Type: application/json" -X POST -d "customer-1" http://localhost:8080/customers
curl -H "Content-Type: application/json" -X POST -d "order-1" http://localhost:8080/orders
----
The destinations, 'customers' and 'orders', are created in the broker (in the exchange for Rabbit or in the topic for Kafka)
with names of 'customers' and 'orders', and the data is published to the appropriate destinations.
===== spring.cloud.stream.sendto.destination
You can also delegate to the framework to dynamically resolve the output destination by specifying `spring.cloud.stream.sendto.destination` header

10
pom.xml
View File

@@ -4,12 +4,16 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-stream-parent</artifactId>
<<<<<<< HEAD
<version>3.2.2-SNAPSHOT</version>
=======
<version>4.0.0-SNAPSHOT</version>
>>>>>>> 4.x
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>3.1.0</version>
<version>4.0.0-SNAPSHOT</version>
<relativePath />
</parent>
<scm>
@@ -22,7 +26,7 @@
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.8</java.version>
<java.version>17</java.version>
<reactor.version>2020.0.7</reactor.version>
<objenesis.version>2.1</objenesis.version>
<spring-cloud-function.version>3.2.2-SNAPSHOT</spring-cloud-function.version>
@@ -130,7 +134,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<!-- <version>1.7</version>-->
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -17,10 +17,7 @@
package org.springframework.cloud.stream.binder;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
@@ -34,9 +31,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.springframework.cloud.stream.binder.AbstractBinderTests.Station.Readings;
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
import org.springframework.cloud.stream.binding.StreamListenerMessageHandler;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
@@ -51,12 +46,8 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -424,193 +415,6 @@ public abstract class AbstractBinderTests<B extends AbstractTestBinder<? extends
return ".";
}
@SuppressWarnings("rawtypes")
@Test
public void testSendPojoReceivePojoWithStreamListenerDefaultContentType(TestInfo testInfo)
throws Exception {
StreamListenerMessageHandler handler = this.buildStreamListener(
AbstractBinderTests.class, "echoStation", Station.class);
Binder binder = getBinder();
BindingProperties producerBindingProperties = createProducerBindingProperties(
createProducerProperties(testInfo));
DirectChannel moduleOutputChannel = createBindableChannel("output",
producerBindingProperties);
BindingProperties consumerBindingProperties = createConsumerBindingProperties(
createConsumerProperties());
DirectChannel moduleInputChannel = createBindableChannel("input",
consumerBindingProperties);
Binding<MessageChannel> producerBinding = binder.bindProducer(
String.format("bad%s0a", getDestinationNameDelimiter()),
moduleOutputChannel, producerBindingProperties.getProducer());
Binding<MessageChannel> consumerBinding = binder.bindConsumer(
String.format("bad%s0a", getDestinationNameDelimiter()), "test-1",
moduleInputChannel, consumerBindingProperties.getConsumer());
Station station = new Station();
Message<?> message = MessageBuilder.withPayload(station).build();
moduleInputChannel.subscribe(handler);
moduleOutputChannel.send(message);
QueueChannel replyChannel = (QueueChannel) handler.getOutputChannel();
Message<?> replyMessage = replyChannel.receive(5000);
assertThat(replyMessage.getPayload() instanceof Station).isTrue();
producerBinding.unbind();
consumerBinding.unbind();
}
@SuppressWarnings("rawtypes")
@Test
public void testSendJsonReceivePojoWithStreamListener(TestInfo testInfo) throws Exception {
StreamListenerMessageHandler handler = this.buildStreamListener(
AbstractBinderTests.class, "echoStation", Station.class);
Binder binder = getBinder();
BindingProperties producerBindingProperties = createProducerBindingProperties(
createProducerProperties(testInfo));
DirectChannel moduleOutputChannel = createBindableChannel("output",
producerBindingProperties);
BindingProperties consumerBindingProperties = createConsumerBindingProperties(
createConsumerProperties());
DirectChannel moduleInputChannel = createBindableChannel("input",
consumerBindingProperties);
Binding<MessageChannel> producerBinding = binder.bindProducer(
String.format("bad%s0d", getDestinationNameDelimiter()),
moduleOutputChannel, producerBindingProperties.getProducer());
Binding<MessageChannel> consumerBinding = binder.bindConsumer(
String.format("bad%s0d", getDestinationNameDelimiter()), "test-4",
moduleInputChannel, consumerBindingProperties.getConsumer());
String value = "{\"readings\":[{\"stationid\":\"fgh\","
+ "\"customerid\":\"12345\",\"timestamp\":null},"
+ "{\"stationid\":\"hjk\",\"customerid\":\"222\",\"timestamp\":null}]}";
Message<?> message = MessageBuilder.withPayload(value)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();
moduleInputChannel.subscribe(handler);
moduleOutputChannel.send(message);
QueueChannel channel = (QueueChannel) handler.getOutputChannel();
Message<Station> reply = (Message<Station>) channel.receive(5000);
assertThat(reply).isNotNull();
assertThat(reply.getPayload() instanceof Station).isTrue();
producerBinding.unbind();
consumerBinding.unbind();
}
@SuppressWarnings("rawtypes")
@Test
public void testSendJsonReceiveJsonWithStreamListener(TestInfo testInfo) throws Exception {
StreamListenerMessageHandler handler = this.buildStreamListener(
AbstractBinderTests.class, "echoStationString", String.class);
Binder binder = getBinder();
BindingProperties producerBindingProperties = createProducerBindingProperties(
createProducerProperties(testInfo));
DirectChannel moduleOutputChannel = createBindableChannel("output",
producerBindingProperties);
BindingProperties consumerBindingProperties = createConsumerBindingProperties(
createConsumerProperties());
DirectChannel moduleInputChannel = createBindableChannel("input",
consumerBindingProperties);
Binding<MessageChannel> producerBinding = binder.bindProducer(
String.format("bad%s0e", getDestinationNameDelimiter()),
moduleOutputChannel, producerBindingProperties.getProducer());
Binding<MessageChannel> consumerBinding = binder.bindConsumer(
String.format("bad%s0e", getDestinationNameDelimiter()), "test-5",
moduleInputChannel, consumerBindingProperties.getConsumer());
String value = "{\"readings\":[{\"stationid\":\"fgh\","
+ "\"customerid\":\"12345\",\"timestamp\":null},"
+ "{\"stationid\":\"hjk\",\"customerid\":\"222\",\"timestamp\":null}]}";
Message<?> message = MessageBuilder.withPayload(value)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();
moduleInputChannel.subscribe(handler);
moduleOutputChannel.send(message);
QueueChannel channel = (QueueChannel) handler.getOutputChannel();
Message<String> reply = (Message<String>) channel.receive(5000);
assertThat(reply).isNotNull();
assertThat(reply.getPayload() instanceof String).isTrue();
producerBinding.unbind();
consumerBinding.unbind();
}
@SuppressWarnings("rawtypes")
@Test
public void testSendPojoReceivePojoWithStreamListener(TestInfo testInfo) throws Exception {
StreamListenerMessageHandler handler = this.buildStreamListener(
AbstractBinderTests.class, "echoStation", Station.class);
Binder binder = getBinder();
BindingProperties producerBindingProperties = createProducerBindingProperties(
createProducerProperties(testInfo));
DirectChannel moduleOutputChannel = createBindableChannel("output",
producerBindingProperties);
BindingProperties consumerBindingProperties = createConsumerBindingProperties(
createConsumerProperties());
DirectChannel moduleInputChannel = createBindableChannel("input",
consumerBindingProperties);
Binding<MessageChannel> producerBinding = binder.bindProducer(
String.format("bad%s0f", getDestinationNameDelimiter()),
moduleOutputChannel, producerBindingProperties.getProducer());
Binding<MessageChannel> consumerBinding = binder.bindConsumer(
String.format("bad%s0f", getDestinationNameDelimiter()), "test-6",
moduleInputChannel, consumerBindingProperties.getConsumer());
Readings r1 = new Readings();
r1.setCustomerid("123");
r1.setStationid("XYZ");
Readings r2 = new Readings();
r2.setCustomerid("546");
r2.setStationid("ABC");
Station station = new Station();
station.setReadings(Arrays.asList(r1, r2));
Message<?> message = MessageBuilder.withPayload(station)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();
moduleInputChannel.subscribe(handler);
moduleOutputChannel.send(message);
QueueChannel channel = (QueueChannel) handler.getOutputChannel();
Message<Station> reply = (Message<Station>) channel.receive(5000);
assertThat(reply).isNotNull();
assertThat(reply.getPayload() instanceof Station).isTrue();
producerBinding.unbind();
consumerBinding.unbind();
}
@SuppressWarnings("unused") // it is used via reflection
private Station echoStation(Station station) {
return station;
@@ -621,32 +425,6 @@ public abstract class AbstractBinderTests<B extends AbstractTestBinder<? extends
return station;
}
private StreamListenerMessageHandler buildStreamListener(Class<?> handlerClass,
String handlerMethodName, Class<?>... parameters) throws Exception {
String channelName = "reply_" + System.nanoTime();
this.applicationContext.getBeanFactory().registerSingleton(channelName, new QueueChannel());
Method m = ReflectionUtils.findMethod(handlerClass, handlerMethodName,
parameters);
InvocableHandlerMethod method = new InvocableHandlerMethod(this, m);
HandlerMethodArgumentResolverComposite resolver = new HandlerMethodArgumentResolverComposite();
CompositeMessageConverterFactory factory = new CompositeMessageConverterFactory();
resolver.addResolver(new PayloadArgumentResolver(
factory.getMessageConverterForAllRegistered()));
method.setMessageMethodArgumentResolvers(resolver);
Constructor<?> c = ReflectionUtils.accessibleConstructor(
StreamListenerMessageHandler.class, InvocableHandlerMethod.class,
boolean.class, String[].class);
StreamListenerMessageHandler handler = (StreamListenerMessageHandler) c
.newInstance(method, false, new String[] {});
handler.setOutputChannelName(channelName);
handler.setBeanFactory(this.applicationContext);
handler.afterPropertiesSet();
// context.refresh();
return handler;
}
public static class Station {
List<Readings> readings = new ArrayList<>();

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -26,7 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamMessageConverter;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.binder.TestSupportBinder;
@@ -59,12 +58,10 @@ public class CustomMessageConverterTests {
private BinderFactory binderFactory;
@Autowired
@StreamMessageConverter
private List<MessageConverter> customMessageConverters;
@Test
public void testCustomMessageConverter() throws Exception {
assertThat(this.customMessageConverters).hasSize(2);
assertThat(this.customMessageConverters).extracting("class")
.contains(FooConverter.class, BarConverter.class);
this.testSource.output().send(MessageBuilder.withPayload(new Foo("hi")).build());
@@ -84,13 +81,11 @@ public class CustomMessageConverterTests {
public static class TestSource {
@Bean
@StreamMessageConverter
public MessageConverter fooConverter() {
return new FooConverter();
}
@Bean
@StreamMessageConverter
public MessageConverter barConverter() {
return new BarConverter();
}

View File

@@ -1,191 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.validation.Valid;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
public class StreamListenerAnnotatedMethodArgumentsTests {
@BeforeClass
public static void init() {
Locale.setDefault(Locale.US);
}
@Test
@SuppressWarnings("unchecked")
public void testAnnotatedArguments() {
ConfigurableApplicationContext context = SpringApplication
.run(TestPojoWithAnnotatedArguments.class, "--server.port=0");
TestPojoWithAnnotatedArguments testPojoWithAnnotatedArguments = context
.getBean(TestPojoWithAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", MimeType.valueOf("application/json"))
.setHeader("testHeader", "testValue").build());
assertThat(testPojoWithAnnotatedArguments.receivedArguments).hasSize(3);
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(0))
.isInstanceOf(StreamListenerTestUtils.FooPojo.class);
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(1))
.isInstanceOf(Map.class);
assertThat((Map<String, Object>) testPojoWithAnnotatedArguments.receivedArguments
.get(1)).containsEntry(MessageHeaders.CONTENT_TYPE,
MimeType.valueOf("application/json"));
assertThat((Map<String, String>) testPojoWithAnnotatedArguments.receivedArguments
.get(1)).containsEntry("testHeader", "testValue");
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(2))
.isEqualTo("application/json");
context.close();
}
@Test
public void testInputAnnotationAtMethodParameter() {
try {
SpringApplication.run(TestPojoWithInvalidInputAnnotatedArgument.class,
"--server.port=0");
fail("Exception expected: " + INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
@Test
public void testValidAnnotationAtMethodParameterWithPojoThatPassesValidation() {
ConfigurableApplicationContext context = SpringApplication.run(
TestPojoWithValidAnnotationThatPassesValidation.class, "--server.port=0");
TestPojoWithValidAnnotationThatPassesValidation testPojoWithValidAnnotationThatPassesValidation = context
.getBean(TestPojoWithValidAnnotationThatPassesValidation.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"" + id + "\"}")
.setHeader("contentType", MimeType.valueOf("application/json")).build());
assertThat(
testPojoWithValidAnnotationThatPassesValidation.receivedArguments.get(0))
.hasFieldOrPropertyWithValue("foo", id);
context.close();
}
@Test
public void testValidAnnotationAtMethodParameterWithPojoThatFailsValidation() {
ConfigurableApplicationContext context = SpringApplication.run(
TestPojoWithValidAnnotationThatPassesValidation.class, "--server.port=0");
Sink sink = context.getBean(Sink.class);
try {
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"\"}")
.setHeader("contentType", MimeType.valueOf("application/json"))
.build());
fail("Exception expected: MethodArgumentNotValidException!");
}
catch (MethodArgumentNotValidException e) {
assertThat(e.getMessage()).contains(
"default message [foo]]; default message [must not be blank]]");
}
context.close();
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
List<Object> receivedArguments = new ArrayList<>();
@StreamListener(Processor.INPUT)
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo,
@Headers Map<String, Object> headers,
@Header(MessageHeaders.CONTENT_TYPE) String contentType) {
this.receivedArguments.add(fooPojo);
this.receivedArguments.add(headers);
this.receivedArguments.add(contentType);
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithInvalidInputAnnotatedArgument {
List<Object> receivedArguments = new ArrayList<>();
@StreamListener
public void receive(
@Input(Processor.INPUT) @Payload StreamListenerTestUtils.FooPojo fooPojo,
@Headers Map<String, Object> headers,
@Header(MessageHeaders.CONTENT_TYPE) String contentType) {
this.receivedArguments.add(fooPojo);
this.receivedArguments.add(headers);
this.receivedArguments.add(contentType);
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithValidAnnotationThatPassesValidation {
List<Object> receivedArguments = new ArrayList<>();
@StreamListener(Processor.INPUT)
public void receive(
@Valid StreamListenerTestUtils.PojoWithValidation pojoWithValidation) {
this.receivedArguments.add(pojoWithValidation);
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.handler.annotation.Payload;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.stream.config.BindingServiceConfiguration.STREAM_LISTENER_ANNOTATION_BEAN_POST_PROCESSOR_NAME;
/**
* @author Marius Bogoevici
*/
public class StreamListenerAnnotationBeanPostProcessorOverrideTest {
@Test
@SuppressWarnings("unchecked")
public void testOverrideStreamListenerAnnotationBeanPostProcessor() throws Exception {
ConfigurableApplicationContext context = SpringApplication
.run(TestPojoWithAnnotatedArguments.class, "--server.port=0");
TestPojoWithAnnotatedArguments testPojoWithAnnotatedArguments = context
.getBean(TestPojoWithAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "foo").build());
sink.input().send(MessageBuilder.withPayload("{\"bar\":\"foofoo" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "bar").build());
assertThat(testPojoWithAnnotatedArguments.receivedFoo).hasSize(1);
assertThat(testPojoWithAnnotatedArguments.receivedFoo.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
context.close();
}
@Configuration
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
List<StreamListenerTestUtils.FooPojo> receivedFoo = new ArrayList<>();
/**
* Overrides the default {@link StreamListenerAnnotationBeanPostProcessor}.
*/
@Bean(name = STREAM_LISTENER_ANNOTATION_BEAN_POST_PROCESSOR_NAME)
public static StreamListenerAnnotationBeanPostProcessor streamListenerAnnotationBeanPostProcessor() {
return new StreamListenerAnnotationBeanPostProcessor() {
@Override
protected StreamListener postProcessAnnotation(
StreamListener originalAnnotation, Method annotatedMethod) {
Map<String, Object> attributes = new HashMap<>(
AnnotationUtils.getAnnotationAttributes(originalAnnotation));
attributes.put("condition",
"headers['type']=='" + originalAnnotation.condition() + "'");
return AnnotationUtils.synthesizeAnnotation(attributes,
StreamListener.class, annotatedMethod);
}
};
}
@StreamListener(value = Sink.INPUT, condition = "foo")
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedFoo.add(fooPojo);
}
}
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import static org.assertj.core.api.Assertions.assertThat;
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
@StreamListener
@interface EventHandler {
/**
* The name of the binding target (e.g. channel) that the method subscribes to.
* @return the name of the binding target.
*/
@AliasFor(annotation = StreamListener.class, attribute = "target")
String value() default "";
/**
* The name of the binding target (e.g. channel) that the method subscribes to.
* @return the name of the binding target.
*/
@AliasFor(annotation = StreamListener.class, attribute = "target")
String target() default "";
/**
* A condition that must be met by all items that are dispatched to this method.
* @return a SpEL expression that must evaluate to a {@code boolean} value.
*/
@AliasFor(annotation = StreamListener.class, attribute = "condition")
String condition() default "";
}
/**
* @author David Turanski
*/
public class StreamListenerAsMetaAnnotationTests {
@Test
public void testCustomAnnotation() {
ConfigurableApplicationContext context = SpringApplication
.run(TestPojoWithCustomAnnotatedArguments.class, "--server.port=0");
TestPojoWithCustomAnnotatedArguments testPojoWithAnnotatedArguments = context
.getBean(TestPojoWithCustomAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "foo").build());
assertThat(testPojoWithAnnotatedArguments.receivedFoo).hasSize(1);
assertThat(testPojoWithAnnotatedArguments.receivedFoo.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
context.close();
}
@Test
public void testAnnotation() {
ConfigurableApplicationContext context = SpringApplication
.run(TestPojoWithAnnotatedArguments.class, "--server.port=0");
TestPojoWithAnnotatedArguments testPojoWithAnnotatedArguments = context
.getBean(TestPojoWithAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "foo").build());
assertThat(testPojoWithAnnotatedArguments.receivedFoo).hasSize(1);
assertThat(testPojoWithAnnotatedArguments.receivedFoo.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
context.close();
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithCustomAnnotatedArguments {
List<StreamListenerTestUtils.FooPojo> receivedFoo = new ArrayList<>();
List<StreamListenerTestUtils.BarPojo> receivedBar = new ArrayList<>();
@EventHandler(value = Sink.INPUT, condition = "headers['type']=='foo'")
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedFoo.add(fooPojo);
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
List<StreamListenerTestUtils.FooPojo> receivedFoo = new ArrayList<>();
List<StreamListenerTestUtils.BarPojo> receivedBar = new ArrayList<>();
@StreamListener(value = Sink.INPUT, condition = "headers['type']=='foo'")
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedFoo.add(fooPojo);
}
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
public class StreamListenerContentTypeConversionTests {
@Test
public void testContentTypeConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication
.run(TestSinkWithContentTypeConversion.class, "--server.port=0");
@SuppressWarnings("unchecked")
TestSinkWithContentTypeConversion testSink = context
.getBean(TestSinkWithContentTypeConversion.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
assertThat(testSink.latch.await(10, TimeUnit.SECONDS));
assertThat(testSink.receivedArguments).hasSize(1);
assertThat(testSink.receivedArguments.get(0)).hasFieldOrPropertyWithValue("foo",
"barbar" + id);
context.close();
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestSinkWithContentTypeConversion {
List<StreamListenerTestUtils.FooPojo> receivedArguments = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
@StreamListener(Sink.INPUT)
public void receive(StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedArguments.add(fooPojo);
this.latch.countDown();
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerErrorMessages;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
public class StreamListenerDuplicateMappingTests {
@Test
@SuppressWarnings("unchecked")
public void testMultipleMappingsWithReturnValue() {
ConfigurableApplicationContext context = null;
try {
context = SpringApplication.run(TestMultipleMappingsWithReturnValue.class,
"--server.port=0");
fail("Exception expected on duplicate mapping");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).startsWith(
StreamListenerErrorMessages.MULTIPLE_VALUE_RETURNING_METHODS);
}
finally {
if (context != null) {
context.close();
}
}
}
@Test
public void testDuplicateMappingFromAbstractMethod() {
ConfigurableApplicationContext context = null;
try {
context = SpringApplication.run(TestDuplicateMappingFromAbstractMethod.class,
"--server.port=0");
}
catch (BeanCreationException e) {
String errorMessage = e.getCause().getMessage()
.startsWith("Duplicate @StreamListener mapping")
? "Duplicate mapping exception is not expected"
: "Test failed with exception";
fail(errorMessage + ": " + e.getMessage());
}
finally {
if (context != null) {
context.close();
}
}
}
public interface GenericSink<T extends Base> {
void testMethod(T msg);
}
public interface Base {
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestMultipleMappingsWithReturnValue {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(Message<String> fooMessage) {
return null;
}
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receiveDuplicateMapping(Message<String> fooMessage) {
return null;
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestDuplicateMappingFromAbstractMethod
implements GenericSink<TestBase> {
@Override
@StreamListener(Sink.INPUT)
public void testMethod(TestBase msg) {
}
}
public class TestBase implements Base {
}
}

View File

@@ -1,150 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
@RunWith(Parameterized.class)
public class StreamListenerHandlerBeanTests {
private Class<?> configClass;
public StreamListenerHandlerBeanTests(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(TestHandlerBeanWithSendTo.class, TestHandlerBean2.class);
}
@Test
@SuppressWarnings("unchecked")
public void testHandlerBean() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(this.configClass,
"--spring.cloud.stream.bindings.output.contentType=application/json",
"--server.port=0");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
HandlerBean handlerBean = context.getBean(HandlerBean.class);
Assertions.assertThat(handlerBean.receivedPojos).hasSize(1);
Assertions.assertThat(handlerBean.receivedPojos.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
assertThat(message.getPayload()).isEqualTo("{\"bar\":\"barbar" + id + "\"}");
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_JSON));
context.close();
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestHandlerBeanWithSendTo {
@Bean
public HandlerBeanWithSendTo handlerBean() {
return new HandlerBeanWithSendTo();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestHandlerBean2 {
@Bean
public HandlerBeanWithOutput handlerBean() {
return new HandlerBeanWithOutput();
}
}
public static class HandlerBeanWithSendTo extends HandlerBean {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(
StreamListenerTestUtils.FooPojo fooMessage) {
this.receivedPojos.add(fooMessage);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooMessage.getFoo());
return barPojo;
}
}
public static class HandlerBeanWithOutput extends HandlerBean {
@StreamListener(Processor.INPUT)
@Output(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(
StreamListenerTestUtils.FooPojo fooMessage) {
this.receivedPojos.add(fooMessage);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooMessage.getFoo());
return barPojo;
}
}
public static class HandlerBean {
List<StreamListenerTestUtils.FooPojo> receivedPojos = new ArrayList<>();
}
}

View File

@@ -1,623 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerErrorMessages;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.annotation.Router;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INPUT_AT_STREAM_LISTENER;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_INBOUND_NAME;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_OUTBOUND_NAME;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_OUTPUT_VALUES;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.NO_INPUT_DESTINATION;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.RETURN_TYPE_NO_OUTBOUND_SPECIFIED;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Gary Russell
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
public class StreamListenerHandlerMethodTests {
@Test
public void testInvalidInputOnMethod() throws Exception {
try {
SpringApplication.run(TestInvalidInputOnMethod.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected: " + INPUT_AT_STREAM_LISTENER);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INPUT_AT_STREAM_LISTENER);
}
}
@SuppressWarnings("unchecked")
@Test
public void testMethodWithObjectAsMethodArgument() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestMethodWithObjectAsMethodArgument.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.input.contentType=text/plain",
"--spring.cloud.stream.bindings.output.contentType=text/plain");
Processor processor = context.getBean(Processor.class);
final String testMessage = "testing";
processor.input().send(MessageBuilder.withPayload(testMessage).build());
MessageCollector messageCollector = context.getBean(MessageCollector.class);
Message<String> result = (Message<String>) messageCollector
.forChannel(processor.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo(testMessage.toUpperCase());
context.close();
}
@SuppressWarnings("unchecked")
@Test
/**
* @since 2.0 : This test is an example of the new behavior of 2.0 when it comes to
* contentType handling. The default contentType being JSON in order to be able to
* check a message without quotes the user needs to set the input/output contentType
* accordingly Also, received messages are always of Message<byte[]> now.
*/
public void testMethodHeadersPropagatged() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestMethodHeadersPropagated.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.input.contentType=text/plain",
"--spring.cloud.stream.bindings.output.contentType=text/plain");
Processor processor = context.getBean(Processor.class);
final String testMessage = "testing";
processor.input().send(
MessageBuilder.withPayload(testMessage).setHeader("foo", "bar").build());
MessageCollector messageCollector = context.getBean(MessageCollector.class);
Message<String> result = (Message<String>) messageCollector
.forChannel(processor.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo(testMessage.toUpperCase());
assertThat(result.getHeaders().get("foo")).isEqualTo("bar");
context.close();
}
@SuppressWarnings("unchecked")
@Test
@Disabled
public void testMethodHeadersNotPropagatged() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestMethodHeadersNotPropagated.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.input.contentType=text/plain",
"--spring.cloud.stream.bindings.output.contentType=text/plain");
Processor processor = context.getBean(Processor.class);
final String testMessage = "testing";
processor.input().send(
MessageBuilder.withPayload(testMessage).setHeader("foo", "bar").build());
MessageCollector messageCollector = context.getBean(MessageCollector.class);
Message<String> result = (Message<String>) messageCollector
.forChannel(processor.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo(testMessage.toUpperCase());
assertThat(result.getHeaders().get("foo")).isNull();
context.close();
}
// TODO: Handle dynamic destinations and contentType
@SuppressWarnings("unchecked")
public void testStreamListenerMethodWithTargetBeanFromOutside() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestStreamListenerMethodWithTargetBeanFromOutside.class,
"--server.port=0", "--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.input.contentType=text/plain",
"--spring.cloud.stream.bindings.output.contentType=text/plain");
Sink sink = context.getBean(Sink.class);
final String testMessageToSend = "testing";
sink.input().send(MessageBuilder.withPayload(testMessageToSend).build());
DirectChannel directChannel = (DirectChannel) context
.getBean(testMessageToSend.toUpperCase(), MessageChannel.class);
MessageCollector messageCollector = context.getBean(MessageCollector.class);
Message<String> result = (Message<String>) messageCollector
.forChannel(directChannel).poll(1000, TimeUnit.MILLISECONDS);
sink.input().send(MessageBuilder.withPayload(testMessageToSend).build());
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo(testMessageToSend.toUpperCase());
context.close();
}
@Test
public void testInvalidReturnTypeWithSendToAndOutput() throws Exception {
try {
SpringApplication.run(TestReturnTypeWithMultipleOutput.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected: " + RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED);
}
}
@Test
public void testInvalidReturnTypeWithNoOutput() throws Exception {
try {
SpringApplication.run(TestInvalidReturnTypeWithNoOutput.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected: " + RETURN_TYPE_NO_OUTBOUND_SPECIFIED);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(RETURN_TYPE_NO_OUTBOUND_SPECIFIED);
}
}
@Test
public void testInvalidInputAnnotationWithNoValue() throws Exception {
try {
SpringApplication.run(TestInvalidInputAnnotationWithNoValue.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected: " + INVALID_INBOUND_NAME);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INVALID_INBOUND_NAME);
}
}
@Test
public void testInvalidOutputAnnotationWithNoValue() throws Exception {
try {
SpringApplication.run(TestInvalidOutputAnnotationWithNoValue.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected: " + INVALID_OUTBOUND_NAME);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INVALID_OUTBOUND_NAME);
}
}
@Test
public void testMethodInvalidInboundName() throws Exception {
try {
SpringApplication.run(TestMethodInvalidInboundName.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected on using invalid inbound name");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
@Test
public void testMethodInvalidOutboundName() throws Exception {
try {
SpringApplication.run(TestMethodInvalidOutboundName.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected on using invalid outbound name");
}
catch (NoSuchBeanDefinitionException e) {
assertThat(e.getMessage()).contains("invalid");
}
}
@Test
public void testAmbiguousMethodArguments1() throws Exception {
try {
SpringApplication.run(TestAmbiguousMethodArguments1.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected: " + AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage())
.contains(AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS);
}
}
@Test
public void testAmbiguousMethodArguments2() throws Exception {
try {
SpringApplication.run(TestAmbiguousMethodArguments2.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected:" + AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage())
.contains(AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS);
}
}
@Test
public void testMethodWithInputAsMethodAndParameter() throws Exception {
try {
SpringApplication.run(TestMethodWithInputAsMethodAndParameter.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected: " + INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
@Test
public void testMethodWithOutputAsMethodAndParameter() throws Exception {
try {
SpringApplication.run(TestMethodWithOutputAsMethodAndParameter.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected:" + INVALID_OUTPUT_VALUES);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).startsWith(INVALID_OUTPUT_VALUES);
}
}
@Test
public void testMethodWithoutInput() throws Exception {
try {
SpringApplication.run(TestMethodWithoutInput.class, "--server.port=0",
"--spring.jmx.enabled=false");
fail("Exception expected when inbound target is not set");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(NO_INPUT_DESTINATION);
}
}
@Test
public void testMethodWithMultipleInputParameters() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestMethodWithMultipleInputParameters.class, "--server.port=0",
"--spring.jmx.enabled=false");
Processor processor = context.getBean(Processor.class);
StreamListenerTestUtils.FooInboundChannel1 inboundChannel2 = context
.getBean(StreamListenerTestUtils.FooInboundChannel1.class);
final CountDownLatch latch = new CountDownLatch(2);
((SubscribableChannel) processor.output()).subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Assert.isTrue(
message.getPayload().equals("footesting")
|| message.getPayload().equals("BARTESTING"),
"Assert failed");
latch.countDown();
}
});
processor.input().send(MessageBuilder.withPayload("{\"foo\":\"fooTESTing\"}")
.setHeader("contentType", "application/json").build());
inboundChannel2.input()
.send(MessageBuilder.withPayload("{\"bar\":\"bartestING\"}")
.setHeader("contentType", "application/json").build());
assertThat(latch.await(1, TimeUnit.SECONDS));
context.close();
}
@Test
public void testMethodWithMultipleOutputParameters() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestMethodWithMultipleOutputParameters.class, "--server.port=0",
"--spring.jmx.enabled=false");
Processor processor = context.getBean(Processor.class);
StreamListenerTestUtils.FooOutboundChannel1 source2 = context
.getBean(StreamListenerTestUtils.FooOutboundChannel1.class);
final CountDownLatch latch = new CountDownLatch(2);
((SubscribableChannel) processor.output()).subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Assert.isTrue(message.getPayload().equals("testing"), "Assert failed");
Assert.isTrue(message.getHeaders().get("output").equals("output2"),
"Assert failed");
latch.countDown();
}
});
((SubscribableChannel) source2.output()).subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Assert.isTrue(message.getPayload().equals("TESTING"), "Assert failed");
Assert.isTrue(message.getHeaders().get("output").equals("output1"),
"Assert failed");
latch.countDown();
}
});
processor.input().send(MessageBuilder.withPayload("testING")
.setHeader("output", "output1").build());
processor.input().send(MessageBuilder.withPayload("TESTing")
.setHeader("output", "output2").build());
assertThat(latch.await(1, TimeUnit.SECONDS));
context.close();
}
@EnableBinding({ Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class })
@EnableAutoConfiguration
public static class TestMethodWithMultipleOutputParameters {
@StreamListener
public void receive(@Input(Processor.INPUT) SubscribableChannel input,
@Output(Processor.OUTPUT) final MessageChannel output1,
@Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT) final MessageChannel output2) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
if (message.getHeaders().get("output").equals("output1")) {
output1.send(org.springframework.messaging.support.MessageBuilder
.withPayload(
message.getPayload().toString().toUpperCase())
.build());
}
else if (message.getHeaders().get("output").equals("output2")) {
output2.send(org.springframework.messaging.support.MessageBuilder
.withPayload(
message.getPayload().toString().toLowerCase())
.build());
}
}
});
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestMethodWithoutInput {
@StreamListener
public void receive(StreamListenerTestUtils.FooPojo fooPojo) {
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestMethodWithObjectAsMethodArgument {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(Object received) {
return received.toString().toUpperCase();
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestMethodHeadersPropagated {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(String received) {
return received.toUpperCase();
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestMethodHeadersNotPropagated {
@StreamListener(value = Processor.INPUT, copyHeaders = "${foo.bar:false}")
@SendTo(Processor.OUTPUT)
public String receive(String received) {
return received.toUpperCase();
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestStreamListenerMethodWithTargetBeanFromOutside {
private static final String ROUTER_QUEUE = "routeInstruction";
@StreamListener(Sink.INPUT)
@SendTo(ROUTER_QUEUE)
public Message<String> convertMessageBody(Message<String> message) {
return new DefaultMessageBuilderFactory()
.withPayload(message.getPayload().toUpperCase()).build();
}
@Router(inputChannel = ROUTER_QUEUE)
public String route(String message) {
return message.toUpperCase();
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestInvalidInputOnMethod {
@StreamListener
@Input(Sink.INPUT)
public void receive(StreamListenerTestUtils.FooPojo fooPojo) {
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestAmbiguousMethodArguments1 {
@StreamListener(Processor.INPUT)
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo,
String value) {
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestAmbiguousMethodArguments2 {
@StreamListener(Processor.INPUT)
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo,
@Payload StreamListenerTestUtils.BarPojo barPojo) {
}
}
@EnableBinding({ Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class })
@EnableAutoConfiguration
public static class TestReturnTypeWithMultipleOutput {
@StreamListener
public String receive(@Input(Processor.INPUT) SubscribableChannel input1,
@Output(Processor.OUTPUT) MessageChannel output1,
@Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT) MessageChannel output2) {
return "foo";
}
}
@EnableBinding({ Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class })
@EnableAutoConfiguration
public static class TestInvalidReturnTypeWithNoOutput {
@StreamListener
public String receive(@Input(Processor.INPUT) SubscribableChannel input1) {
return "foo";
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestInvalidInputAnnotationWithNoValue {
@StreamListener
public void receive(@Input SubscribableChannel input) {
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestInvalidOutputAnnotationWithNoValue {
@StreamListener
public void receive(@Input(Processor.OUTPUT) SubscribableChannel input,
@Output MessageChannel output) {
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestMethodInvalidInboundName {
@StreamListener
public void receive(@Input("invalid") SubscribableChannel input) {
}
}
@EnableBinding({ Processor.class })
@EnableAutoConfiguration
public static class TestMethodInvalidOutboundName {
@StreamListener
public void receive(@Input(Processor.INPUT) SubscribableChannel input,
@Output("invalid") MessageChannel output) {
}
}
@EnableBinding({ Sink.class })
@EnableAutoConfiguration
public static class TestMethodWithInputAsMethodAndParameter {
@StreamListener
public void receive(@Input(Sink.INPUT) StreamListenerTestUtils.FooPojo fooPojo) {
}
}
@EnableBinding({ Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class })
@EnableAutoConfiguration
public static class TestMethodWithOutputAsMethodAndParameter {
@StreamListener
@Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT)
public void receive(@Input(Processor.INPUT) SubscribableChannel input,
@Output(Processor.OUTPUT) final MessageChannel output1) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output1.send(org.springframework.messaging.support.MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
@EnableBinding({ Processor.class, StreamListenerTestUtils.FooInboundChannel1.class })
@EnableAutoConfiguration
public static class TestMethodWithMultipleInputParameters {
@StreamListener
public void receive(@Input(Processor.INPUT) SubscribableChannel input1,
@Input(StreamListenerTestUtils.FooInboundChannel1.INPUT) SubscribableChannel input2,
final @Output(Processor.OUTPUT) MessageChannel output) {
input1.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(org.springframework.messaging.support.MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
input2.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(org.springframework.messaging.support.MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
}

View File

@@ -1,125 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
@RunWith(Parameterized.class)
public class StreamListenerMessageArgumentTests {
private Class<?> configClass;
public StreamListenerMessageArgumentTests(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(new Class[] { TestPojoWithMessageArgument1.class,
TestPojoWithMessageArgument2.class });
}
@Test
@SuppressWarnings("unchecked")
public void testMessageArgument() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(this.configClass,
"--server.port=0",
"--spring.cloud.stream.bindings.output.contentType=text/plain",
"--spring.jmx.enabled=false");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("barbar" + id)
.setHeader("contentType", "text/plain").build());
TestPojoWithMessageArgument testPojoWithMessageArgument = context
.getBean(TestPojoWithMessageArgument.class);
assertThat(testPojoWithMessageArgument.receivedMessages).hasSize(1);
assertThat(testPojoWithMessageArgument.receivedMessages.get(0).getPayload())
.isEqualTo("barbar" + id);
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
assertThat(message.getPayload()).contains("barbar" + id);
context.close();
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageArgument1 extends TestPojoWithMessageArgument {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(Message<String> fooMessage) {
this.receivedMessages.add(fooMessage);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooMessage.getPayload());
return barPojo;
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageArgument2 extends TestPojoWithMessageArgument {
@StreamListener(Processor.INPUT)
@Output(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(Message<String> fooMessage) {
this.receivedMessages.add(fooMessage);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooMessage.getPayload());
return barPojo;
}
}
public static class TestPojoWithMessageArgument {
List<Message<String>> receivedMessages = new ArrayList<>();
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.mockito.Mockito.verify;
/**
* See issue https://github.com/spring-cloud/spring-cloud-stream/issues/1080
*
* StreamListener method called twice when using @SpyBean
*
* @author Soby Chacko
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class StreamListenerMethodRegisteredOnlyOnceTest {
@Autowired
private SomeSink sink;
@SpyBean
private SomeHandler handler;
@Test
public void should_handleSomeMessage() {
this.sink.channel().send(new GenericMessage<>("Payload"));
verify(this.handler).handleMessage(); // should only be invoked once.
}
public interface SomeSink {
@Input(Sink.INPUT)
SubscribableChannel channel();
}
@EnableBinding(SomeSink.class)
@EnableAutoConfiguration
public static class SomeHandler {
@StreamListener(Sink.INPUT)
public void handleMessage() {
}
}
}

View File

@@ -1,200 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*
*/
@RunWith(StreamListenerMethodReturnWithConversionTests.class)
@Suite.SuiteClasses({
StreamListenerMethodReturnWithConversionTests.TestReturnConversion.class,
StreamListenerMethodReturnWithConversionTests.TestReturnNoConversion.class })
public class StreamListenerMethodReturnWithConversionTests extends Suite {
public StreamListenerMethodReturnWithConversionTests(Class<?> klass,
RunnerBuilder builder) throws InitializationError {
super(klass, builder);
}
@RunWith(Parameterized.class)
public static class TestReturnConversion {
private Class<?> configClass;
public TestReturnConversion(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(new Class[] { TestPojoWithMimeType1.class,
TestPojoWithMimeType2.class });
}
@Test
@SuppressWarnings("unchecked")
public void testReturnConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
this.configClass,
"--spring.cloud.stream.bindings.output.contentType=application/json",
"--server.port=0", "--spring.jmx.enabled=false");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMimeType testPojoWithMimeType = context
.getBean(TestPojoWithMimeType.class);
Assertions.assertThat(testPojoWithMimeType.receivedPojos).hasSize(1);
Assertions.assertThat(testPojoWithMimeType.receivedPojos.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
assertThat(new String(message.getPayload()))
.isEqualTo("{\"bar\":\"barbar" + id + "\"}");
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_JSON));
context.close();
}
}
@RunWith(Parameterized.class)
public static class TestReturnNoConversion {
private Class<?> configClass;
private ObjectMapper mapper = new ObjectMapper();
public TestReturnNoConversion(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(new Class[] { TestPojoWithMimeType1.class,
TestPojoWithMimeType2.class });
}
@Test
@SuppressWarnings("unchecked")
public void testReturnNoConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
this.configClass, "--server.port=0", "--spring.jmx.enabled=false");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMimeType testPojoWithMimeType = context
.getBean(TestPojoWithMimeType.class);
Assertions.assertThat(testPojoWithMimeType.receivedPojos).hasSize(1);
Assertions.assertThat(testPojoWithMimeType.receivedPojos.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
StreamListenerTestUtils.BarPojo barPojo = this.mapper.readValue(
message.getPayload(), StreamListenerTestUtils.BarPojo.class);
assertThat(barPojo.getBar()).isEqualTo("barbar" + id);
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE,
MimeType.class) != null);
context.close();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMimeType1 extends TestPojoWithMimeType {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(
StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooPojo.getFoo());
return barPojo;
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMimeType2 extends TestPojoWithMimeType {
@StreamListener(Processor.INPUT)
@Output(Processor.OUTPUT)
public StreamListenerTestUtils.BarPojo receive(
StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooPojo.getFoo());
return barPojo;
}
}
public static class TestPojoWithMimeType {
List<StreamListenerTestUtils.FooPojo> receivedPojos = new ArrayList<>();
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor;
import org.springframework.cloud.stream.binding.StreamListenerSetupMethodOrchestrator;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Soby Chacko
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class StreamListenerMethodSetupOrchestratorTests {
@SpyBean
CustomOrchestrator customOrchestrator;
@SpyBean
MultipleStreamListenerProcessor multipleStreamListenerProcessor;
@SpyBean
StreamListenerAnnotationBeanPostProcessor streamListenerAnnotationBeanPostProcessor;
@Test
@SuppressWarnings("unchecked")
public void testCustomStreamListenerOrchestratorAndDefaultTogetherInSameContext()
throws Exception {
// Two StreamListener methods, so 2 invocations
verify(this.customOrchestrator, times(2)).supports(any());
Method method = this.multipleStreamListenerProcessor.getClass()
.getMethod("handleMessage");
StreamListener streamListener = AnnotatedElementUtils.findMergedAnnotation(method,
StreamListener.class);
// verify that the invocation happened on the custom Orchestrator
verify(this.customOrchestrator).orchestrateStreamListenerSetupMethod(
streamListener, method, this.multipleStreamListenerProcessor);
Method method1 = this.multipleStreamListenerProcessor.getClass()
.getMethod("produceString");
StreamListener streamListener1 = AnnotatedElementUtils
.findMergedAnnotation(method, StreamListener.class);
// Verify that the invocation did not happen on the custom orchestrator
verify(this.customOrchestrator, never()).orchestrateStreamListenerSetupMethod(
streamListener1, method1, this.multipleStreamListenerProcessor);
Field field = ReflectionUtils.findField(
this.streamListenerAnnotationBeanPostProcessor.getClass(),
"streamListenerSetupMethodOrchestrators");
ReflectionUtils.makeAccessible(field);
Set<StreamListenerSetupMethodOrchestrator> field1;
field1 = (LinkedHashSet<StreamListenerSetupMethodOrchestrator>) ReflectionUtils
.getField(field, this.streamListenerAnnotationBeanPostProcessor);
List<StreamListenerSetupMethodOrchestrator> list = new ArrayList<>(field1);
// Ensure that the custom orchestrator did not support this request
assertThat(list.get(0).supports(method1)).isEqualTo(false);
// Ensure that we are using the default Orchestrator in
// StreamListenerAnnoatationBeanPostProcessor
assertThat(list.get(1).supports(method1)).isEqualTo(true);
}
public interface SomeProcessor {
@Input(Sink.INPUT)
SubscribableChannel channel1();
@Input("foobar")
SubscribableChannel channel2();
@Output(Source.OUTPUT)
MessageChannel channel3();
}
@EnableBinding(SomeProcessor.class)
@EnableAutoConfiguration
public static class MultipleStreamListenerProcessor {
@StreamListener(Sink.INPUT)
public void handleMessage() {
}
@StreamListener("foobar")
@SendTo("output")
public String produceString() {
return "foobar";
}
@Bean
public CustomOrchestrator myOrchestrator() {
return new CustomOrchestrator();
}
}
static class CustomOrchestrator implements StreamListenerSetupMethodOrchestrator {
@Override
public boolean supports(Method method) {
return method.getReturnType() != String.class;
}
@Override
public void orchestrateStreamListenerSetupMethod(StreamListener streamListener,
Method method, Object bean) {
// stub method
}
}
}

View File

@@ -1,125 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
@RunWith(Parameterized.class)
public class StreamListenerMethodWithReturnMessageTests {
private Class<?> configClass;
public StreamListenerMethodWithReturnMessageTests(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(new Class[] { TestPojoWithMessageReturn1.class,
TestPojoWithMessageReturn2.class });
}
@Test
@SuppressWarnings("unchecked")
public void testReturnMessage() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(this.configClass,
"--server.port=0", "--spring.jmx.enabled=false");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMessageReturn testPojoWithMessageReturn = context
.getBean(TestPojoWithMessageReturn.class);
Assertions.assertThat(testPojoWithMessageReturn.receivedPojos).hasSize(1);
Assertions.assertThat(testPojoWithMessageReturn.receivedPojos.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message).isNotNull();
assertThat(message.getPayload()).contains("barbar" + id);
context.close();
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageReturn1 extends TestPojoWithMessageReturn {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<?> receive(StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
StreamListenerTestUtils.BarPojo barPojo = new StreamListenerTestUtils.BarPojo();
barPojo.setBar(fooPojo.getFoo());
return MessageBuilder.withPayload(barPojo).setHeader("foo", "bar").build();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageReturn2 extends TestPojoWithMessageReturn {
@StreamListener(Processor.INPUT)
@Output(Processor.OUTPUT)
public Message<?> receive(StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
StreamListenerTestUtils.BarPojo bazPojo = new StreamListenerTestUtils.BarPojo();
bazPojo.setBar(fooPojo.getFoo());
return MessageBuilder.withPayload(bazPojo).setHeader("foo", "bar").build();
}
}
public static class TestPojoWithMessageReturn {
List<StreamListenerTestUtils.FooPojo> receivedPojos = new ArrayList<>();
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*/
@RunWith(Parameterized.class)
public class StreamListenerMethodWithReturnValueTests {
private Class<?> configClass;
public StreamListenerMethodWithReturnValueTests(Class<?> configClass) {
this.configClass = configClass;
}
@Parameterized.Parameters
public static Collection<?> InputConfigs() {
return Arrays.asList(
new Class[] { TestStringProcessor1.class, TestStringProcessor2.class });
}
@Test
@SuppressWarnings("unchecked")
public void testReturn() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(this.configClass,
"--server.port=0", "--spring.jmx.enabled=false");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input()
.send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
Message<String> message = (Message<String>) collector
.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
TestStringProcessor testStringProcessor = context
.getBean(TestStringProcessor.class);
Assertions.assertThat(testStringProcessor.receivedPojos).hasSize(1);
Assertions.assertThat(testStringProcessor.receivedPojos.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
assertThat(message).isNotNull();
assertThat(message.getPayload()).contains("barbar" + id);
context.close();
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestStringProcessor1 extends TestStringProcessor {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
return fooPojo.getFoo();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestStringProcessor2 extends TestStringProcessor {
@StreamListener(Processor.INPUT)
@Output(Processor.OUTPUT)
public String receive(StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedPojos.add(fooPojo);
return fooPojo.getFoo();
}
}
public static class TestStringProcessor {
List<StreamListenerTestUtils.FooPojo> receivedPojos = new ArrayList<>();
}
}

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import javax.validation.constraints.NotBlank;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
/**
* @author Ilayaperumal Gopinathan
*/
public class StreamListenerTestUtils {
public interface FooInboundChannel1 {
String INPUT = "foo1-input";
@Input(FooInboundChannel1.INPUT)
SubscribableChannel input();
}
public interface FooOutboundChannel1 {
String OUTPUT = "foo1-output";
@Output(FooOutboundChannel1.OUTPUT)
MessageChannel output();
}
public static class FooPojo {
private String foo;
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("FooPojo{");
sb.append("foo='").append(this.foo).append('\'');
sb.append('}');
return sb.toString();
}
}
public static class BarPojo {
private String bar;
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("BarPojo{");
sb.append("bar='").append(this.bar).append('\'');
sb.append('}');
return sb.toString();
}
}
public static class PojoWithValidation {
@NotBlank
private String foo;
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
}

View File

@@ -1,185 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerErrorMessages;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.MessageBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
public class StreamListenerWithAnnotatedInputOutputArgsTests {
@Test
public void testInputOutputArgs() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestInputOutputArgs.class, "--server.port=0",
"--spring.cloud.stream.bindings.output.contentType=text/plain",
"--spring.jmx.enabled=false");
sendMessageAndValidate(context);
}
@Test
public void testInputOutputArgsWithMoreParameters() {
try {
SpringApplication.run(TestInputOutputArgsWithMoreParameters.class,
"--server.port=0");
fail("Expected exception: " + INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
@Test
public void testInputOutputArgsWithInvalidBindableTarget() {
try {
SpringApplication.run(TestInputOutputArgsWithInvalidBindableTarget.class,
"--server.port=0", "--spring.jmx.enabled=false");
fail("Exception expected on using invalid bindable target as method parameter");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
@Test
public void testInputOutputArgsWithParameterOrderChanged() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(
TestInputOutputArgsWithParameterOrderChanged.class, "--server.port=0",
"--spring.cloud.stream.bindings.output.contentType=text/plain",
"--spring.jmx.enabled=false");
sendMessageAndValidate(context);
}
@SuppressWarnings("unchecked")
private void sendMessageAndValidate(ConfigurableApplicationContext context)
throws InterruptedException {
Processor processor = context.getBean(Processor.class);
processor.input().send(MessageBuilder.withPayload("hello")
.setHeader("contentType", "text/plain").build());
MessageCollector messageCollector = context.getBean(MessageCollector.class);
Message<String> result = (Message<String>) messageCollector
.forChannel(processor.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(result).isNotNull();
assertThat(result.getPayload()).isEqualTo("HELLO");
context.close();
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestInputOutputArgs {
@StreamListener
public void receive(@Input(Processor.INPUT) SubscribableChannel input,
@Output(Processor.OUTPUT) final MessageChannel output) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestInputOutputArgsWithMoreParameters {
@StreamListener
public void receive(@Input(Processor.INPUT) SubscribableChannel input,
@Output(Processor.OUTPUT) final MessageChannel output, String someArg) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestInputOutputArgsWithInvalidBindableTarget {
@StreamListener
public void receive(@Input("invalid") SubscribableChannel input,
@Output(Processor.OUTPUT) final MessageChannel output) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestInputOutputArgsWithParameterOrderChanged {
@StreamListener
public void receive(@Output(Processor.OUTPUT) final MessageChannel output,
@Input("input") SubscribableChannel input) {
input.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
output.send(MessageBuilder
.withPayload(message.getPayload().toString().toUpperCase())
.build());
}
});
}
}
}

View File

@@ -1,142 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerErrorMessages;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.annotation.Payload;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Marius Bogoevici
*/
public class StreamListenerWithConditionsTest {
@Test
public void testAnnotatedArgumentsWithConditionalClass() throws Exception {
ConfigurableApplicationContext context = SpringApplication
.run(TestPojoWithAnnotatedArguments.class, "--server.port=0");
TestPojoWithAnnotatedArguments testPojoWithAnnotatedArguments = context
.getBean(TestPojoWithAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"foo\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "foo").build());
sink.input().send(MessageBuilder.withPayload("{\"bar\":\"foofoo" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "bar").build());
sink.input().send(MessageBuilder.withPayload("{\"bar\":\"foofoo" + id + "\"}")
.setHeader("contentType", "application/json")
.setHeader("testHeader", "testValue").setHeader("type", "qux").build());
assertThat(testPojoWithAnnotatedArguments.receivedFoo).hasSize(1);
assertThat(testPojoWithAnnotatedArguments.receivedFoo.get(0))
.hasFieldOrPropertyWithValue("foo", "barbar" + id);
assertThat(testPojoWithAnnotatedArguments.receivedBar).hasSize(1);
assertThat(testPojoWithAnnotatedArguments.receivedBar.get(0))
.hasFieldOrPropertyWithValue("bar", "foofoo" + id);
context.close();
}
@Test
public void testConditionalFailsWithReturnValue() throws Exception {
try {
ConfigurableApplicationContext context = SpringApplication.run(
TestConditionalOnMethodWithReturnValueFails.class, "--server.port=0");
context.close();
fail("Context creation failure expected");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(
StreamListenerErrorMessages.CONDITION_ON_METHOD_RETURNING_VALUE);
}
}
@Test
public void testConditionalFailsWithDeclarativeMethod() throws Exception {
try {
ConfigurableApplicationContext context = SpringApplication.run(
TestConditionalOnDeclarativeMethodFails.class, "--server.port=0");
context.close();
fail("Context creation failure expected");
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains(
StreamListenerErrorMessages.CONDITION_ON_DECLARATIVE_METHOD);
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
List<StreamListenerTestUtils.FooPojo> receivedFoo = new ArrayList<>();
List<StreamListenerTestUtils.BarPojo> receivedBar = new ArrayList<>();
@StreamListener(value = Sink.INPUT, condition = "headers['type']=='foo'")
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo) {
this.receivedFoo.add(fooPojo);
}
@StreamListener(target = Sink.INPUT, condition = "headers['type']=='bar'")
public void receive(@Payload StreamListenerTestUtils.BarPojo barPojo) {
this.receivedBar.add(barPojo);
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestConditionalOnDeclarativeMethodFails {
@StreamListener(condition = "headers['type']=='foo'")
public void receive(@Input("input") MessageChannel input) {
// do nothing
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestConditionalOnMethodWithReturnValueFails {
@StreamListener(value = Sink.INPUT, condition = "headers['type']=='foo'")
public String receive(String value) {
return null;
}
}
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.TestSupportBinder;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
* @since 1.2
*/
@RunWith(SpringJUnit4ClassRunner.class)
// @checkstyle:off
@SpringBootTest(classes = TextPlainToJsonConversionTest.FooProcessor.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
// @checkstyle:on
public class TextPlainToJsonConversionTest {
@Autowired
private Processor testProcessor;
@Autowired
private BinderFactory binderFactory;
private ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("unchecked")
@Test
public void testNoContentTypeToJsonConversionOnInput() throws Exception {
this.testProcessor.input()
.send(MessageBuilder.withPayload("{\"name\":\"Bar\"}").build());
Message<String> received = (Message<String>) ((TestSupportBinder) this.binderFactory
.getBinder(null, MessageChannel.class)).messageCollector()
.forChannel(this.testProcessor.output())
.poll(1, TimeUnit.SECONDS);
assertThat(received).isNotNull();
Foo foo = this.mapper.readValue(received.getPayload(), Foo.class);
assertThat(foo.getName()).isEqualTo("transformed-Bar");
}
/**
* @since 2.0: Conversion from text/plain -> json is no longer supported. Strict
* contentType only.
*/
@Test(expected = MessagingException.class)
public void testTextPlainToJsonConversionOnInput() {
this.testProcessor.input().send(MessageBuilder.withPayload("{\"name\":\"Bar\"}")
.setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build());
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class FooProcessor {
@StreamListener("input")
@SendTo("output")
public Foo consume(Foo foo) {
Foo returnFoo = new Foo();
returnFoo.setName("transformed-" + foo.getName());
return returnFoo;
}
}
public static class Foo {
private String name;
public Foo() {
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Foo{name='" + this.name + "'}";
}
}
}

View File

@@ -0,0 +1,123 @@
///*
// * Copyright 2017-2018 the original author or authors.
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * https://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package org.springframework.cloud.stream.config;
//
//import java.util.concurrent.TimeUnit;
//
//import com.fasterxml.jackson.databind.ObjectMapper;
//import org.junit.Test;
//import org.junit.runner.RunWith;
//
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
//import org.springframework.boot.test.context.SpringBootTest;
//import org.springframework.cloud.stream.annotation.EnableBinding;
//import org.springframework.cloud.stream.annotation.StreamListener;
//import org.springframework.cloud.stream.binder.BinderFactory;
//import org.springframework.cloud.stream.messaging.Processor;
//import org.springframework.cloud.stream.test.binder.TestSupportBinder;
//import org.springframework.integration.support.MessageBuilder;
//import org.springframework.messaging.Message;
//import org.springframework.messaging.MessageChannel;
//import org.springframework.messaging.MessageHeaders;
//import org.springframework.messaging.MessagingException;
//import org.springframework.messaging.handler.annotation.SendTo;
//import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//
//import static org.assertj.core.api.Assertions.assertThat;
//
///**
// * @author Marius Bogoevici
// * @author Vinicius Carvalho
// * @author Oleg Zhurakousky
// * @since 1.2
// */
//@RunWith(SpringJUnit4ClassRunner.class)
//// @checkstyle:off
//@SpringBootTest(classes = TextPlainToJsonConversionTest.FooProcessor.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
//// @checkstyle:on
//public class TextPlainToJsonConversionTest {
//
// @Autowired
// private Processor testProcessor;
//
// @Autowired
// private BinderFactory binderFactory;
//
// private ObjectMapper mapper = new ObjectMapper();
//
// @SuppressWarnings("unchecked")
// @Test
// public void testNoContentTypeToJsonConversionOnInput() throws Exception {
// this.testProcessor.input()
// .send(MessageBuilder.withPayload("{\"name\":\"Bar\"}").build());
// Message<String> received = (Message<String>) ((TestSupportBinder) this.binderFactory
// .getBinder(null, MessageChannel.class)).messageCollector()
// .forChannel(this.testProcessor.output())
// .poll(1, TimeUnit.SECONDS);
// assertThat(received).isNotNull();
// Foo foo = this.mapper.readValue(received.getPayload(), Foo.class);
// assertThat(foo.getName()).isEqualTo("transformed-Bar");
// }
//
// /**
// * @since 2.0: Conversion from text/plain -> json is no longer supported. Strict
// * contentType only.
// */
// @Test(expected = MessagingException.class)
// public void testTextPlainToJsonConversionOnInput() {
// this.testProcessor.input().send(MessageBuilder.withPayload("{\"name\":\"Bar\"}")
// .setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build());
// }
//
// @EnableBinding(Processor.class)
// @EnableAutoConfiguration
// public static class FooProcessor {
//
// @StreamListener("input")
// @SendTo("output")
// public Foo consume(Foo foo) {
// Foo returnFoo = new Foo();
// returnFoo.setName("transformed-" + foo.getName());
// return returnFoo;
// }
//
// }
//
// public static class Foo {
//
// private String name;
//
// public Foo() {
// }
//
// public String getName() {
// return this.name;
// }
//
// public void setName(String name) {
// this.name = name;
// }
//
// @Override
// public String toString() {
// return "Foo{name='" + this.name + "'}";
// }
//
// }
//
//}

View File

@@ -1,272 +0,0 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.config.contentType;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
*/
@SuppressWarnings("unchecked")
public class ContentTypeTests {
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testSendWithDefaultContentType() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
User user = new User("Alice");
source.output().send(MessageBuilder.withPayload(user).build());
Message<String> message = (Message<String>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
User received = this.mapper.readValue(message.getPayload(), User.class);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_JSON));
assertThat(user.getName()).isEqualTo(received.getName());
}
}
@Test
public void testSendJsonAsString() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
User user = new User("Alice");
String json = this.mapper.writeValueAsString(user);
source.output().send(MessageBuilder.withPayload(user).build());
Message<String> message = (Message<String>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_JSON));
assertThat(json).isEqualTo(message.getPayload());
}
}
@Test
public void testSendJsonString() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
source.output().send(MessageBuilder.withPayload("foo").build());
Message<String> message = (Message<String>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_JSON));
assertThat("foo").isEqualTo(message.getPayload());
}
}
@Test
public void testSendBynaryData() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
byte[] data = new byte[] { 0, 1, 2, 3 };
source.output()
.send(MessageBuilder.withPayload(data)
.setHeader(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.APPLICATION_OCTET_STREAM)
.build());
Message<byte[]> message = (Message<byte[]>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.APPLICATION_OCTET_STREAM));
assertThat(message.getPayload()).isEqualTo(data);
}
}
@Test
public void testSendBinaryDataWithContentType() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0", "--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=image/jpeg")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
byte[] data = new byte[] { 0, 1, 2, 3 };
source.output().send(MessageBuilder.withPayload(data).build());
Message<byte[]> message = (Message<byte[]>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(message.getPayload()).isEqualTo(data);
}
}
@Test
public void testSendBinaryDataWithContentTypeUsingHeaders() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
byte[] data = new byte[] { 0, 1, 2, 3 };
source.output().send(MessageBuilder.withPayload(data)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_JPEG)
.build());
Message<byte[]> message = (Message<byte[]>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.IMAGE_JPEG));
assertThat(message.getPayload()).isEqualTo(data);
}
}
@Test
public void testSendStringType() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SourceApplication.class, "--server.port=0", "--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=text/plain")) {
MessageCollector collector = context.getBean(MessageCollector.class);
Source source = context.getBean(Source.class);
User user = new User("Alice");
source.output().send(MessageBuilder.withPayload(user).build());
Message<String> message = (Message<String>) collector
.forChannel(source.output()).poll(1, TimeUnit.SECONDS);
assertThat(
message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
.includes(MimeTypeUtils.TEXT_PLAIN));
assertThat(message.getPayload()).isEqualTo(user.toString());
}
}
@Test
public void testReceiveWithDefaults() throws Exception {
try (ConfigurableApplicationContext context = SpringApplication.run(
SinkApplication.class, "--server.port=0", "--spring.jmx.enabled=false")) {
TestSink testSink = context.getBean(TestSink.class);
SinkApplication sourceApp = context.getBean(SinkApplication.class);
User user = new User("Alice");
testSink.pojo().send(MessageBuilder
.withPayload(this.mapper.writeValueAsBytes(user)).build());
Map<String, Object> headers = (Map<String, Object>) sourceApp.arguments.pop();
User received = (User) sourceApp.arguments.pop();
assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
.includes(MimeTypeUtils.APPLICATION_JSON));
assertThat(user.getName()).isEqualTo(received.getName());
}
}
@Test
public void testReceiveRawWithDifferentContentTypes() {
try (ConfigurableApplicationContext context = SpringApplication.run(
SinkApplication.class, "--server.port=0", "--spring.jmx.enabled=false")) {
TestSink testSink = context.getBean(TestSink.class);
SinkApplication sourceApp = context.getBean(SinkApplication.class);
testSink.raw().send(MessageBuilder.withPayload(new byte[4])
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_JPEG)
.build());
testSink.raw().send(MessageBuilder.withPayload(new byte[4])
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_GIF)
.build());
Map<String, Object> headers = (Map<String, Object>) sourceApp.arguments.pop();
sourceApp.arguments.pop();
assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
.includes(MimeTypeUtils.IMAGE_GIF));
headers = (Map<String, Object>) sourceApp.arguments.pop();
sourceApp.arguments.pop();
assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
.includes(MimeTypeUtils.IMAGE_JPEG));
}
}
public interface TestSink {
@Input("POJO_INPUT")
SubscribableChannel pojo();
@Input("STRING_INPUT")
SubscribableChannel string();
@Input("TUPLE_INPUT")
SubscribableChannel tuple();
@Input("RAW_INPUT")
SubscribableChannel raw();
}
@EnableBinding(Source.class)
@SpringBootApplication
public static class SourceApplication {
}
@EnableBinding(TestSink.class)
@SpringBootApplication
public static class SinkApplication {
public LinkedList<? super Object> arguments = new LinkedList<>();
@StreamListener("POJO_INPUT")
public void receive(User user, @Headers Map<String, Object> headers) {
this.arguments.push(user);
this.arguments.push(headers);
}
@StreamListener("STRING_INPUT")
public void receive(String string) {
}
@StreamListener("RAW_INPUT")
public void receive(byte[] data, @Headers Map<String, Object> headers) {
this.arguments.push(data);
this.arguments.push(headers);
}
}
}

View File

@@ -0,0 +1,272 @@
///*
// * Copyright 2017-2019 the original author or authors.
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * https://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package org.springframework.cloud.stream.config.contentType;
//
//import java.util.LinkedList;
//import java.util.Map;
//import java.util.concurrent.TimeUnit;
//
//import com.fasterxml.jackson.databind.ObjectMapper;
//import org.junit.Test;
//
//import org.springframework.boot.SpringApplication;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
//import org.springframework.cloud.stream.annotation.EnableBinding;
//import org.springframework.cloud.stream.annotation.Input;
//import org.springframework.cloud.stream.annotation.StreamListener;
//import org.springframework.cloud.stream.messaging.Source;
//import org.springframework.cloud.stream.test.binder.MessageCollector;
//import org.springframework.context.ConfigurableApplicationContext;
//import org.springframework.integration.support.MessageBuilder;
//import org.springframework.messaging.Message;
//import org.springframework.messaging.MessageHeaders;
//import org.springframework.messaging.SubscribableChannel;
//import org.springframework.messaging.handler.annotation.Headers;
//import org.springframework.util.MimeType;
//import org.springframework.util.MimeTypeUtils;
//
//import static org.assertj.core.api.Assertions.assertThat;
//
///**
// * @author Vinicius Carvalho
// * @author Oleg Zhurakousky
// */
//@SuppressWarnings("unchecked")
//public class ContentTypeTests {
//
// private ObjectMapper mapper = new ObjectMapper();
//
// @Test
// public void testSendWithDefaultContentType() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0",
// "--spring.jmx.enabled=false")) {
//
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// User user = new User("Alice");
// source.output().send(MessageBuilder.withPayload(user).build());
// Message<String> message = (Message<String>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// User received = this.mapper.readValue(message.getPayload(), User.class);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.APPLICATION_JSON));
// assertThat(user.getName()).isEqualTo(received.getName());
// }
// }
//
// @Test
// public void testSendJsonAsString() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0",
// "--spring.jmx.enabled=false")) {
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// User user = new User("Alice");
// String json = this.mapper.writeValueAsString(user);
// source.output().send(MessageBuilder.withPayload(user).build());
// Message<String> message = (Message<String>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.APPLICATION_JSON));
// assertThat(json).isEqualTo(message.getPayload());
// }
// }
//
// @Test
// public void testSendJsonString() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0",
// "--spring.jmx.enabled=false")) {
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// source.output().send(MessageBuilder.withPayload("foo").build());
// Message<String> message = (Message<String>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.APPLICATION_JSON));
// assertThat("foo").isEqualTo(message.getPayload());
// }
// }
//
// @Test
// public void testSendBynaryData() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0",
// "--spring.jmx.enabled=false")) {
//
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// byte[] data = new byte[] { 0, 1, 2, 3 };
// source.output()
// .send(MessageBuilder.withPayload(data)
// .setHeader(MessageHeaders.CONTENT_TYPE,
// MimeTypeUtils.APPLICATION_OCTET_STREAM)
// .build());
// Message<byte[]> message = (Message<byte[]>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.APPLICATION_OCTET_STREAM));
// assertThat(message.getPayload()).isEqualTo(data);
// }
// }
//
// @Test
// public void testSendBinaryDataWithContentType() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0", "--spring.jmx.enabled=false",
// "--spring.cloud.stream.bindings.output.contentType=image/jpeg")) {
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// byte[] data = new byte[] { 0, 1, 2, 3 };
// source.output().send(MessageBuilder.withPayload(data).build());
// Message<byte[]> message = (Message<byte[]>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(message.getPayload()).isEqualTo(data);
// }
// }
//
// @Test
// public void testSendBinaryDataWithContentTypeUsingHeaders() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0",
// "--spring.jmx.enabled=false")) {
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// byte[] data = new byte[] { 0, 1, 2, 3 };
// source.output().send(MessageBuilder.withPayload(data)
// .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_JPEG)
// .build());
// Message<byte[]> message = (Message<byte[]>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.IMAGE_JPEG));
// assertThat(message.getPayload()).isEqualTo(data);
// }
// }
//
// @Test
// public void testSendStringType() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SourceApplication.class, "--server.port=0", "--spring.jmx.enabled=false",
// "--spring.cloud.stream.bindings.output.contentType=text/plain")) {
// MessageCollector collector = context.getBean(MessageCollector.class);
// Source source = context.getBean(Source.class);
// User user = new User("Alice");
// source.output().send(MessageBuilder.withPayload(user).build());
// Message<String> message = (Message<String>) collector
// .forChannel(source.output()).poll(1, TimeUnit.SECONDS);
// assertThat(
// message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)
// .includes(MimeTypeUtils.TEXT_PLAIN));
// assertThat(message.getPayload()).isEqualTo(user.toString());
// }
// }
//
// @Test
// public void testReceiveWithDefaults() throws Exception {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SinkApplication.class, "--server.port=0", "--spring.jmx.enabled=false")) {
// TestSink testSink = context.getBean(TestSink.class);
// SinkApplication sourceApp = context.getBean(SinkApplication.class);
// User user = new User("Alice");
// testSink.pojo().send(MessageBuilder
// .withPayload(this.mapper.writeValueAsBytes(user)).build());
// Map<String, Object> headers = (Map<String, Object>) sourceApp.arguments.pop();
// User received = (User) sourceApp.arguments.pop();
// assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
// .includes(MimeTypeUtils.APPLICATION_JSON));
// assertThat(user.getName()).isEqualTo(received.getName());
// }
// }
//
// @Test
// public void testReceiveRawWithDifferentContentTypes() {
// try (ConfigurableApplicationContext context = SpringApplication.run(
// SinkApplication.class, "--server.port=0", "--spring.jmx.enabled=false")) {
// TestSink testSink = context.getBean(TestSink.class);
// SinkApplication sourceApp = context.getBean(SinkApplication.class);
// testSink.raw().send(MessageBuilder.withPayload(new byte[4])
// .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_JPEG)
// .build());
// testSink.raw().send(MessageBuilder.withPayload(new byte[4])
// .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.IMAGE_GIF)
// .build());
// Map<String, Object> headers = (Map<String, Object>) sourceApp.arguments.pop();
// sourceApp.arguments.pop();
// assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
// .includes(MimeTypeUtils.IMAGE_GIF));
// headers = (Map<String, Object>) sourceApp.arguments.pop();
// sourceApp.arguments.pop();
// assertThat(((MimeType) headers.get(MessageHeaders.CONTENT_TYPE))
// .includes(MimeTypeUtils.IMAGE_JPEG));
// }
// }
//
//
// public interface TestSink {
//
// @Input("POJO_INPUT")
// SubscribableChannel pojo();
//
// @Input("STRING_INPUT")
// SubscribableChannel string();
//
// @Input("TUPLE_INPUT")
// SubscribableChannel tuple();
//
// @Input("RAW_INPUT")
// SubscribableChannel raw();
//
// }
//
// @EnableBinding(Source.class)
// @SpringBootApplication
// public static class SourceApplication {
//
// }
//
// @EnableBinding(TestSink.class)
// @SpringBootApplication
// public static class SinkApplication {
//
// public LinkedList<? super Object> arguments = new LinkedList<>();
//
// @StreamListener("POJO_INPUT")
// public void receive(User user, @Headers Map<String, Object> headers) {
// this.arguments.push(user);
// this.arguments.push(headers);
// }
//
// @StreamListener("STRING_INPUT")
// public void receive(String string) {
// }
//
// @StreamListener("RAW_INPUT")
// public void receive(byte[] data, @Headers Map<String, Object> headers) {
// this.arguments.push(data);
// this.arguments.push(headers);
// }
//
// }
//
//}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
<description>Set of classes and utility code that may assist in testing both

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-stream-test-support</artifactId>
<description>A set of classes to ease testing of Spring Cloud Stream modules.

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -1,180 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.stream.binding.StreamListenerParameterAdapter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.messaging.handler.annotation.MessageMapping;
/**
* <b>NOTE: It is no longer recommended to use StreamListener in favor of functional programming model.
* It will be deprecated and subsequently removed in the future</b>
* <br>
* <br>
* Annotation that marks a method to be a listener to inputs declared via
* {@link EnableBinding} (e.g. channels).
*
* Annotated methods are allowed to have flexible signatures, which determine how the
* method is invoked and how their return results are processed. This annotation can be
* applied for two separate classes of methods.
*
* <h3>Declarative mode</h3>
*
* A method is considered declarative if all its method parameter types and return type
* (if not void) are binding targets or conversion targets from binding targets via a
* registered {@link StreamListenerParameterAdapter}.
*
* Only declarative methods can have binding targets or conversion targets as arguments
* and return type.
*
* Declarative methods must specify what inputs and outputs correspond to their arguments
* and return type, and can do this in one of the following ways.
*
* <ul>
* <li>By using either the {@link Input} or {@link Output} annotation for each of the
* parameters and the {@link Output} annotation on the method for the return type (if
* applicable). The use of annotations in this case is mandatory. In this case the
* {@link StreamListener} annotation must not specify a value.</li>
* <li>By setting an {@link Input} bound target as the annotation value of
* {@link StreamListener} and using
* {@link org.springframework.messaging.handler.annotation.SendTo} on the method for
* the return type (if applicable). In this case the method must have exactly one
* parameter, corresponding to an input.
* </li>
* </ul>
*
* An example of declarative method signature using the former idiom is as follows:
*
* <pre class="code">
* &#064;StreamListener
* public &#064;Output("joined") Flux&lt;String&gt; join(
* &#064;Input("input1") Flux&lt;String&gt; input1,
* &#064;Input("input2") Flux&lt;String&gt; input2) {
* // ... join the two input streams via functional operators
* }
* </pre>
*
* An example of declarative method signature using the latter idiom is as follows:
*
* <pre class="code">
* &#064;StreamListener(Processor.INPUT)
* &#064;SendTo(Processor.OUTPUT)
* public Flux&lt;String&gt; convert(Flux&lt;String&gt; input) {
* return input.map(String::toUppercase);
* }
* </pre>
*
* Declarative methods are invoked only once, when the context is refreshed.
*
* <h3>Individual message handler mode</h3>
*
* Non declarative methods are treated as message handler based, and are invoked for each
* incoming message received from that target. In this case, the method can have a
* flexible signature, as described by {@link MessageMapping}.
*
* If the method returns a {@link org.springframework.messaging.Message}, the result will
* be automatically sent to a binding target, as follows:
* <ul>
* <li>A result of the type {@link org.springframework.messaging.Message} will be sent
* as-is</li>
* <li>All other results will become the payload of a
* {@link org.springframework.messaging.Message}</li>
* </ul>
*
* The output binding target where the return message is sent is determined by consulting
* in the following order:
* <ul>
* <li>The {@link org.springframework.messaging.MessageHeaders} of the resulting
* message.</li>
* <li>The value set on the
* {@link org.springframework.messaging.handler.annotation.SendTo} annotation, if
* present</li>
* </ul>
*
* An example of individual message handler signature is as follows:
*
* <pre class="code">
* &#064;StreamListener(Processor.INPUT)
* &#064;SendTo(Processor.OUTPUT)
* public String convert(String input) {
* return input.toUppercase();
* }
* </pre>
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Gary Russell
* @see MessageMapping
* @see EnableBinding
* @see org.springframework.messaging.handler.annotation.SendTo
*
* @deprecated as of 3.1 in favor of functional programming model
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
@Deprecated
public @interface StreamListener {
/**
* The name of the binding target (e.g. channel) that the method subscribes to.
* @return the name of the binding target.
*/
@AliasFor("target")
String value() default "";
/**
* The name of the binding target (e.g. channel) that the method subscribes to.
* @return the name of the binding target.
*/
@AliasFor("value")
String target() default "";
/**
* A condition that must be met by all items that are dispatched to this method.
* @return a SpEL expression that must evaluate to a {@code boolean} value.
*/
String condition() default "";
/**
* When "true" (default), and a {@code @SendTo} annotation is present, copy the
* inbound headers to the outbound message (if the header is absent on the outbound
* message). Can be an expression ({@code #{...}}) or property placeholder. Must
* resolve to a boolean or a string that is parsed by {@code Boolean.parseBoolean()}.
* An expression that resolves to {@code null} is interpreted to mean {@code false}.
*
* The expression is evaluated during application initialization, and not for each
* individual message.
*
* Prior to version 1.3.0, the default value used to be "false" and headers were not
* propagated by default.
*
* Starting with version 1.3.0, the default value is "true".
*
* @since 1.2.3
* @return {@link Boolean} in a String format
*/
String copyHeaders() default "true";
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
/**
* Marker to tag {@link org.springframework.messaging.converter.MessageConverter} beans
* that will be added to the
* {@link org.springframework.cloud.stream.converter.CompositeMessageConverterFactory}.
*
* @author Vinicius Carvalho
* @author Arten Bilan
*
* @deprecated as of 3.0 and is not used by the framework anymore.
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
@Bean
@Deprecated
public @interface StreamMessageConverter {
}

View File

@@ -20,9 +20,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.Min;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.Min;
import org.springframework.messaging.Message;

View File

@@ -18,14 +18,13 @@ package org.springframework.cloud.stream.binder;
import java.io.IOException;
import javax.validation.constraints.Min;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import jakarta.validation.constraints.Min;
import org.springframework.expression.Expression;

View File

@@ -1,157 +0,0 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.core.BeanFactoryMessageChannelDestinationResolver;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* A {@link org.springframework.messaging.core.DestinationResolver} implementation that
* resolves the channel from the bean factory and, if not present, creates a new channel
* and adds it to the factory after binding it to the binder.
*
* @author Mark Fisher
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*
* @deprecated As of 3.0.0 in favor if providing `spring.cloud.stream.sendto.destination` property.
* This is primarily for function-based programming model. For StreamListener it would still be
* required and thus will stay until we deprecate and eventually discontinue StreamListener
* and annotation-based programming model.
*/
@Deprecated
public class BinderAwareChannelResolver
extends BeanFactoryMessageChannelDestinationResolver {
private final BindingService bindingService;
private final AbstractBindingTargetFactory<? extends MessageChannel> bindingTargetFactory;
private final DynamicDestinationsBindable dynamicDestinationsBindable;
@SuppressWarnings("rawtypes")
private final NewDestinationBindingCallback newBindingCallback;
private ConfigurableListableBeanFactory beanFactory;
public BinderAwareChannelResolver(BindingService bindingService,
AbstractBindingTargetFactory<? extends MessageChannel> bindingTargetFactory,
DynamicDestinationsBindable dynamicDestinationsBindable) {
this(bindingService, bindingTargetFactory, dynamicDestinationsBindable, null);
}
@SuppressWarnings("rawtypes")
public BinderAwareChannelResolver(BindingService bindingService,
AbstractBindingTargetFactory<? extends MessageChannel> bindingTargetFactory,
DynamicDestinationsBindable dynamicDestinationsBindable,
NewDestinationBindingCallback callback) {
this.dynamicDestinationsBindable = dynamicDestinationsBindable;
Assert.notNull(bindingService, "'bindingService' cannot be null");
Assert.notNull(bindingTargetFactory, "'bindingTargetFactory' cannot be null");
this.bindingService = bindingService;
this.bindingTargetFactory = bindingTargetFactory;
this.newBindingCallback = callback;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
Assert.isTrue(beanFactory instanceof ConfigurableListableBeanFactory,
"'beanFactory' must be an instance of ConfigurableListableBeanFactory");
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
/*
* See the following for more discussion on it as well as demo reproducing it, thanks
* to Anshul Mehra (@Walliee)
* https://github.com/spring-cloud/spring-cloud-stream/issues/1603
*/
@SuppressWarnings("unchecked")
@Override
public synchronized MessageChannel resolveDestination(String channelName) {
BindingServiceProperties bindingServiceProperties = this.bindingService
.getBindingServiceProperties();
String[] dynamicDestinations = bindingServiceProperties.getDynamicDestinations();
MessageChannel channel;
boolean dynamicAllowed = ObjectUtils.isEmpty(dynamicDestinations)
|| ObjectUtils.containsElement(dynamicDestinations, channelName);
try {
channel = super.resolveDestination(channelName);
}
catch (DestinationResolutionException e) {
if (!dynamicAllowed) {
throw e;
}
else {
channel = this.bindingTargetFactory.createOutput(channelName);
ProducerProperties producerProperties = bindingServiceProperties
.getProducerProperties(channelName);
if (this.newBindingCallback != null) {
Object extendedProducerProperties = this.bindingService
.getExtendedProducerProperties(channel, channelName);
this.newBindingCallback.configure(channelName, channel,
producerProperties, extendedProducerProperties);
}
bindingServiceProperties.updateProducerProperties(channelName,
producerProperties);
this.beanFactory.registerSingleton(channelName, channel);
channel = (MessageChannel) this.beanFactory.initializeBean(channel,
channelName);
Binding<MessageChannel> binding = this.bindingService
.bindProducer(channel, channelName);
this.dynamicDestinationsBindable.addOutputBinding(channelName, binding);
}
}
return channel;
}
/**
* Configure a new destination before it is bound.
*
* @param <T> the extended properties type. If you need to support dynamic binding
* with multiple binders, use {@link Object} and cast as needed.
* @since 2.0
*
*/
@FunctionalInterface
public interface NewDestinationBindingCallback<T> {
/**
* Configure the properties or channel before binding.
* @param channelName the name of the new channel.
* @param channel the channel that is about to be bound.
* @param producerProperties the producer properties.
* @param extendedProducerProperties the extended producer properties (type
* depends on binder type and may be null if the binder doesn't support extended
* properties).
*/
void configure(String channelName, MessageChannel channel,
ProducerProperties producerProperties, T extendedProducerProperties);
}
}

View File

@@ -22,7 +22,7 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.core.DestinationResolver;
/**
* A {@link BeanPostProcessor} that sets a {@link BinderAwareChannelResolver} on any bean
* A {@link BeanPostProcessor} that sets a BinderAwareChannelResolver on any bean
* of type {@link AbstractMappingMessageRouter} within the context.
*
* @author Mark Fisher

View File

@@ -1,149 +0,0 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* An {@link AbstractReplyProducingMessageHandler} that delegates to a collection of
* internal {@link ConditionalStreamListenerMessageHandlerWrapper} instances, executing
* the ones that match the given expression.
*
* @author Marius Bogoevici
* @since 1.2
*/
final class DispatchingStreamListenerMessageHandler
extends AbstractReplyProducingMessageHandler {
private final List<ConditionalStreamListenerMessageHandlerWrapper> handlerMethods;
private final boolean evaluateExpressions;
private final EvaluationContext evaluationContext;
DispatchingStreamListenerMessageHandler(
Collection<ConditionalStreamListenerMessageHandlerWrapper> handlerMethods,
EvaluationContext evaluationContext) {
Assert.notEmpty(handlerMethods, "'handlerMethods' cannot be empty");
this.handlerMethods = Collections
.unmodifiableList(new ArrayList<>(handlerMethods));
boolean evaluateExpressions = false;
for (ConditionalStreamListenerMessageHandlerWrapper handlerMethod : handlerMethods) {
if (handlerMethod.getCondition() != null) {
evaluateExpressions = true;
break;
}
}
this.evaluateExpressions = evaluateExpressions;
if (evaluateExpressions) {
Assert.notNull(evaluationContext,
"'evaluationContext' cannot be null if conditions are used");
}
this.evaluationContext = evaluationContext;
}
@Override
protected boolean shouldCopyRequestHeaders() {
return false;
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
List<ConditionalStreamListenerMessageHandlerWrapper> matchingHandlers = this.evaluateExpressions
? findMatchingHandlers(requestMessage) : this.handlerMethods;
if (matchingHandlers.size() == 0) {
if (this.logger.isWarnEnabled()) {
this.logger.warn(
"Cannot find a @StreamListener matching for message with id: "
+ requestMessage.getHeaders().getId());
}
return null;
}
else if (matchingHandlers.size() > 1) {
for (ConditionalStreamListenerMessageHandlerWrapper matchingMethod : matchingHandlers) {
matchingMethod.getStreamListenerMessageHandler()
.handleMessage(requestMessage);
}
return null;
}
else {
final ConditionalStreamListenerMessageHandlerWrapper singleMatchingHandler = matchingHandlers
.get(0);
singleMatchingHandler.getStreamListenerMessageHandler()
.handleMessage(requestMessage);
return null;
}
}
private List<ConditionalStreamListenerMessageHandlerWrapper> findMatchingHandlers(
Message<?> message) {
ArrayList<ConditionalStreamListenerMessageHandlerWrapper> matchingMethods = new ArrayList<>();
for (ConditionalStreamListenerMessageHandlerWrapper wrapper : this.handlerMethods) {
if (wrapper.getCondition() == null) {
matchingMethods.add(wrapper);
}
else {
boolean conditionMetOnMessage = wrapper.getCondition()
.getValue(this.evaluationContext, message, Boolean.class);
if (conditionMetOnMessage) {
matchingMethods.add(wrapper);
}
}
}
return matchingMethods;
}
static class ConditionalStreamListenerMessageHandlerWrapper {
private final Expression condition;
private final StreamListenerMessageHandler streamListenerMessageHandler;
ConditionalStreamListenerMessageHandlerWrapper(Expression condition,
StreamListenerMessageHandler streamListenerMessageHandler) {
Assert.notNull(streamListenerMessageHandler,
"the message handler cannot be null");
Assert.isTrue(condition == null || streamListenerMessageHandler.isVoid(),
"cannot specify a condition and a return value at the same time");
this.condition = condition;
this.streamListenerMessageHandler = streamListenerMessageHandler;
}
public Expression getCondition() {
return this.condition;
}
public boolean isVoid() {
return this.streamListenerMessageHandler.isVoid();
}
public StreamListenerMessageHandler getStreamListenerMessageHandler() {
return this.streamListenerMessageHandler;
}
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.io.Closeable;
import java.io.IOException;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
/**
* A {@link StreamListenerResultAdapter} used for bridging an
* {@link org.springframework.cloud.stream.annotation.Output} {@link MessageChannel} to a
* bound {@link MessageChannel}.
*
* @author Marius Bogoevici
* @author Soby Chacko
*/
public class MessageChannelStreamListenerResultAdapter
implements StreamListenerResultAdapter<MessageChannel, MessageChannel> {
@Override
public boolean supports(Class<?> resultType, Class<?> bindingTarget) {
return MessageChannel.class.isAssignableFrom(resultType)
&& MessageChannel.class.isAssignableFrom(bindingTarget);
}
@Override
public Closeable adapt(MessageChannel streamListenerResult,
MessageChannel bindingTarget) {
BridgeHandler handler = new BridgeHandler();
handler.setOutputChannel(bindingTarget);
handler.afterPropertiesSet();
((SubscribableChannel) streamListenerResult).subscribe(handler);
return new NoOpCloseeable();
}
private static final class NoOpCloseeable implements Closeable {
@Override
public void close() throws IOException {
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2022-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.messaging.MessageChannel;
/**
* Configure a new destination before it is bound.
*
* @param <T> the extended properties type. If you need to support dynamic binding
* with multiple binders, use {@link Object} and cast as needed.
* @since 2.0
*
*/
@FunctionalInterface
public interface NewDestinationBindingCallback<T> {
/**
* Configure the properties or channel before binding.
* @param channelName the name of the new channel.
* @param channel the channel that is about to be bound.
* @param producerProperties the producer properties.
* @param extendedProducerProperties the extended producer properties (type
* depends on binder type and may be null if the binder doesn't support extended
* properties).
*/
void configure(String channelName, MessageChannel channel,
ProducerProperties producerProperties, T extendedProducerProperties);
}

View File

@@ -1,572 +0,0 @@
/*
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.config.SpringIntegrationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link BeanPostProcessor} that handles {@link StreamListener} annotations found on bean
* methods.
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Soby Chacko
* @author Oleg Zhurakousky
*/
public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProcessor,
ApplicationContextAware, SmartInitializingSingleton {
private static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser();
// @checkstyle:off
private final MultiValueMap<String, StreamListenerHandlerMethodMapping> mappedListenerMethods = new LinkedMultiValueMap<>();
// @checkstyle:on
private final Set<Runnable> streamListenerCallbacks = new HashSet<>();
// == dependencies that are injected in 'afterSingletonsInstantiated' to avoid early
// initialization
private DestinationResolver<MessageChannel> binderAwareChannelResolver;
private MessageHandlerMethodFactory messageHandlerMethodFactory;
// == end dependencies
private SpringIntegrationProperties springIntegrationProperties;
private ConfigurableApplicationContext applicationContext;
private BeanExpressionResolver resolver;
private BeanExpressionContext expressionContext;
private Set<StreamListenerSetupMethodOrchestrator> streamListenerSetupMethodOrchestrators = new LinkedHashSet<>();
private boolean streamListenerPresent;
@Override
public final void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
this.resolver = this.applicationContext.getBeanFactory()
.getBeanExpressionResolver();
this.expressionContext = new BeanExpressionContext(
this.applicationContext.getBeanFactory(), null);
}
@Override
public final void afterSingletonsInstantiated() {
if (!this.streamListenerPresent) {
return;
}
this.injectAndPostProcessDependencies();
EvaluationContext evaluationContext = IntegrationContextUtils
.getEvaluationContext(this.applicationContext.getBeanFactory());
for (Map.Entry<String, List<StreamListenerHandlerMethodMapping>> mappedBindingEntry : this.mappedListenerMethods
.entrySet()) {
ArrayList<DispatchingStreamListenerMessageHandler.ConditionalStreamListenerMessageHandlerWrapper> handlers;
handlers = new ArrayList<>();
for (StreamListenerHandlerMethodMapping mapping : mappedBindingEntry
.getValue()) {
final InvocableHandlerMethod invocableHandlerMethod = this.messageHandlerMethodFactory
.createInvocableHandlerMethod(mapping.getTargetBean(),
checkProxy(mapping.getMethod(), mapping.getTargetBean()));
StreamListenerMessageHandler streamListenerMessageHandler = new StreamListenerMessageHandler(
invocableHandlerMethod,
resolveExpressionAsBoolean(mapping.getCopyHeaders(),
"copyHeaders"),
this.springIntegrationProperties
.getMessageHandlerNotPropagatedHeaders());
streamListenerMessageHandler
.setApplicationContext(this.applicationContext);
streamListenerMessageHandler
.setBeanFactory(this.applicationContext.getBeanFactory());
if (StringUtils.hasText(mapping.getDefaultOutputChannel())) {
streamListenerMessageHandler
.setOutputChannelName(mapping.getDefaultOutputChannel());
}
streamListenerMessageHandler.afterPropertiesSet();
if (StringUtils.hasText(mapping.getCondition())) {
String conditionAsString = resolveExpressionAsString(
mapping.getCondition(), "condition");
Expression condition = SPEL_EXPRESSION_PARSER
.parseExpression(conditionAsString);
handlers.add(
new DispatchingStreamListenerMessageHandler.ConditionalStreamListenerMessageHandlerWrapper(
condition, streamListenerMessageHandler));
}
else {
handlers.add(
new DispatchingStreamListenerMessageHandler.ConditionalStreamListenerMessageHandlerWrapper(
null, streamListenerMessageHandler));
}
}
if (handlers.size() > 1) {
for (DispatchingStreamListenerMessageHandler.ConditionalStreamListenerMessageHandlerWrapper handler : handlers) {
Assert.isTrue(handler.isVoid(),
StreamListenerErrorMessages.MULTIPLE_VALUE_RETURNING_METHODS);
}
}
AbstractReplyProducingMessageHandler handler;
if (handlers.size() > 1 || handlers.get(0).getCondition() != null) {
handler = new DispatchingStreamListenerMessageHandler(handlers,
evaluationContext);
}
else {
handler = handlers.get(0).getStreamListenerMessageHandler();
}
handler.setApplicationContext(this.applicationContext);
handler.setChannelResolver(this.binderAwareChannelResolver);
handler.afterPropertiesSet();
this.applicationContext.getBeanFactory().registerSingleton(
handler.getClass().getSimpleName() + handler.hashCode(), handler);
this.applicationContext
.getBean(mappedBindingEntry.getKey(), SubscribableChannel.class)
.subscribe(handler);
}
this.mappedListenerMethods.clear();
}
@Override
public final Object postProcessAfterInitialization(Object bean, final String beanName)
throws BeansException {
Class<?> targetClass = AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean)
: bean.getClass();
Method[] uniqueDeclaredMethods = ReflectionUtils
.getUniqueDeclaredMethods(targetClass, ReflectionUtils.USER_DECLARED_METHODS);
for (Method method : uniqueDeclaredMethods) {
StreamListener streamListener = AnnotatedElementUtils
.findMergedAnnotation(method, StreamListener.class);
if (streamListener != null) {
this.streamListenerPresent = true;
this.streamListenerCallbacks.add(() -> {
Assert.isTrue(method.getAnnotation(Input.class) == null,
StreamListenerErrorMessages.INPUT_AT_STREAM_LISTENER);
this.doPostProcess(streamListener, method, bean);
});
}
}
return bean;
}
/**
* Extension point, allowing subclasses to customize the {@link StreamListener}
* annotation detected by the postprocessor.
* @param originalAnnotation the original annotation
* @param annotatedMethod the method on which the annotation has been found
* @return the postprocessed {@link StreamListener} annotation
*/
protected StreamListener postProcessAnnotation(StreamListener originalAnnotation,
Method annotatedMethod) {
return originalAnnotation;
}
private void doPostProcess(StreamListener streamListener, Method method,
Object bean) {
streamListener = postProcessAnnotation(streamListener, method);
Optional<StreamListenerSetupMethodOrchestrator> orchestratorOptional;
orchestratorOptional = this.streamListenerSetupMethodOrchestrators.stream()
.filter(t -> t.supports(method)).findFirst();
Assert.isTrue(orchestratorOptional.isPresent(),
"A matching StreamListenerSetupMethodOrchestrator must be present");
StreamListenerSetupMethodOrchestrator streamListenerSetupMethodOrchestrator = orchestratorOptional
.get();
streamListenerSetupMethodOrchestrator
.orchestrateStreamListenerSetupMethod(streamListener, method, bean);
}
private Method checkProxy(Method methodArg, Object bean) {
Method method = methodArg;
if (AopUtils.isJdkDynamicProxy(bean)) {
try {
// Found a @StreamListener method on the target class for this JDK proxy
// ->
// is it also present on the proxy itself?
method = bean.getClass().getMethod(method.getName(),
method.getParameterTypes());
Class<?>[] proxiedInterfaces = ((Advised) bean).getProxiedInterfaces();
for (Class<?> iface : proxiedInterfaces) {
try {
method = iface.getMethod(method.getName(),
method.getParameterTypes());
break;
}
catch (NoSuchMethodException noMethod) {
}
}
}
catch (SecurityException ex) {
ReflectionUtils.handleReflectionException(ex);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException(String.format(
"@StreamListener method '%s' found on bean target class '%s', "
+ "but not found in any interface(s) for bean JDK proxy. Either "
+ "pull the method up to an interface or switch to subclass (CGLIB) "
+ "proxies by setting proxy-target-class/proxyTargetClass attribute to 'true'",
method.getName(), method.getDeclaringClass().getSimpleName()),
ex);
}
}
return method;
}
private String resolveExpressionAsString(String value, String property) {
Object resolved = resolveExpression(value);
if (resolved instanceof String) {
return (String) resolved;
}
else {
throw new IllegalStateException("Resolved " + property + " to ["
+ resolved.getClass() + "] instead of String for [" + value + "]");
}
}
private boolean resolveExpressionAsBoolean(String value, String property) {
Object resolved = resolveExpression(value);
if (resolved == null) {
return false;
}
else if (resolved instanceof String) {
return Boolean.parseBoolean((String) resolved);
}
else if (resolved instanceof Boolean) {
return (Boolean) resolved;
}
else {
throw new IllegalStateException(
"Resolved " + property + " to [" + resolved.getClass()
+ "] instead of String or Boolean for [" + value + "]");
}
}
private String resolveExpression(String value) {
String resolvedValue = this.applicationContext.getBeanFactory()
.resolveEmbeddedValue(value);
if (resolvedValue.startsWith("#{") && value.endsWith("}")) {
resolvedValue = (String) this.resolver.evaluate(resolvedValue,
this.expressionContext);
}
return resolvedValue;
}
/**
* This operations ensures that required dependencies are not accidentally injected
* early given that this bean is BPP.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void injectAndPostProcessDependencies() {
Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters = this.applicationContext
.getBeansOfType(StreamListenerParameterAdapter.class).values();
Collection<StreamListenerResultAdapter> streamListenerResultAdapters = this.applicationContext
.getBeansOfType(StreamListenerResultAdapter.class).values();
this.binderAwareChannelResolver = this.applicationContext
.getBean("binderAwareChannelResolver", DestinationResolver.class);
this.messageHandlerMethodFactory = this.applicationContext
.getBean("integrationMessageHandlerMethodFactory", MessageHandlerMethodFactory.class);
this.springIntegrationProperties = this.applicationContext
.getBean(SpringIntegrationProperties.class);
this.streamListenerSetupMethodOrchestrators.addAll(this.applicationContext
.getBeansOfType(StreamListenerSetupMethodOrchestrator.class).values());
// Default orchestrator for StreamListener method invocation is added last into
// the LinkedHashSet.
this.streamListenerSetupMethodOrchestrators.add(
new DefaultStreamListenerSetupMethodOrchestrator(this.applicationContext,
streamListenerParameterAdapters, streamListenerResultAdapters));
this.streamListenerCallbacks.forEach(Runnable::run);
}
private static class StreamListenerHandlerMethodMapping {
private final Object targetBean;
private final Method method;
private final String condition;
private final String defaultOutputChannel;
private final String copyHeaders;
StreamListenerHandlerMethodMapping(Object targetBean, Method method,
String condition, String defaultOutputChannel, String copyHeaders) {
this.targetBean = targetBean;
this.method = method;
this.condition = condition;
this.defaultOutputChannel = defaultOutputChannel;
this.copyHeaders = copyHeaders;
}
Object getTargetBean() {
return this.targetBean;
}
Method getMethod() {
return this.method;
}
String getCondition() {
return this.condition;
}
String getDefaultOutputChannel() {
return this.defaultOutputChannel;
}
public String getCopyHeaders() {
return this.copyHeaders;
}
}
@SuppressWarnings("rawtypes")
private final class DefaultStreamListenerSetupMethodOrchestrator
implements StreamListenerSetupMethodOrchestrator {
private final ConfigurableApplicationContext applicationContext;
private final Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters;
private final Collection<StreamListenerResultAdapter> streamListenerResultAdapters;
private DefaultStreamListenerSetupMethodOrchestrator(
ConfigurableApplicationContext applicationContext,
Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters,
Collection<StreamListenerResultAdapter> streamListenerResultAdapters) {
this.applicationContext = applicationContext;
this.streamListenerParameterAdapters = streamListenerParameterAdapters;
this.streamListenerResultAdapters = streamListenerResultAdapters;
}
@Override
public void orchestrateStreamListenerSetupMethod(StreamListener streamListener,
Method method, Object bean) {
String methodAnnotatedInboundName = streamListener.value();
String methodAnnotatedOutboundName = StreamListenerMethodUtils
.getOutboundBindingTargetName(method);
int inputAnnotationCount = StreamListenerMethodUtils
.inputAnnotationCount(method);
int outputAnnotationCount = StreamListenerMethodUtils
.outputAnnotationCount(method);
boolean isDeclarative = checkDeclarativeMethod(method,
methodAnnotatedInboundName, methodAnnotatedOutboundName);
StreamListenerMethodUtils.validateStreamListenerMethod(method,
inputAnnotationCount, outputAnnotationCount,
methodAnnotatedInboundName, methodAnnotatedOutboundName,
isDeclarative, streamListener.condition());
if (isDeclarative) {
StreamListenerParameterAdapter[] toSlpaArray;
toSlpaArray = new StreamListenerParameterAdapter[this.streamListenerParameterAdapters
.size()];
Object[] adaptedInboundArguments = adaptAndRetrieveInboundArguments(
method, methodAnnotatedInboundName, this.applicationContext,
this.streamListenerParameterAdapters.toArray(toSlpaArray));
invokeStreamListenerResultAdapter(method, bean,
methodAnnotatedOutboundName, adaptedInboundArguments);
}
else {
registerHandlerMethodOnListenedChannel(method, streamListener, bean);
}
}
@Override
public boolean supports(Method method) {
// default catch all orchestrator
return true;
}
@SuppressWarnings("unchecked")
private void invokeStreamListenerResultAdapter(Method method, Object bean,
String outboundName, Object... arguments) {
try {
if (Void.TYPE.equals(method.getReturnType())) {
method.invoke(bean, arguments);
}
else {
Object result = method.invoke(bean, arguments);
if (!StringUtils.hasText(outboundName)) {
for (int parameterIndex = 0; parameterIndex < method
.getParameterCount(); parameterIndex++) {
MethodParameter methodParameter = MethodParameter
.forExecutable(method, parameterIndex);
if (methodParameter.hasParameterAnnotation(Output.class)) {
outboundName = methodParameter
.getParameterAnnotation(Output.class).value();
}
}
}
Object targetBean = this.applicationContext.getBean(outboundName);
for (StreamListenerResultAdapter streamListenerResultAdapter : this.streamListenerResultAdapters) {
if (streamListenerResultAdapter.supports(result.getClass(),
targetBean.getClass())) {
streamListenerResultAdapter.adapt(result, targetBean);
break;
}
}
}
}
catch (Exception e) {
throw new BeanInitializationException(
"Cannot setup StreamListener for " + method, e);
}
}
private void registerHandlerMethodOnListenedChannel(Method method,
StreamListener streamListener, Object bean) {
Assert.hasText(streamListener.value(), "The binding name cannot be null");
if (!StringUtils.hasText(streamListener.value())) {
throw new BeanInitializationException(
"A bound component name must be specified");
}
final String defaultOutputChannel = StreamListenerMethodUtils
.getOutboundBindingTargetName(method);
if (Void.TYPE.equals(method.getReturnType())) {
Assert.isTrue(StringUtils.isEmpty(defaultOutputChannel),
"An output channel cannot be specified for a method that does not return a value");
}
else {
Assert.isTrue(!StringUtils.isEmpty(defaultOutputChannel),
"An output channel must be specified for a method that can return a value");
}
StreamListenerMethodUtils.validateStreamListenerMessageHandler(method);
StreamListenerAnnotationBeanPostProcessor.this.mappedListenerMethods.add(
streamListener.value(),
new StreamListenerHandlerMethodMapping(bean, method,
streamListener.condition(), defaultOutputChannel,
streamListener.copyHeaders()));
}
private boolean checkDeclarativeMethod(Method method,
String methodAnnotatedInboundName, String methodAnnotatedOutboundName) {
int methodArgumentsLength = method.getParameterCount();
for (int parameterIndex = 0; parameterIndex < methodArgumentsLength; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
if (methodParameter.hasParameterAnnotation(Input.class)) {
String inboundName = (String) AnnotationUtils.getValue(
methodParameter.getParameterAnnotation(Input.class));
Assert.isTrue(StringUtils.hasText(inboundName),
StreamListenerErrorMessages.INVALID_INBOUND_NAME);
Assert.isTrue(
isDeclarativeMethodParameter(inboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
String outboundName = (String) AnnotationUtils.getValue(
methodParameter.getParameterAnnotation(Output.class));
Assert.isTrue(StringUtils.hasText(outboundName),
StreamListenerErrorMessages.INVALID_OUTBOUND_NAME);
Assert.isTrue(
isDeclarativeMethodParameter(outboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (StringUtils.hasText(methodAnnotatedOutboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedOutboundName,
methodParameter);
}
else if (StringUtils.hasText(methodAnnotatedInboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedInboundName,
methodParameter);
}
}
return false;
}
/**
* Determines if method parameters signify an imperative or declarative listener
* definition. <br>
* Imperative - where handler method is invoked on each message by the handler
* infrastructure provided by the framework <br>
* Declarative - where handler is provided by the method itself. <br>
* Declarative method parameter could either be {@link MessageChannel} or any
* other Object for which there is a {@link StreamListenerParameterAdapter} (i.e.,
* {@link reactor.core.publisher.Flux}). Declarative method is invoked only once
* during initialization phase.
* @param targetBeanName name of the bean
* @param methodParameter method parameter
* @return {@code true} when the method parameter is declarative
*/
@SuppressWarnings("unchecked")
private boolean isDeclarativeMethodParameter(String targetBeanName,
MethodParameter methodParameter) {
boolean declarative = false;
if (!methodParameter.getParameterType().isAssignableFrom(Object.class)
&& this.applicationContext.containsBean(targetBeanName)) {
declarative = MessageChannel.class
.isAssignableFrom(methodParameter.getParameterType());
if (!declarative) {
Class<?> targetBeanClass = this.applicationContext
.getType(targetBeanName);
declarative = this.streamListenerParameterAdapters.stream().anyMatch(
slpa -> slpa.supports(targetBeanClass, methodParameter));
}
}
return declarative;
}
}
}

View File

@@ -1,133 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
/**
* @author Ilayaperumal Gopinathan
*/
public abstract class StreamListenerErrorMessages {
/**
* Error message when the inbound name was invalid.
*/
public static final String INVALID_INBOUND_NAME = "The @Input annotation must have the name of an input as value";
/**
* Error message when the outbound name was invalid.
*/
public static final String INVALID_OUTBOUND_NAME = "The @Output annotation must have the name of an input as value";
/**
* Error message when there were no outputs specified.
*/
public static final String ATLEAST_ONE_OUTPUT = "At least one output must be specified";
/**
* Error message when multiple destinations were specified.
*/
public static final String SEND_TO_MULTIPLE_DESTINATIONS = "Multiple destinations cannot be specified";
/**
* Error message when empty destination was provided.
*/
public static final String SEND_TO_EMPTY_DESTINATION = "An empty destination cannot be specified";
/**
* Error message when the input or output annotation got placed on a method parameter.
*/
public static final String INVALID_INPUT_OUTPUT_METHOD_PARAMETERS = "@Input or @Output annotations "
+ "are not permitted on "
+ "method parameters while using the @StreamListener value and a method-level output specification";
/**
* Error message when no input destination was provided.
*/
public static final String NO_INPUT_DESTINATION = "No input destination is configured. "
+ "Use either the @StreamListener value or @Input";
/**
* Error message when an ambiguous message handler method argument was found.
*/
public static final String AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS = "Ambiguous method arguments "
+ "for the StreamListener method";
/**
* Error message when invalid input values where set.
*/
public static final String INVALID_INPUT_VALUES = "Cannot set both @StreamListener "
+ "value and @Input annotation as method parameter";
/**
* Error message when invalid input value with output method parameter was set.
*/
public static final String INVALID_INPUT_VALUE_WITH_OUTPUT_METHOD_PARAM = "Setting the @StreamListener "
+ "value when using @Output annotation as method parameter is not permitted. "
+ "Use @Input method parameter annotation to specify inbound value instead";
/**
* Error message when invalid output values were set.
*/
public static final String INVALID_OUTPUT_VALUES = "Cannot set both output (@Output/@SendTo) method annotation value"
+ " and @Output annotation as a method parameter";
/**
* Error message when condition was set in declarative mode.
*/
public static final String CONDITION_ON_DECLARATIVE_METHOD = "Cannot set a condition when "
+ "using @StreamListener in declarative mode";
/**
* Error message when condition was set for methods that return a value.
*/
public static final String CONDITION_ON_METHOD_RETURNING_VALUE = "Cannot set a condition "
+ "for methods that return a value";
/**
* Error message when multiple value returning methods were provided.
*/
public static final String MULTIPLE_VALUE_RETURNING_METHODS = "If multiple @StreamListener "
+ "methods are listening to the same binding target, none of them may return a value";
private static final String PREFIX = "A method annotated with @StreamListener ";
/**
* Error message when @StreamListener was used with @Input.
*/
public static final String INPUT_AT_STREAM_LISTENER = PREFIX
+ "may never be annotated with @Input. "
+ "If it should listen to a specific input, use the value of @StreamListener instead";
/**
* Error message when invalid input value with output method parameter was set.
*/
public static final String RETURN_TYPE_NO_OUTBOUND_SPECIFIED = PREFIX
+ "having a return type should also have an outbound target specified";
/**
* Error message when return type was specified for multiple outbound targets.
*/
public static final String RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED = PREFIX
+ "having a return type should have only one outbound target specified";
/**
* Error message when invalid declarative method parameters were set.
*/
public static final String INVALID_DECLARATIVE_METHOD_PARAMETERS = PREFIX
+ "may use @Input or @Output annotations only in declarative mode "
+ "and for parameters that are binding targets or convertible from binding targets.";
}

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
/**
* @author Marius Bogoevici
* @author Gary Russell
* @since 1.2
*/
public class StreamListenerMessageHandler extends AbstractReplyProducingMessageHandler {
private final InvocableHandlerMethod invocableHandlerMethod;
private final boolean copyHeaders;
StreamListenerMessageHandler(InvocableHandlerMethod invocableHandlerMethod,
boolean copyHeaders, String[] notPropagatedHeaders) {
super();
this.invocableHandlerMethod = invocableHandlerMethod;
this.copyHeaders = copyHeaders;
this.setNotPropagatedHeaders(notPropagatedHeaders);
}
@Override
protected boolean shouldCopyRequestHeaders() {
return this.copyHeaders;
}
public boolean isVoid() {
return this.invocableHandlerMethod.isVoid();
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
try {
return this.invocableHandlerMethod.invoke(requestMessage);
}
catch (Exception e) {
if (e instanceof MessagingException) {
throw (MessagingException) e;
}
else {
throw new MessagingException(requestMessage,
"Exception thrown while invoking "
+ this.invocableHandlerMethod.getShortLogMessage(),
e);
}
}
}
}

View File

@@ -1,185 +0,0 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.lang.reflect.Method;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* This class contains utility methods for handling {@link StreamListener} annotated bean
* methods.
*
* @author Ilayaperumal Gopinathan
*/
public final class StreamListenerMethodUtils {
private StreamListenerMethodUtils() {
throw new IllegalStateException("Can't instantiate a utility class");
}
protected static int inputAnnotationCount(Method method) {
int inputAnnotationCount = 0;
for (int parameterIndex = 0; parameterIndex < method
.getParameterTypes().length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
if (methodParameter.hasParameterAnnotation(Input.class)) {
inputAnnotationCount++;
}
}
return inputAnnotationCount;
}
protected static int outputAnnotationCount(Method method) {
int outputAnnotationCount = 0;
for (int parameterIndex = 0; parameterIndex < method
.getParameterTypes().length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
if (methodParameter.hasParameterAnnotation(Output.class)) {
outputAnnotationCount++;
}
}
return outputAnnotationCount;
}
protected static void validateStreamListenerMethod(Method method,
int inputAnnotationCount, int outputAnnotationCount,
String methodAnnotatedInboundName, String methodAnnotatedOutboundName,
boolean isDeclarative, String condition) {
int methodArgumentsLength = method.getParameterTypes().length;
if (!isDeclarative) {
Assert.isTrue(inputAnnotationCount == 0 && outputAnnotationCount == 0,
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
if (StringUtils.hasText(methodAnnotatedInboundName)
&& StringUtils.hasText(methodAnnotatedOutboundName)) {
Assert.isTrue(inputAnnotationCount == 0 && outputAnnotationCount == 0,
StreamListenerErrorMessages.INVALID_INPUT_OUTPUT_METHOD_PARAMETERS);
}
if (StringUtils.hasText(methodAnnotatedInboundName)) {
Assert.isTrue(inputAnnotationCount == 0,
StreamListenerErrorMessages.INVALID_INPUT_VALUES);
Assert.isTrue(outputAnnotationCount == 0,
StreamListenerErrorMessages.INVALID_INPUT_VALUE_WITH_OUTPUT_METHOD_PARAM);
}
else {
Assert.isTrue(inputAnnotationCount >= 1,
StreamListenerErrorMessages.NO_INPUT_DESTINATION);
}
if (StringUtils.hasText(methodAnnotatedOutboundName)) {
Assert.isTrue(outputAnnotationCount == 0,
StreamListenerErrorMessages.INVALID_OUTPUT_VALUES);
}
if (!Void.TYPE.equals(method.getReturnType())) {
Assert.isTrue(!StringUtils.hasText(condition),
StreamListenerErrorMessages.CONDITION_ON_METHOD_RETURNING_VALUE);
}
if (isDeclarative) {
Assert.isTrue(!StringUtils.hasText(condition),
StreamListenerErrorMessages.CONDITION_ON_DECLARATIVE_METHOD);
for (int parameterIndex = 0; parameterIndex < methodArgumentsLength; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
if (methodParameter.hasParameterAnnotation(Input.class)) {
String inboundName = (String) AnnotationUtils.getValue(
methodParameter.getParameterAnnotation(Input.class));
Assert.isTrue(StringUtils.hasText(inboundName),
StreamListenerErrorMessages.INVALID_INBOUND_NAME);
}
if (methodParameter.hasParameterAnnotation(Output.class)) {
String outboundName = (String) AnnotationUtils.getValue(
methodParameter.getParameterAnnotation(Output.class));
Assert.isTrue(StringUtils.hasText(outboundName),
StreamListenerErrorMessages.INVALID_OUTBOUND_NAME);
}
}
if (methodArgumentsLength > 1) {
Assert.isTrue(
inputAnnotationCount
+ outputAnnotationCount == methodArgumentsLength,
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
if (!method.getReturnType().equals(Void.TYPE)) {
if (!StringUtils.hasText(methodAnnotatedOutboundName)) {
if (outputAnnotationCount == 0) {
throw new IllegalArgumentException(
StreamListenerErrorMessages.RETURN_TYPE_NO_OUTBOUND_SPECIFIED);
}
Assert.isTrue((outputAnnotationCount == 1),
StreamListenerErrorMessages.RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED);
}
}
}
protected static void validateStreamListenerMessageHandler(Method method) {
int methodArgumentsLength = method.getParameterTypes().length;
if (methodArgumentsLength > 1) {
int numAnnotatedMethodParameters = 0;
int numPayloadAnnotations = 0;
for (int parameterIndex = 0; parameterIndex < methodArgumentsLength; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
if (methodParameter.hasParameterAnnotations()) {
numAnnotatedMethodParameters++;
}
if (methodParameter.hasParameterAnnotation(Payload.class)) {
numPayloadAnnotations++;
}
}
if (numPayloadAnnotations > 0) {
Assert.isTrue(
methodArgumentsLength == numAnnotatedMethodParameters
&& numPayloadAnnotations <= 1,
StreamListenerErrorMessages.AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS);
}
}
}
protected static String getOutboundBindingTargetName(Method method) {
SendTo sendTo = AnnotationUtils.findAnnotation(method, SendTo.class);
if (sendTo != null) {
Assert.isTrue(!ObjectUtils.isEmpty(sendTo.value()),
StreamListenerErrorMessages.ATLEAST_ONE_OUTPUT);
Assert.isTrue(sendTo.value().length == 1,
StreamListenerErrorMessages.SEND_TO_MULTIPLE_DESTINATIONS);
Assert.hasText(sendTo.value()[0],
StreamListenerErrorMessages.SEND_TO_EMPTY_DESTINATION);
return sendTo.value()[0];
}
Output output = AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
Assert.isTrue(StringUtils.hasText(output.value()),
StreamListenerErrorMessages.ATLEAST_ONE_OUTPUT);
return output.value();
}
return null;
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import org.springframework.core.MethodParameter;
/**
* Strategy for adapting a method argument type annotated with
* {@link org.springframework.cloud.stream.annotation.Input} or
* {@link org.springframework.cloud.stream.annotation.Output} from a binding type (e.g.
* {@link org.springframework.messaging.MessageChannel}) supported by an existing binder.
*
* This is a framework extension and is not primarily intended for use by end-users.
*
* @param <A> adapter type
* @param <B> binding result type
* @author Marius Bogoevici
*/
public interface StreamListenerParameterAdapter<A, B> {
/**
* Return true if the conversion from the binding target type to the argument type is
* supported.
* @param bindingTargetType the binding target type
* @param methodParameter the method parameter for which the conversion is performed
* @return true if the conversion is supported
*/
boolean supports(Class<?> bindingTargetType, MethodParameter methodParameter);
/**
* Adapts the binding target to the argument type. The result will be passed as
* argument to a method annotated with
* {@link org.springframework.cloud.stream.annotation.StreamListener} when used for
* setting up a pipeline.
* @param bindingTarget the binding target
* @param parameter the method parameter for which the conversion is performed
* @return an instance of the parameter type, which will be passed to the method
*/
A adapt(B bindingTarget, MethodParameter parameter);
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.io.Closeable;
/**
* A strategy for adapting the result of a
* {@link org.springframework.cloud.stream.annotation.StreamListener} annotated method to
* a binding target annotated with
* {@link org.springframework.cloud.stream.annotation.Output}.
*
* Used when the {@link org.springframework.cloud.stream.annotation.StreamListener}
* annotated method is operating in declarative mode.
*
* @param <R> stream listener result type
* @param <B> binding target type
* @author Marius Bogoevici
*/
public interface StreamListenerResultAdapter<R, B> {
/**
* Return true if the result type can be converted to the binding target.
* @param resultType the result type.
* @param bindingTarget the binding target.
* @return true if the conversion can take place.
*/
boolean supports(Class<?> resultType, Class<?> bindingTarget);
/**
* Adapts the result to the binding target.
* @param streamListenerResult the result of invoking the method.
* @param bindingTarget the binding target.
* @return an adapted result
*/
Closeable adapt(R streamListenerResult, B bindingTarget);
}

View File

@@ -1,137 +0,0 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binding;
import java.lang.reflect.Method;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Orchestrator used for invoking the {@link StreamListener} setup method.
*
* By default {@link StreamListenerAnnotationBeanPostProcessor} will use an internal
* implementation of this interface to invoke {@link StreamListenerParameterAdapter}s and
* {@link StreamListenerResultAdapter}s or handler mappings on the method annotated with
* {@link StreamListener}.
*
* By providing a different implementation of this interface and registering it as a
* Spring Bean in the context, one can override the default invocation strategies used by
* the {@link StreamListenerAnnotationBeanPostProcessor}. A typical usecase for such
* overriding can happen when a downstream
* {@link org.springframework.cloud.stream.binder.Binder} implementation wants to change
* the way in which any of the default StreamListener handling needs to be changed in a
* custom manner.
*
* When beans of this interface are present in the context, they get priority in the
* {@link StreamListenerAnnotationBeanPostProcessor} before falling back to the default
* implementation.
*
* @author Soby Chacko
* @see StreamListener
* @see StreamListenerAnnotationBeanPostProcessor
*/
public interface StreamListenerSetupMethodOrchestrator {
/**
* Checks the method annotated with {@link StreamListener} to see if this
* implementation can successfully orchestrate this method.
* @param method annotated with {@link StreamListener}
* @return true if this implementation can orchestrate this method, false otherwise
*/
boolean supports(Method method);
/**
* Method that allows custom orchestration on the {@link StreamListener} setup method.
* @param streamListener reference to the {@link StreamListener} annotation on the
* method
* @param method annotated with {@link StreamListener}
* @param bean that contains the StreamListener method
*
*/
void orchestrateStreamListenerSetupMethod(StreamListener streamListener,
Method method, Object bean);
/**
* Default implementation for adapting each of the incoming method arguments using an
* available {@link StreamListenerParameterAdapter} and provide the adapted collection
* of arguments back to the caller.
* @param method annotated with {@link StreamListener}
* @param inboundName inbound binding
* @param applicationContext spring application context
* @param streamListenerParameterAdapters used for adapting the method arguments
* @return adapted incoming arguments
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
default Object[] adaptAndRetrieveInboundArguments(Method method, String inboundName,
ApplicationContext applicationContext,
StreamListenerParameterAdapter... streamListenerParameterAdapters) {
Object[] arguments = new Object[method.getParameterTypes().length];
for (int parameterIndex = 0; parameterIndex < arguments.length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method,
parameterIndex);
Class<?> parameterType = methodParameter.getParameterType();
Object targetReferenceValue = null;
if (methodParameter.hasParameterAnnotation(Input.class)) {
targetReferenceValue = AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Input.class));
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
targetReferenceValue = AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Output.class));
}
else if (arguments.length == 1 && StringUtils.hasText(inboundName)) {
targetReferenceValue = inboundName;
}
if (targetReferenceValue != null) {
Assert.isInstanceOf(String.class, targetReferenceValue,
"Annotation value must be a String");
Object targetBean = applicationContext
.getBean((String) targetReferenceValue);
// Iterate existing parameter adapters first
for (StreamListenerParameterAdapter streamListenerParameterAdapter : streamListenerParameterAdapters) {
if (streamListenerParameterAdapter.supports(targetBean.getClass(),
methodParameter)) {
arguments[parameterIndex] = streamListenerParameterAdapter
.adapt(targetBean, methodParameter);
break;
}
}
if (arguments[parameterIndex] == null
&& parameterType.isAssignableFrom(targetBean.getClass())) {
arguments[parameterIndex] = targetBean;
}
Assert.notNull(arguments[parameterIndex],
"Cannot convert argument " + parameterIndex + " of " + method
+ "from " + targetBean.getClass() + " to "
+ parameterType);
}
else {
throw new IllegalStateException(
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
return arguments;
}
}

View File

@@ -16,10 +16,9 @@
package org.springframework.cloud.stream.config;
import javax.validation.constraints.AssertTrue;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import jakarta.validation.constraints.AssertTrue;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;

View File

@@ -42,18 +42,14 @@ import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.binder.BinderType;
import org.springframework.cloud.stream.binder.BinderTypeRegistry;
import org.springframework.cloud.stream.binder.DefaultBinderFactory;
import org.springframework.cloud.stream.binding.AbstractBindingTargetFactory;
import org.springframework.cloud.stream.binding.Bindable;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.binding.BinderAwareRouter;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.BindingsLifecycleController;
import org.springframework.cloud.stream.binding.ContextStartAfterRefreshListener;
import org.springframework.cloud.stream.binding.DynamicDestinationsBindable;
import org.springframework.cloud.stream.binding.InputBindingLifecycle;
import org.springframework.cloud.stream.binding.MessageChannelStreamListenerResultAdapter;
import org.springframework.cloud.stream.binding.OutputBindingLifecycle;
import org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor;
import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider;
import org.springframework.cloud.stream.function.StreamFunctionProperties;
import org.springframework.cloud.stream.micrometer.DestinationPublishingMetricsAutoConfiguration;
@@ -177,12 +173,6 @@ public class BindingServiceConfiguration {
};
}
@Bean(name = STREAM_LISTENER_ANNOTATION_BEAN_POST_PROCESSOR_NAME)
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static StreamListenerAnnotationBeanPostProcessor streamListenerAnnotationBeanPostProcessor() {
return new StreamListenerAnnotationBeanPostProcessor();
}
@Bean
public BindingHandlerAdvise BindingHandlerAdvise(
@Nullable MappingsProvider[] providers) {
@@ -209,11 +199,6 @@ public class BindingServiceConfiguration {
return binderFactory;
}
@Bean
public MessageChannelStreamListenerResultAdapter messageChannelStreamListenerResultAdapter() {
return new MessageChannelStreamListenerResultAdapter();
}
@Bean
// This conditional is intentionally not in an autoconfig (usually a bad idea) because
// it is used to detect a BindingService in the parent context (which we know
@@ -253,18 +238,6 @@ public class BindingServiceConfiguration {
return new ContextStartAfterRefreshListener();
}
@SuppressWarnings("rawtypes")
@Bean
public BinderAwareChannelResolver binderAwareChannelResolver(
BindingService bindingService,
AbstractBindingTargetFactory<? extends MessageChannel> bindingTargetFactory,
DynamicDestinationsBindable dynamicDestinationsBindable,
@Nullable BinderAwareChannelResolver.NewDestinationBindingCallback callback) {
return new BinderAwareChannelResolver(bindingService, bindingTargetFactory,
dynamicDestinationsBindable, callback);
}
@Bean
public DynamicDestinationsBindable dynamicDestinationsBindable() {
return new DynamicDestinationsBindable();

View File

@@ -71,7 +71,7 @@ import org.springframework.cloud.stream.binder.BindingCreatedEvent;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.binding.BindableProxyFactory;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver.NewDestinationBindingCallback;
import org.springframework.cloud.stream.binding.NewDestinationBindingCallback;
import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration;
import org.springframework.cloud.stream.config.BindingBeansRegistrar;
import org.springframework.cloud.stream.config.BindingProperties;

View File

@@ -35,9 +35,9 @@ import org.springframework.cloud.function.context.message.MessageUtils;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver.NewDestinationBindingCallback;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.DefaultPartitioningInterceptor;
import org.springframework.cloud.stream.binding.NewDestinationBindingCallback;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel;

View File

@@ -1,205 +0,0 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.Bindable;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.DynamicDestinationsBindable;
import org.springframework.cloud.stream.binding.SubscribableChannelBindingTargetFactory;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.interceptor.GlobalChannelInterceptorWrapper;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
import org.springframework.messaging.support.InterceptableChannel;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Mark Fisher
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*/
public class BinderAwareChannelResolverTests {
protected ConfigurableApplicationContext context;
protected volatile BinderAwareChannelResolver resolver;
protected volatile Binder<MessageChannel, ConsumerProperties, ProducerProperties> binder;
protected volatile SubscribableChannelBindingTargetFactory bindingTargetFactory;
protected volatile BindingServiceProperties bindingServiceProperties;
protected volatile DynamicDestinationsBindable dynamicDestinationsBindable;
@SuppressWarnings("unchecked")
@Before
public void setupContext() throws Exception {
this.context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
BinderAwareChannelResolverTests.InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run();
this.resolver = this.context.getBean(BinderAwareChannelResolver.class);
this.binder = this.context.getBean(Binder.class);
this.bindingServiceProperties = this.context
.getBean(BindingServiceProperties.class);
this.bindingTargetFactory = this.context
.getBean(SubscribableChannelBindingTargetFactory.class);
}
@Test
public void resolveChannel() {
Map<String, Bindable> bindables = this.context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0); // producer
assertThat(bindable.getOutputs().size()).isEqualTo(0); // consumer
}
MessageChannel registered = this.resolver.resolveDestination("foo");
assertThat(((InterceptableChannel) registered).getInterceptors().size())
.isEqualTo(2);
assertThat(((InterceptableChannel) registered).getInterceptors()
.get(1) instanceof ImmutableMessageChannelInterceptor).isTrue();
bindables = this.context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0); // producer
assertThat(bindable.getOutputs().size()).isEqualTo(1); // consumer
}
DirectChannel testChannel = new DirectChannel();
testChannel.setComponentName("INPUT");
final CountDownLatch latch = new CountDownLatch(1);
final List<Message<?>> received = new ArrayList<>();
testChannel.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
received.add(message);
latch.countDown();
}
});
this.binder.bindConsumer("foo", null, testChannel, new ConsumerProperties());
assertThat(received).hasSize(0);
registered.send(MessageBuilder.withPayload("hello").build());
try {
assertThat(latch.await(1, TimeUnit.SECONDS)).describedAs("Latch timed out");
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("interrupted while awaiting latch");
}
assertThat(received).hasSize(1);
assertThat(new String((byte[]) received.get(0).getPayload())).isEqualTo("hello");
this.context.close();
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0);
assertThat(bindable.getOutputs().size()).isEqualTo(0); // Must not be bound"
}
}
@Test
public void resolveNonRegisteredChannel() {
MessageChannel other = this.resolver.resolveDestination("other");
assertThat(this.context.getBean("other")).isSameAs(other);
this.context.close();
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
public void propertyPassthrough() {
Map<String, BindingProperties> bindings = new HashMap<>();
BindingProperties genericProperties = new BindingProperties();
genericProperties.setContentType("text/plain");
bindings.put("foo", genericProperties);
this.bindingServiceProperties.setBindings(bindings);
Binder binder = mock(Binder.class);
Binder binder2 = mock(Binder.class);
BinderFactory mockBinderFactory = Mockito.mock(BinderFactory.class);
Binding<MessageChannel> fooBinding = Mockito.mock(Binding.class);
Binding<MessageChannel> barBinding = Mockito.mock(Binding.class);
when(binder.bindProducer(matches("foo"), any(DirectChannel.class),
any(ProducerProperties.class))).thenReturn(fooBinding);
when(binder2.bindProducer(matches("bar"), any(DirectChannel.class),
any(ProducerProperties.class))).thenReturn(barBinding);
when(mockBinderFactory.getBinder(null, DirectWithAttributesChannel.class))
.thenReturn(binder);
when(mockBinderFactory.getBinder("someTransport",
DirectWithAttributesChannel.class)).thenReturn(binder2);
BindingService bindingService = new BindingService(this.bindingServiceProperties,
mockBinderFactory, new ObjectMapper());
BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(
bindingService, this.bindingTargetFactory,
new DynamicDestinationsBindable());
resolver.setBeanFactory(this.context.getBeanFactory());
SubscribableChannel resolved = (SubscribableChannel) resolver
.resolveDestination("foo");
verify(binder).bindProducer(eq("foo"), any(MessageChannel.class),
any(ProducerProperties.class));
assertThat(resolved).isSameAs(this.context.getBean("foo"));
this.context.close();
}
@Configuration
public static class InterceptorConfiguration {
@Bean
public GlobalChannelInterceptorWrapper testInterceptor() {
return new GlobalChannelInterceptorWrapper(
new ImmutableMessageChannelInterceptor());
}
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.cloud.stream.binder;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.Test;
import org.mockito.Mockito;
@@ -23,14 +26,11 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@@ -58,9 +58,9 @@ public class ErrorBindingTests {
Binder binder = binderFactory.getBinder(null, MessageChannel.class);
Mockito.verify(binder).bindConsumer(eq("input"), isNull(),
Mockito.verify(binder).bindConsumer(eq("processor-in-0"), isNull(),
any(MessageChannel.class), any(ConsumerProperties.class));
Mockito.verify(binder).bindProducer(eq("output"), any(MessageChannel.class),
Mockito.verify(binder).bindProducer(eq("processor-out-0"), any(MessageChannel.class),
any(ProducerProperties.class));
Mockito.verifyNoMoreInteractions(binder);
applicationContext.close();
@@ -72,7 +72,7 @@ public class ErrorBindingTests {
TestChannelBinderConfiguration.getCompleteConfiguration(
ErrorBindingTests.ErrorConfigurationDefault.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.input.consumer.max-attempts=1",
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
@@ -91,7 +91,7 @@ public class ErrorBindingTests {
TestChannelBinderConfiguration.getCompleteConfiguration(
ErrorBindingTests.ErrorConfigurationWithCustomErrorHandler.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.input.consumer.max-attempts=1",
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
@@ -104,36 +104,41 @@ public class ErrorBindingTests {
assertThat(errorConfiguration.counter == 6);
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestProcessor {
@Bean
public Function<String, String> processor() {
return s -> s;
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class ErrorConfigurationDefault {
private int counter;
@StreamListener(Sink.INPUT)
public void handle(Object value) {
this.counter++;
throw new RuntimeException("BOOM!");
@Bean
public Consumer<Object> handle() {
return v -> {
this.counter++;
throw new RuntimeException("BOOM!");
};
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class ErrorConfigurationWithCustomErrorHandler {
private int counter;
@StreamListener(Sink.INPUT)
public void handle(Object value) {
this.counter++;
throw new RuntimeException("BOOM!");
@Bean
public Consumer<Object> handle() {
return v -> {
this.counter++;
throw new RuntimeException("BOOM!");
};
}
@ServiceActivator(inputChannel = "input.anonymous.errors")

View File

@@ -1,95 +0,0 @@
/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.cloud.stream.binding.Bindable;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Mark Fisher
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*/
public class ExtendedPropertiesBinderAwareChannelResolverTests
extends BinderAwareChannelResolverTests {
@Test
@Override
public void resolveChannel() {
Map<String, Bindable> bindables = this.context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0); // producer
assertThat(bindable.getOutputs().size()).isEqualTo(0); // consumer
}
MessageChannel registered = this.resolver.resolveDestination("foo");
bindables = this.context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0); // producer
assertThat(bindable.getOutputs().size()).isEqualTo(1); // consumer
}
DirectChannel testChannel = new DirectChannel();
final CountDownLatch latch = new CountDownLatch(1);
final List<Message<?>> received = new ArrayList<>();
testChannel.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
received.add(message);
latch.countDown();
}
});
this.binder.bindConsumer("foo", null, testChannel,
new ExtendedConsumerProperties<ConsumerProperties>(
new ConsumerProperties()));
assertThat(received).hasSize(0);
registered.send(MessageBuilder.withPayload("hello").build());
try {
assertThat(latch.await(1, TimeUnit.SECONDS)).describedAs("latch timed out");
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("interrupted while awaiting latch");
}
assertThat(received).hasSize(1);
assertThat(new String((byte[]) received.get(0).getPayload())).isEqualTo("hello");
this.context.close();
for (Bindable bindable : bindables.values()) {
assertThat(bindable.getInputs().size()).isEqualTo(0);
assertThat(bindable.getOutputs().size()).isEqualTo(0); // Must not be bound"
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.tck;
import org.junit.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
*
*/
public class ErrorHandlingTests {
@Test
public void testGlobalErrorWithMessage() {
ApplicationContext context = new SpringApplicationBuilder(
GlobalErrorHandlerWithErrorMessageConfig.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<>("foo".getBytes()));
GlobalErrorHandlerWithErrorMessageConfig config = context
.getBean(GlobalErrorHandlerWithErrorMessageConfig.class);
assertThat(config.globalErroInvoked).isTrue();
}
@Test
public void testGlobalErrorWithThrowable() {
ApplicationContext context = new SpringApplicationBuilder(
GlobalErrorHandlerWithThrowableConfig.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<>("foo".getBytes()));
GlobalErrorHandlerWithThrowableConfig config = context
.getBean(GlobalErrorHandlerWithThrowableConfig.class);
assertThat(config.globalErroInvoked).isTrue();
}
@EnableBinding(Processor.class)
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class GlobalErrorHandlerWithErrorMessageConfig {
private boolean globalErroInvoked;
@StreamListener(target = Processor.INPUT)
public void input(final String value) {
throw new RuntimeException("test exception");
}
@StreamListener("errorChannel")
public void generalError(Message<?> message) {
this.globalErroInvoked = true;
}
}
@EnableBinding(Processor.class)
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class GlobalErrorHandlerWithThrowableConfig {
private boolean globalErroInvoked;
@StreamListener(target = Processor.INPUT)
public void input(final String value) {
throw new RuntimeException("test exception");
}
@StreamListener("errorChannel")
public void generalError(Throwable exception) {
this.globalErroInvoked = true;
}
}
}

View File

@@ -0,0 +1,104 @@
///*
// * Copyright 2019-2019 the original author or authors.
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * https://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package org.springframework.cloud.stream.binder.tck;
//
//import org.junit.Test;
//
//import org.springframework.boot.WebApplicationType;
//import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
//import org.springframework.boot.builder.SpringApplicationBuilder;
//import org.springframework.cloud.stream.annotation.EnableBinding;
//import org.springframework.cloud.stream.annotation.StreamListener;
//import org.springframework.cloud.stream.binder.test.InputDestination;
//import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
//import org.springframework.cloud.stream.messaging.Processor;
//import org.springframework.context.ApplicationContext;
//import org.springframework.context.annotation.Import;
//import org.springframework.messaging.Message;
//import org.springframework.messaging.support.GenericMessage;
//
//import static org.assertj.core.api.Assertions.assertThat;
//
///**
// * @author Oleg Zhurakousky
// *
// */
//public class ErrorHandlingTests {
//
// @Test
// public void testGlobalErrorWithMessage() {
// ApplicationContext context = new SpringApplicationBuilder(
// GlobalErrorHandlerWithErrorMessageConfig.class)
// .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
// InputDestination source = context.getBean(InputDestination.class);
// source.send(new GenericMessage<>("foo".getBytes()));
// GlobalErrorHandlerWithErrorMessageConfig config = context
// .getBean(GlobalErrorHandlerWithErrorMessageConfig.class);
// assertThat(config.globalErroInvoked).isTrue();
// }
//
// @Test
// public void testGlobalErrorWithThrowable() {
// ApplicationContext context = new SpringApplicationBuilder(
// GlobalErrorHandlerWithThrowableConfig.class).web(WebApplicationType.NONE)
// .run("--spring.jmx.enabled=false");
// InputDestination source = context.getBean(InputDestination.class);
// source.send(new GenericMessage<>("foo".getBytes()));
// GlobalErrorHandlerWithThrowableConfig config = context
// .getBean(GlobalErrorHandlerWithThrowableConfig.class);
// assertThat(config.globalErroInvoked).isTrue();
// }
//
// @EnableBinding(Processor.class)
// @Import(TestChannelBinderConfiguration.class)
// @EnableAutoConfiguration
// public static class GlobalErrorHandlerWithErrorMessageConfig {
//
// private boolean globalErroInvoked;
//
// @StreamListener(target = Processor.INPUT)
// public void input(final String value) {
// throw new RuntimeException("test exception");
// }
//
// @StreamListener("errorChannel")
// public void generalError(Message<?> message) {
// this.globalErroInvoked = true;
// }
//
// }
//
// @EnableBinding(Processor.class)
// @Import(TestChannelBinderConfiguration.class)
// @EnableAutoConfiguration
// public static class GlobalErrorHandlerWithThrowableConfig {
//
// private boolean globalErroInvoked;
//
// @StreamListener(target = Processor.INPUT)
// public void input(final String value) {
// throw new RuntimeException("test exception");
// }
//
// @StreamListener("errorChannel")
// public void generalError(Throwable exception) {
// this.globalErroInvoked = true;
// }
//
// }
//
//}

View File

@@ -1,91 +0,0 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.test;
import java.nio.charset.StandardCharsets;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.PollableMessageSource;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Sample spring cloud stream application that demonstrates the usage of
* {@link TestChannelBinder}.
*
* @author Oleg Zhurakousky
* @author Gary Russell
*
*/
@SpringBootApplication
@EnableBinding(SampleStreamApp.PolledConsumer.class)
@Import(TestChannelBinderConfiguration.class)
public class SampleStreamApp {
public static void main(String[] args) {
ApplicationContext context = new SpringApplicationBuilder(SampleStreamApp.class)
.web(WebApplicationType.NONE).run("--server.port=0");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
Message<?> message = target.receive();
assertThat(new String((byte[]) message.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("Hello");
}
@Bean
public ApplicationRunner runner(PollableMessageSource pollableSource) {
return args -> pollableSource.poll(message -> {
System.out.println("Polled payload: " + message.getPayload());
});
}
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(String value) {
System.out.println("Handling payload: " + value);
return value;
}
@ServiceActivator(inputChannel = "input.anonymous.errors")
public void error(String value) {
System.out.println("Handling ERROR payload: " + value);
}
public interface PolledConsumer extends Processor {
@Input
PollableMessageSource pollableSource();
}
}

View File

@@ -0,0 +1,91 @@
///*
// * Copyright 2017-2018 the original author or authors.
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * https://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package org.springframework.cloud.stream.binder.test;
//
//import java.nio.charset.StandardCharsets;
//
//import org.springframework.boot.ApplicationRunner;
//import org.springframework.boot.WebApplicationType;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
//import org.springframework.boot.builder.SpringApplicationBuilder;
//import org.springframework.cloud.stream.annotation.EnableBinding;
//import org.springframework.cloud.stream.annotation.Input;
//import org.springframework.cloud.stream.annotation.StreamListener;
//import org.springframework.cloud.stream.binder.PollableMessageSource;
//import org.springframework.cloud.stream.messaging.Processor;
//import org.springframework.context.ApplicationContext;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Import;
//import org.springframework.integration.annotation.ServiceActivator;
//import org.springframework.messaging.Message;
//import org.springframework.messaging.handler.annotation.SendTo;
//import org.springframework.messaging.support.GenericMessage;
//
//import static org.assertj.core.api.Assertions.assertThat;
//
///**
// * Sample spring cloud stream application that demonstrates the usage of
// * {@link TestChannelBinder}.
// *
// * @author Oleg Zhurakousky
// * @author Gary Russell
// *
// */
//@SpringBootApplication
//@EnableBinding(SampleStreamApp.PolledConsumer.class)
//@Import(TestChannelBinderConfiguration.class)
//public class SampleStreamApp {
//
// public static void main(String[] args) {
// ApplicationContext context = new SpringApplicationBuilder(SampleStreamApp.class)
// .web(WebApplicationType.NONE).run("--server.port=0");
// InputDestination source = context.getBean(InputDestination.class);
// OutputDestination target = context.getBean(OutputDestination.class);
// source.send(new GenericMessage<byte[]>("Hello".getBytes()));
//
// Message<?> message = target.receive();
// assertThat(new String((byte[]) message.getPayload(), StandardCharsets.UTF_8))
// .isEqualTo("Hello");
// }
//
// @Bean
// public ApplicationRunner runner(PollableMessageSource pollableSource) {
// return args -> pollableSource.poll(message -> {
// System.out.println("Polled payload: " + message.getPayload());
// });
// }
//
// @StreamListener(Processor.INPUT)
// @SendTo(Processor.OUTPUT)
// public String receive(String value) {
// System.out.println("Handling payload: " + value);
// return value;
// }
//
// @ServiceActivator(inputChannel = "input.anonymous.errors")
// public void error(String value) {
// System.out.println("Handling ERROR payload: " + value);
// }
//
// public interface PolledConsumer extends Processor {
//
// @Input
// PollableMessageSource pollableSource();
//
// }
//
//}

View File

@@ -24,23 +24,16 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -58,14 +51,11 @@ import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.DefaultBinderFactory;
import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.reflection.GenericsUtils;
@@ -80,21 +70,16 @@ import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -326,93 +311,94 @@ public class BindingServiceTests {
binderFactory.destroy();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void checkDynamicBinding() {
BindingServiceProperties properties = new BindingServiceProperties();
BindingProperties bindingProperties = new BindingProperties();
bindingProperties.setProducer(new ProducerProperties());
properties.setBindings(Collections.singletonMap("foo", bindingProperties));
DefaultBinderFactory binderFactory = createMockBinderFactory();
final ExtendedPropertiesBinder binder = mock(ExtendedPropertiesBinder.class);
Properties extendedProps = new Properties();
when(binder.getExtendedProducerProperties(anyString())).thenReturn(extendedProps);
Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
final AtomicReference<MessageChannel> dynamic = new AtomicReference<>();
when(binder.bindProducer(matches("foo"), any(DirectChannel.class),
any(ProducerProperties.class))).thenReturn(mockBinding);
BindingService bindingService = new BindingService(properties, binderFactory, new ObjectMapper()) {
@Override
protected <T> Binder<T, ?, ?> getBinder(String channelName,
Class<T> bindableType) {
return binder;
}
};
SubscribableChannelBindingTargetFactory bindableSubscribableChannelFactory;
bindableSubscribableChannelFactory = new SubscribableChannelBindingTargetFactory(
new MessageConverterConfigurer(properties,
new CompositeMessageConverterFactory().getMessageConverterForAllRegistered()));
final AtomicBoolean callbackInvoked = new AtomicBoolean();
BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(
bindingService, bindableSubscribableChannelFactory,
new DynamicDestinationsBindable(), (name, channel, props, extended) -> {
callbackInvoked.set(true);
assertThat(name).isEqualTo("foo");
assertThat(channel).isNotNull();
assertThat(props).isNotNull();
assertThat(extended).isSameAs(extendedProps);
props.setUseNativeEncoding(true);
extendedProps.setProperty("bar", "baz");
});
ConfigurableListableBeanFactory beanFactory = mock(
ConfigurableListableBeanFactory.class);
when(beanFactory.getBean("foo", MessageChannel.class))
.thenThrow(new NoSuchBeanDefinitionException(MessageChannel.class));
when(beanFactory.getBean("bar", MessageChannel.class))
.thenThrow(new NoSuchBeanDefinitionException(MessageChannel.class));
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
dynamic.set(invocation.getArgument(1));
return null;
}
}).when(beanFactory).registerSingleton(eq("foo"), any(MessageChannel.class));
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return dynamic.get();
}
}).when(beanFactory).initializeBean(any(MessageChannel.class), eq("foo"));
resolver.setBeanFactory(beanFactory);
MessageChannel resolved = resolver.resolveDestination("foo");
assertThat(resolved).isSameAs(dynamic.get());
ArgumentCaptor<ProducerProperties> captor = ArgumentCaptor
.forClass(ProducerProperties.class);
verify(binder).bindProducer(eq("foo"), eq(dynamic.get()), captor.capture());
assertThat(captor.getValue().isUseNativeEncoding()).isTrue();
assertThat(captor.getValue()).isInstanceOf(ExtendedProducerProperties.class);
assertThat(((ExtendedProducerProperties) captor.getValue()).getExtension())
.isSameAs(extendedProps);
doReturn(dynamic.get()).when(beanFactory).getBean("foo", MessageChannel.class);
properties.setDynamicDestinations(new String[] { "foo" });
resolved = resolver.resolveDestination("foo");
assertThat(resolved).isSameAs(dynamic.get());
properties.setDynamicDestinations(new String[] { "test" });
try {
resolver.resolveDestination("bar");
fail("Should throw an exception");
}
catch (DestinationResolutionException e) {
assertThat(e).hasMessageContaining(
"Failed to find MessageChannel bean with name 'bar'");
}
}
//TODO: Need to re-write the following test.
//@SuppressWarnings({ "rawtypes", "unchecked" })
// @Test
// public void checkDynamicBinding() {
// BindingServiceProperties properties = new BindingServiceProperties();
// BindingProperties bindingProperties = new BindingProperties();
// bindingProperties.setProducer(new ProducerProperties());
// properties.setBindings(Collections.singletonMap("foo", bindingProperties));
// DefaultBinderFactory binderFactory = createMockBinderFactory();
// final ExtendedPropertiesBinder binder = mock(ExtendedPropertiesBinder.class);
// Properties extendedProps = new Properties();
// when(binder.getExtendedProducerProperties(anyString())).thenReturn(extendedProps);
// Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
// final AtomicReference<MessageChannel> dynamic = new AtomicReference<>();
// when(binder.bindProducer(matches("foo"), any(DirectChannel.class),
// any(ProducerProperties.class))).thenReturn(mockBinding);
// BindingService bindingService = new BindingService(properties, binderFactory, new ObjectMapper()) {
//
// @Override
// protected <T> Binder<T, ?, ?> getBinder(String channelName,
// Class<T> bindableType) {
// return binder;
// }
//
// };
// SubscribableChannelBindingTargetFactory bindableSubscribableChannelFactory;
// bindableSubscribableChannelFactory = new SubscribableChannelBindingTargetFactory(
// new MessageConverterConfigurer(properties,
// new CompositeMessageConverterFactory().getMessageConverterForAllRegistered()));
// final AtomicBoolean callbackInvoked = new AtomicBoolean();
// BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(
// bindingService, bindableSubscribableChannelFactory,
// new DynamicDestinationsBindable(), (name, channel, props, extended) -> {
// callbackInvoked.set(true);
// assertThat(name).isEqualTo("foo");
// assertThat(channel).isNotNull();
// assertThat(props).isNotNull();
// assertThat(extended).isSameAs(extendedProps);
// props.setUseNativeEncoding(true);
// extendedProps.setProperty("bar", "baz");
// });
// ConfigurableListableBeanFactory beanFactory = mock(
// ConfigurableListableBeanFactory.class);
// when(beanFactory.getBean("foo", MessageChannel.class))
// .thenThrow(new NoSuchBeanDefinitionException(MessageChannel.class));
// when(beanFactory.getBean("bar", MessageChannel.class))
// .thenThrow(new NoSuchBeanDefinitionException(MessageChannel.class));
// doAnswer(new Answer<Void>() {
//
// @Override
// public Void answer(InvocationOnMock invocation) throws Throwable {
// dynamic.set(invocation.getArgument(1));
// return null;
// }
//
// }).when(beanFactory).registerSingleton(eq("foo"), any(MessageChannel.class));
// doAnswer(new Answer<Object>() {
//
// @Override
// public Object answer(InvocationOnMock invocation) throws Throwable {
// return dynamic.get();
// }
//
// }).when(beanFactory).initializeBean(any(MessageChannel.class), eq("foo"));
// resolver.setBeanFactory(beanFactory);
// MessageChannel resolved = resolver.resolveDestination("foo");
// assertThat(resolved).isSameAs(dynamic.get());
// ArgumentCaptor<ProducerProperties> captor = ArgumentCaptor
// .forClass(ProducerProperties.class);
// verify(binder).bindProducer(eq("foo"), eq(dynamic.get()), captor.capture());
// assertThat(captor.getValue().isUseNativeEncoding()).isTrue();
// assertThat(captor.getValue()).isInstanceOf(ExtendedProducerProperties.class);
// assertThat(((ExtendedProducerProperties) captor.getValue()).getExtension())
// .isSameAs(extendedProps);
// doReturn(dynamic.get()).when(beanFactory).getBean("foo", MessageChannel.class);
// properties.setDynamicDestinations(new String[] { "foo" });
// resolved = resolver.resolveDestination("foo");
// assertThat(resolved).isSameAs(dynamic.get());
// properties.setDynamicDestinations(new String[] { "test" });
// try {
// resolver.resolveDestination("bar");
// fail("Should throw an exception");
// }
// catch (DestinationResolutionException e) {
// assertThat(e).hasMessageContaining(
// "Failed to find MessageChannel bean with name 'bar'");
// }
// }
@Test
public void testProducerPropertiesValidation() {

View File

@@ -16,9 +16,8 @@
package org.springframework.cloud.stream.config;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;

View File

@@ -16,18 +16,15 @@
package org.springframework.cloud.stream.function;
import java.util.function.Consumer;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
@@ -40,6 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Oleg Zhurakousky
* @author David Turanski
*
*
* TODO: Need to rewrite this test.
*/
public class DynamicDestinationFunctionTests {
@@ -50,6 +49,7 @@ public class DynamicDestinationFunctionTests {
}
@Test
@Ignore
public void testEmptyConfiguration() {
TestChannelBinderConfiguration.applicationContextRunner(SampleConfiguration.class)
.withPropertyValues(
@@ -72,8 +72,8 @@ public class DynamicDestinationFunctionTests {
@EnableAutoConfiguration
public static class SampleConfiguration {
@Autowired
private BinderAwareChannelResolver resolver;
// @Autowired
// private BinderAwareChannelResolver resolver;
@Bean
public PartitionKeyExtractorStrategy keyExtractor() {
@@ -86,12 +86,12 @@ public class DynamicDestinationFunctionTests {
};
}
@Bean
public Consumer<String> cons() {
return value -> {
resolver.resolveDestination(value).send(new GenericMessage<String>(value));
};
}
// @Bean
// public Consumer<String> cons() {
// return value -> {
// resolver.resolveDestination(value).send(new GenericMessage<String>(value));
// };
// }
}
}

View File

@@ -45,8 +45,6 @@ import org.springframework.cloud.function.context.FunctionType;
import org.springframework.cloud.function.context.catalog.FunctionAroundWrapper;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.BindingCreatedEvent;
import org.springframework.cloud.stream.binder.test.FunctionBindingTestUtils;
@@ -56,7 +54,6 @@ import org.springframework.cloud.stream.binder.test.TestChannelBinderConfigurati
import org.springframework.cloud.stream.binding.BindingsLifecycleController;
import org.springframework.cloud.stream.binding.BindingsLifecycleController.State;
import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -466,16 +463,16 @@ public class ImplicitFunctionBindingTests {
}
}
@Test
public void testFunctionConfigDisabledIfStreamListenerIsUsed() {
System.clearProperty("spring.cloud.function.definition");
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(LegacyConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
assertThat(context.getBean("supplierInitializer").getClass().getSimpleName()).isEqualTo("NullBean");
}
}
// @Test
// public void testFunctionConfigDisabledIfStreamListenerIsUsed() {
// System.clearProperty("spring.cloud.function.definition");
// try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
// TestChannelBinderConfiguration.getCompleteConfiguration(LegacyConfiguration.class))
// .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
//
// assertThat(context.getBean("supplierInitializer").getClass().getSimpleName()).isEqualTo("NullBean");
// }
// }
@Test
public void testDeclaredTypeVsActualInstance() {
@@ -1340,16 +1337,6 @@ public class ImplicitFunctionBindingTests {
}
}
@EnableAutoConfiguration
@EnableBinding(Sink.class)
public static class LegacyConfiguration {
@StreamListener(Sink.INPUT)
public void handle(String value) {
}
}
@EnableAutoConfiguration
public static class EmptyConfiguration {

View File

@@ -21,11 +21,11 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.rules.ExpectedException;
import reactor.core.publisher.Flux;
@@ -70,6 +70,7 @@ public class SourceToFunctionsSupportTests {
}
@Test
@Ignore
public void testFunctionIsAppliedToExistingMessageSource() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
@@ -98,7 +99,7 @@ public class SourceToFunctionsSupportTests {
}
@Test
@Disabled // fails intermittently
@Ignore // fails intermittently
public void testFunctionsAreAppliedToExistingMessageSourceReactive() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
@@ -113,6 +114,7 @@ public class SourceToFunctionsSupportTests {
}
@Test
@Ignore
public void testImperativeSupplier() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
@@ -236,7 +238,7 @@ public class SourceToFunctionsSupportTests {
}
@Test
@Disabled
@Ignore
public void testFiniteFluxSupplierMessage() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(FunctionsConfiguration.class,

View File

@@ -37,7 +37,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver.NewDestinationBindingCallback;
import org.springframework.cloud.stream.binding.NewDestinationBindingCallback;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;