Introduce StreamListener annotation

Fixes #156

- Supports Spring Messaging infrastructure for argument mapping and type conversion, sharing the same configured converters as the ones configured on input/output channels;

- Also: refactored AbstractFromMessageConverter to return only payload - conversion to a message enriched with the content type of the channel is done by a separate wrapping converter;
This commit is contained in:
Marius Bogoevici
2016-03-09 01:45:00 -05:00
committed by Mark Pollack
parent 0aeef58495
commit 4d2ef3c4d0
20 changed files with 840 additions and 134 deletions

View File

@@ -0,0 +1,380 @@
/*
* Copyright 2016 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 static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
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.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.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.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
/**
* @author Marius Bogoevici
*/
public class BindingListenerTests {
@Test
public void testContentTypeConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestSink.class);
@SuppressWarnings("unchecked")
TestSink testSink = context.getBean(TestSink.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
assertTrue(testSink.latch.await(10, TimeUnit.SECONDS));
assertThat(testSink.receivedArguments, hasSize(1));
assertThat(testSink.receivedArguments.get(0), hasProperty("bar", equalTo("barbar" + id)));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testAnnotatedArguments() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithAnnotatedArguments.class);
TestPojoWithAnnotatedArguments testPojoWithAnnotatedArguments = context.getBean(TestPojoWithAnnotatedArguments.class);
Sink sink = context.getBean(Sink.class);
String id = UUID.randomUUID().toString();
sink.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").setHeader("testHeader", "testValue").build());
assertThat(testPojoWithAnnotatedArguments.receivedArguments, hasSize(3));
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(0), instanceOf(FooPojo.class));
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(0), hasProperty("bar", equalTo("barbar" + id)));
assertThat(testPojoWithAnnotatedArguments.receivedArguments.get(1), instanceOf(Map.class));
assertThat((Map<String, String>) testPojoWithAnnotatedArguments.receivedArguments.get(1),
hasEntry(MessageHeaders.CONTENT_TYPE, "application/json"));
assertThat((Map<String, String>) testPojoWithAnnotatedArguments.receivedArguments.get(1),
hasEntry(equalTo("testHeader"), equalTo("testValue")));
assertThat((String) testPojoWithAnnotatedArguments.receivedArguments.get(2), equalTo("application/json"));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testReturn() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestStringProcessor.class);
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("{\"bar\":\"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);
assertThat(testStringProcessor.receivedPojos, hasSize(1));
assertThat(testStringProcessor.receivedPojos.get(0), hasProperty("bar", equalTo("barbar" + id)));
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload(), equalTo("barbar" + id));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testReturnConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithMimeType.class,
"--spring.cloud.stream.bindings.output.contentType=application/json");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMimeType testPojoWithMimeType = context.getBean(TestPojoWithMimeType.class);
assertThat(testPojoWithMimeType.receivedPojos, hasSize(1));
assertThat(testPojoWithMimeType.receivedPojos.get(0), hasProperty("bar", equalTo("barbar" + id)));
Message<String> message = (Message<String>) collector.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload(), equalTo("{\"qux\":\"barbar" + id + "\"}"));
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE, String.class), equalTo("application/json"));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testReturnNoConversion() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithMimeType.class);
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMimeType testPojoWithMimeType = context.getBean(TestPojoWithMimeType.class);
assertThat(testPojoWithMimeType.receivedPojos, hasSize(1));
assertThat(testPojoWithMimeType.receivedPojos.get(0), hasProperty("bar", equalTo("barbar" + id)));
Message<BazPojo> message = (Message<BazPojo>) collector.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload().getQux(), equalTo("barbar" + id));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testReturnMessage() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithMessageReturn.class);
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
TestPojoWithMessageReturn testPojoWithMessageReturn = context.getBean(TestPojoWithMessageReturn.class);
assertThat(testPojoWithMessageReturn.receivedPojos, hasSize(1));
assertThat(testPojoWithMessageReturn.receivedPojos.get(0), hasProperty("bar", equalTo("barbar" + id)));
Message<BazPojo> message = (Message<BazPojo>) collector.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload().getQux(), equalTo("barbar" + id));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testMessageArgument() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestPojoWithMessageArgument.class);
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(), equalTo("barbar" + id));
Message<BazPojo> message = (Message<BazPojo>) collector.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload().getQux(), equalTo("barbar" + id));
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testDuplicateMapping() throws Exception {
try {
ConfigurableApplicationContext context = SpringApplication.run(TestDuplicateMapping.class);
fail("Exception expected on duplicate mapping");
}
catch (BeanCreationException e) {
assertThat(e.getCause().getMessage(), startsWith("Duplicate @StreamListener mapping"));
}
}
@Test
@SuppressWarnings("unchecked")
public void testHandlerBean() throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(TestHandlerBean.class,
"--spring.cloud.stream.bindings.output.contentType=application/json");
MessageCollector collector = context.getBean(MessageCollector.class);
Processor processor = context.getBean(Processor.class);
String id = UUID.randomUUID().toString();
processor.input().send(MessageBuilder.withPayload("{\"bar\":\"barbar" + id + "\"}")
.setHeader("contentType", "application/json").build());
HandlerBean handlerBean = context.getBean(HandlerBean.class);
assertThat(handlerBean.receivedPojos, hasSize(1));
assertThat(handlerBean.receivedPojos.get(0), hasProperty("bar", equalTo("barbar" + id)));
Message<String> message = (Message<String>) collector.forChannel(processor.output()).poll(1, TimeUnit.SECONDS);
assertThat(message, not(nullValue(Message.class)));
assertThat(message.getPayload(), equalTo("{\"qux\":\"barbar" + id + "\"}"));
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE, String.class), equalTo("application/json"));
context.close();
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestSink {
List<FooPojo> receivedArguments = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
@StreamListener(Sink.INPUT)
public void receive(FooPojo fooPojo) {
receivedArguments.add(fooPojo);
latch.countDown();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestStringProcessor {
List<FooPojo> receivedPojos = new ArrayList<>();
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(FooPojo fooPojo) {
receivedPojos.add(fooPojo);
return fooPojo.getBar();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMimeType {
List<FooPojo> receivedPojos = new ArrayList<>();
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public BazPojo receive(FooPojo fooPojo) {
receivedPojos.add(fooPojo);
BazPojo bazPojo = new BazPojo();
bazPojo.setQux(fooPojo.getBar());
return bazPojo;
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
List<Object> receivedArguments = new ArrayList<>();
@StreamListener(Processor.INPUT)
public void receive(@Payload FooPojo fooPojo, @Headers Map<String, Object> headers,
@Header(MessageHeaders.CONTENT_TYPE) String contentType) {
receivedArguments.add(fooPojo);
receivedArguments.add(headers);
receivedArguments.add(contentType);
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageReturn {
List<FooPojo> receivedPojos = new ArrayList<>();
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<?> receive(FooPojo fooPojo) {
receivedPojos.add(fooPojo);
BazPojo bazPojo = new BazPojo();
bazPojo.setQux(fooPojo.getBar());
return MessageBuilder.withPayload(bazPojo).setHeader("foo", "bar").build();
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestPojoWithMessageArgument {
List<Message<String>> receivedMessages = new ArrayList<>();
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public BazPojo receive(Message<String> fooMessage) {
receivedMessages.add(fooMessage);
BazPojo bazPojo = new BazPojo();
bazPojo.setQux(fooMessage.getPayload());
return bazPojo;
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestDuplicateMapping {
@StreamListener(Processor.INPUT)
public void receive(Message<String> fooMessage) {
}
@StreamListener(Processor.INPUT)
public void receive2(Message<String> fooMessage) {
}
}
@EnableBinding(Processor.class)
@EnableAutoConfiguration
public static class TestHandlerBean {
@Bean
public HandlerBean handlerBean() {
return new HandlerBean();
}
}
public static class HandlerBean {
List<FooPojo> receivedPojos = new ArrayList<>();
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public BazPojo receive(FooPojo fooMessage) {
receivedPojos.add(fooMessage);
BazPojo bazPojo = new BazPojo();
bazPojo.setQux(fooMessage.getBar());
return bazPojo;
}
}
public static class FooPojo {
private String bar;
public String getBar() {
return bar;
}
public void setBar(String bar) {
this.bar = bar;
}
}
public static class BazPojo {
private String qux;
public String getQux() {
return qux;
}
public void setQux(String qux) {
this.qux = qux;
}
}
}

View File

@@ -21,7 +21,6 @@ import org.springframework.cloud.stream.annotation.Bindings;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.converter.AbstractFromMessageConverter;
import org.springframework.cloud.stream.converter.MessageConverterUtils;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.binder.TestSupportBinder;
import org.springframework.context.annotation.Bean;
@@ -52,15 +51,15 @@ public class CustomMessageConverterTests {
@Test
public void testCustomMessageConverter() throws Exception {
assertTrue(customMessageConverters.size() == 2);
assertThat(customMessageConverters, hasItem(isA(FooConverter.class)));
assertThat(customMessageConverters, hasItem(isA(BarConverter.class)));
assertThat(customMessageConverters, hasItem(isA(FooToBarConverter.class)));
assertThat(customMessageConverters, hasItem(isA(BarToFooConverter.class)));
testSource.output().send(MessageBuilder.withPayload(new Foo("hi")).build());
@SuppressWarnings("unchecked")
Message<String> received = (Message<String>) ((TestSupportBinder) binderFactory.getBinder(null))
.messageCollector().forChannel(testSource.output()).poll(1, TimeUnit.SECONDS);
Assert.assertThat(received, notNullValue());
assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString(),
equalTo("application/x-java-object;type=org.springframework.cloud.stream.config.CustomMessageConverterTests$Bar"));
equalTo("test/bar"));
}
@EnableBinding(Source.class)
@@ -71,19 +70,19 @@ public class CustomMessageConverterTests {
@Bean
public AbstractFromMessageConverter fooConverter() {
return new FooConverter();
return new FooToBarConverter();
}
@Bean
public AbstractFromMessageConverter barConverter() {
return new BarConverter();
return new BarToFooConverter();
}
}
public static class FooConverter extends AbstractFromMessageConverter {
public static class FooToBarConverter extends AbstractFromMessageConverter {
public FooConverter() {
super(MimeType.valueOf("foo/test"));
public FooToBarConverter() {
super(MimeType.valueOf("test/bar"));
}
@Override
@@ -109,15 +108,14 @@ public class CustomMessageConverterTests {
logger.error(e.getMessage(), e);
return null;
}
return buildConvertedMessage(result, message.getHeaders(),
MessageConverterUtils.javaObjectMimeType(targetClass));
return result;
}
}
public static class BarConverter extends AbstractFromMessageConverter {
public static class BarToFooConverter extends AbstractFromMessageConverter {
public BarConverter() {
super(MimeType.valueOf("bar/test"));
public BarToFooConverter() {
super(MimeType.valueOf("test/foo"));
}
@Override
@@ -143,8 +141,7 @@ public class CustomMessageConverterTests {
logger.error(e.getMessage(), e);
return null;
}
return buildConvertedMessage(result, message.getHeaders(),
MessageConverterUtils.javaObjectMimeType(targetClass));
return result;
}
}

View File

@@ -1,2 +1,2 @@
spring.cloud.stream.bindings.output.destination=configure1
spring.cloud.stream.bindings.output.contentType=foo/test
spring.cloud.stream.bindings.output.contentType=test/bar