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:
Marius Bogoevici
2017-01-25 18:16:08 -05:00
committed by Gary Russell
parent a64860abc8
commit 076f0ac1cb
10 changed files with 649 additions and 93 deletions

View File

@@ -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);
}
};
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}