Add Dispatching Capabilities to @StreamListener
`@StreamListener` has support for a `condition` parameter, that contains a SpEL expression that is evaluated before the method is invoked. Fix #682 Move StreamListenerMessageHandler as a top level class Use dispatching and add test Refactor dispatching mechanism Throw error when conditions are used in declarative mode Make StreamListenerAnnotationBeanPostProcessor overridable Remove unused field in test Address some PR comments Add placeholder resolution Update how multiple matches work with return values - Methods with return values are not allowed to specify conditions - If multiple matches are detected (e.g. multiple methods without conditions, or a mix of methods with and without conditions) checks that all of them have no return value;
This commit is contained in:
committed by
Gary Russell
parent
a64860abc8
commit
076f0ac1cb
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.beans.factory.config.BeanPostProcessor;
|
||||
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.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();
|
||||
}
|
||||
|
||||
@EnableBinding(Sink.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TestPojoWithAnnotatedArguments {
|
||||
|
||||
List<StreamListenerTestUtils.FooPojo> receivedFoo = new ArrayList<>();
|
||||
|
||||
@StreamListener(value = Sink.INPUT, condition = "foo")
|
||||
public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo) {
|
||||
this.receivedFoo.add(fooPojo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default {@link StreamListenerAnnotationBeanPostProcessor}.
|
||||
*/
|
||||
@Bean(name = STREAM_LISTENER_ANNOTATION_BEAN_POST_PROCESSOR_NAME)
|
||||
public static BeanPostProcessor 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,12 @@ 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.junit.Assert.fail;
|
||||
@@ -38,14 +41,14 @@ public class StreamListenerDuplicateMappingTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDuplicateMapping() {
|
||||
public void testMultipleMappingsWithReturnValue() {
|
||||
ConfigurableApplicationContext context = null;
|
||||
try {
|
||||
context = SpringApplication.run(TestDuplicateMapping.class, "--server.port=0");
|
||||
context = SpringApplication.run(TestMultipleMappingsWithReturnValue.class, "--server.port=0");
|
||||
fail("Exception expected on duplicate mapping");
|
||||
}
|
||||
catch (BeanCreationException e) {
|
||||
assertThat(e.getCause().getMessage()).startsWith("Duplicate @StreamListener mapping");
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage()).startsWith(StreamListenerErrorMessages.MULTIPLE_VALUE_RETURNING_METHODS);
|
||||
}
|
||||
finally {
|
||||
if (context != null) {
|
||||
@@ -72,16 +75,20 @@ public class StreamListenerDuplicateMappingTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableBinding(Sink.class)
|
||||
@EnableBinding(Processor.class)
|
||||
@EnableAutoConfiguration
|
||||
public static class TestDuplicateMapping {
|
||||
public static class TestMultipleMappingsWithReturnValue {
|
||||
|
||||
@StreamListener(Sink.INPUT)
|
||||
public void receive(Message<String> fooMessage) {
|
||||
@StreamListener(Processor.INPUT)
|
||||
@SendTo(Processor.OUTPUT)
|
||||
public String receive(Message<String> fooMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@StreamListener(Sink.INPUT)
|
||||
public void receiveDuplicateMapping(Message<String> fooMessage) {
|
||||
@StreamListener(Processor.INPUT)
|
||||
@SendTo(Processor.OUTPUT)
|
||||
public String receiveDuplicateMapping(Message<String> fooMessage) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.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.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
|
||||
@SuppressWarnings("unchecked")
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConditionalFailsWithReturnValue() throws Exception {
|
||||
try {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(TestConditionalOnMethodWithReturnValueFails.class,
|
||||
"--server.port=0");
|
||||
context.close();
|
||||
fail("Context creation failure expected");
|
||||
} catch (BeanCreationException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessageContaining(StreamListenerErrorMessages.CONDITION_ON_METHOD_RETURNING_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConditionalFailsWithDeclarativeMethod() throws Exception {
|
||||
try {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(TestConditionalOnDeclarativeMethodFails.class,
|
||||
"--server.port=0");
|
||||
context.close();
|
||||
fail("Context creation failure expected");
|
||||
} catch (BeanCreationException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessageContaining(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user