GH-1742 Support Batch Listeners in Core (Function)

- Support `batchMode` in function properties so, for binders that support batch listeners, a message
  payload of `List<byte[]>` can be properly converted.
- In batch mode, handle
    `public Function<List<Person>, Person> func()`
    `public Function<List<List<Person>>, Person> func()`
    `Function<Message<List<Person>>, Person> func()`
- Also use `ParameterizedType` as a conversion hint for `public Function<List<Person>, Person> func()`
  when not in batch mode (json: `[{\"name\":\"bob\"},{\"name\":\"jill\"}]`). Previously, just the'
  class was used without generic type information.

Move batchMode to ConsumerProperties.

Fix imports

Remove duplicate test from if test for message conversion.
This commit is contained in:
Gary Russell
2019-06-19 14:47:50 -04:00
committed by Oleg Zhurakousky
parent 562a025cda
commit 9fd674909a
8 changed files with 1255 additions and 30 deletions

View File

@@ -587,6 +587,8 @@ support.
[[spring_cloud_function]]
==== Spring Cloud Function support
===== Overview
Since Spring Cloud Stream v2.1, another alternative for defining _stream handlers_ and _sources_ is to use build-in
support for https://cloud.spring.io/spring-cloud-function/[Spring Cloud Function] where they can be expressed as beans of
type `java.util.function.[Supplier/Function/Consumer]`.
@@ -712,7 +714,19 @@ For example, the above composition could be defined as such (if both functions p
--spring.cloud.stream.function.definition=reactiveUpperCase|wrapInQuotes
----
===== Batch Consumers
When using a `MessageChannelBinder` that supports batch listeners, and the feature is enabled for the consumer binding, you can set `spring.cloud.stream.function.definition` to `true` to enable the entire batch of messages to be passed to the function in a `List`.
====
[source, java]
----
@Bean
public Function<List<Person>, Person> findFirstPerson() {
return persons -> persons.get(0);
}
----
====
[[spring-cloud-streams-overview-using-polled-consumers]]
==== Using Polled Consumers

View File

@@ -17,12 +17,15 @@
package org.springframework.cloud.stream.binder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.Min;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.messaging.Message;
/**
* Common consumer properties - spring.cloud.stream.bindings.[destinationName].consumer.
*
@@ -157,6 +160,15 @@ public class ConsumerProperties {
*/
private boolean multiplex;
/**
* When set to true, if the binder supports it, the messages emitted will have a {@link List}
* payload; When used in conjunction with functions, the function can receive a list of
* objects (or {@link Message}s) with the payloads converted if necessary.
*
* @since 3.0
*/
private boolean batchMode;
public String getRetryTemplateName() {
return retryTemplateName;
}
@@ -285,4 +297,12 @@ public class ConsumerProperties {
this.autoStartup = autoStartup;
}
public boolean isBatchMode() {
return this.batchMode;
}
public void setBatchMode(boolean batchMode) {
this.batchMode = batchMode;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.stream.converter;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -44,7 +45,7 @@ import org.springframework.messaging.converter.MessageConversionException;
*/
class ApplicationJsonMessageMarshallingConverter extends MappingJackson2MessageConverter {
private final Map<ParameterizedTypeReference<?>, JavaType> typeCache = new ConcurrentHashMap<>();
private final Map<Type, JavaType> typeCache = new ConcurrentHashMap<>();
ApplicationJsonMessageMarshallingConverter(@Nullable ObjectMapper objectMapper) {
if (objectMapper != null) {
@@ -67,8 +68,8 @@ class ApplicationJsonMessageMarshallingConverter extends MappingJackson2MessageC
}
@Override
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
@Nullable Object conversionHint) {
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object hint) {
Object conversionHint = hint;
Object result = null;
if (conversionHint instanceof MethodParameter) {
Class<?> conversionHintType = ((MethodParameter) conversionHint)
@@ -87,12 +88,14 @@ class ApplicationJsonMessageMarshallingConverter extends MappingJackson2MessageC
ParameterizedTypeReference<Object> forType = ParameterizedTypeReference
.forType(((MethodParameter) conversionHint)
.getGenericParameterType());
result = convertParameterizedType(message, targetClass, forType);
result = convertParameterizedType(message, forType.getType());
}
}
else if (conversionHint instanceof ParameterizedTypeReference) {
result = convertParameterizedType(message, targetClass,
(ParameterizedTypeReference<?>) conversionHint);
result = convertParameterizedType(message, ((ParameterizedTypeReference<?>) conversionHint).getType());
}
else if (conversionHint instanceof ParameterizedType) {
result = convertParameterizedType(message, (Type) conversionHint);
}
if (result == null) {
@@ -109,15 +112,14 @@ class ApplicationJsonMessageMarshallingConverter extends MappingJackson2MessageC
return result;
}
private Object convertParameterizedType(Message<?> message, Class<?> targetClass,
ParameterizedTypeReference<?> conversionHint) {
private Object convertParameterizedType(Message<?> message, Type conversionHint) {
ObjectMapper objectMapper = this.getObjectMapper();
Object payload = message.getPayload();
try {
JavaType type = this.typeCache.get(conversionHint);
if (type == null) {
type = objectMapper.getTypeFactory()
.constructType((conversionHint).getType());
.constructType(conversionHint);
this.typeCache.put(conversionHint, type);
}
if (payload instanceof byte[]) {

View File

@@ -17,23 +17,26 @@
package org.springframework.cloud.stream.function;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.FunctionType;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.integration.support.MessageBuilder;
@@ -52,6 +55,7 @@ import org.springframework.util.ReflectionUtils;
* @author Oleg Zhurakousky
* @author David Turanski
* @author Tolga Kavukcu
* @author Gary Russell
* @since 2.1
*/
class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O>>> {
@@ -68,16 +72,22 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
private final Class<?> inputClass;
private final ParameterizedType inputParameterizedType;
private final Class<?> outputClass;
private final Function<Flux<?>, Flux<?>> userFunction;
private final CompositeMessageConverter messageConverter;
private final BeanFactory beanFactory;
private final MessageChannel errorChannel;
private final boolean isInputArgumentMessage;
private final Class<?> messagePayloadClass;
private final Type messagePayloadType;
private final ConsumerProperties consumerProperties;
private final ProducerProperties producerProperties;
@@ -86,6 +96,12 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
private final StreamFunctionProperties functionProperties;
private final boolean batchMode;
private final Type listContentParameterizedType;
private final Class<?> listContentClass;
FunctionInvoker(StreamFunctionProperties functionProperties,
FunctionCatalog functionCatalog, FunctionInspector functionInspector,
CompositeMessageConverterFactory compositeMessageConverterFactory) {
@@ -97,7 +113,7 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
FunctionInvoker(StreamFunctionProperties functionProperties,
FunctionCatalog functionCatalog, FunctionInspector functionInspector,
CompositeMessageConverterFactory compositeMessageConverterFactory,
BeanFactory beanFactory) {
MessageChannel errorChannel) {
this.functionProperties = functionProperties;
Object originalUserFunction = functionCatalog
@@ -113,12 +129,63 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
this.isInputArgumentMessage = functionType.isMessage();
this.inputClass = functionType.getInputType();
this.outputClass = functionType.getOutputType();
this.beanFactory = beanFactory;
this.errorChannel = errorChannel;
this.bindingServiceProperties = functionProperties.getBindingServiceProperties();
this.consumerProperties = this.bindingServiceProperties
.getConsumerProperties(functionProperties.getInputDestinationName());
this.producerProperties = this.bindingServiceProperties
.getProducerProperties(functionProperties.getOutputDestinationName());
this.batchMode = this.consumerProperties.isBatchMode();
Type type = functionType.getType();
ParameterizedType functionInputParameterizedType = null;
Type listContainsType = null;
Type payloadType = null;
if (type instanceof ParameterizedType) {
Type functionInputType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (functionInputType instanceof ParameterizedType) {
functionInputParameterizedType = (ParameterizedType) functionInputType;
Type rawType = ((ParameterizedType) functionInputType).getRawType();
if (rawType.equals(List.class)) {
listContainsType = ((ParameterizedType) functionInputType).getActualTypeArguments()[0];
}
else if (rawType.equals(Message.class)) {
payloadType = determinePayloadType(functionInputType);
}
}
}
if (listContainsType instanceof Class) {
this.listContentClass = (Class<?>) listContainsType;
this.listContentParameterizedType = null;
}
else {
this.listContentClass = Object.class;
this.listContentParameterizedType = listContainsType;
}
if ((functionInputParameterizedType != null && functionInputParameterizedType.getRawType().equals(Flux.class))
|| payloadType != null) {
functionInputParameterizedType = null;
}
this.inputParameterizedType = functionInputParameterizedType;
if (payloadType instanceof Class) {
this.messagePayloadClass = (Class<?>) payloadType;
this.messagePayloadType = null;
}
else {
this.messagePayloadClass = Object.class;
this.messagePayloadType = payloadType;
}
}
private Type determinePayloadType(Type functionInputType) {
Type payloadType;
payloadType = ((ParameterizedType) functionInputType).getActualTypeArguments()[0];
if (payloadType instanceof ParameterizedType) {
Type payloadRawType = ((ParameterizedType) payloadType).getRawType();
if (payloadRawType.equals(List.class)) {
payloadType = ((ParameterizedType) payloadType).getActualTypeArguments()[0];
}
}
return payloadType;
}
@Override
@@ -127,7 +194,8 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
return input.concatMap(message -> {
return Flux.just(message).doOnNext(originalMessageRef::set)
.map(this::resolveArgument).transform(this.userFunction::apply)
.map(this::resolveArgument)
.transform(this.userFunction::apply)
.retryBackoff(this.consumerProperties.getMaxAttempts(),
Duration.ofMillis(
this.consumerProperties.getBackOffInitialInterval()),
@@ -143,19 +211,10 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
}
private void onError(Throwable t, Message<I> originalMessage) {
String inputDestinationName = functionProperties.getInputDestinationName();
BindingProperties bindingProperties = functionProperties.getBindingServiceProperties().getBindings().get(inputDestinationName);
String destinationName = bindingProperties.getDestination();
String groupName = bindingProperties.getGroup();
String bindingErrorChannelName = destinationName + "." + groupName + ".errors";
if (beanFactory != null) {
MessageChannel errorChannel = beanFactory.containsBean(bindingErrorChannelName)
? beanFactory.getBean(bindingErrorChannelName, MessageChannel.class)
: beanFactory.getBean("errorChannel", MessageChannel.class);
ErrorMessage em = new ErrorMessage(t, originalMessage.getHeaders(), (Message<?>) originalMessage);
if (this.errorChannel != null) {
ErrorMessage em = new ErrorMessage(t, originalMessage);
logger.error(em);
errorChannel.send(em);
this.errorChannel.send(em);
}
else {
logger.error(t);
@@ -221,19 +280,48 @@ class FunctionInvoker<I, O> implements Function<Flux<Message<I>>, Flux<Message<O
}
T argument = (T) (shouldConvertFromMessage(message)
? this.messageConverter.fromMessage(message, this.inputClass) : message);
? this.messageConverter.fromMessage(message, this.inputClass, this.inputParameterizedType) : message);
Assert.notNull(argument, "Failed to resolve argument type '" + this.inputClass
+ "' from message: " + message);
if (this.isInputArgumentMessage && !(argument instanceof Message)) {
if (this.batchMode
&& this.messagePayloadClass != null
&& this.isInputArgumentMessage
&& argument instanceof Message
&& ((Message<?>) argument).getPayload() instanceof List
&& !this.messagePayloadClass.isAssignableFrom(((Message<?>) argument).getPayload().getClass())) {
argument = (T) MessageBuilder
.withPayload(convertListContents(message.getPayload(), this.messagePayloadClass,
this.messagePayloadType))
.build();
}
else if (this.isInputArgumentMessage && !(argument instanceof Message)) {
if (shouldBatchConvert(argument)) {
argument = convertListContents(argument, this.messagePayloadClass, this.messagePayloadType);
}
argument = (T) MessageBuilder.withPayload(argument)
.copyHeaders(message.getHeaders()).build();
}
else if (!this.isInputArgumentMessage && argument instanceof Message) {
argument = ((Message<T>) argument).getPayload();
if (shouldBatchConvert(argument)) {
argument = convertListContents(argument, this.listContentClass, this.listContentParameterizedType);
}
}
return argument;
}
private <T> boolean shouldBatchConvert(T argument) {
return this.batchMode && argument instanceof List && this.listContentClass != null;
}
@SuppressWarnings("unchecked")
private <T> T convertListContents(T argument, Class<?> targetClass, Type hint) {
return (T) ((List<?>) argument).stream()
.map(payload -> this.messageConverter.fromMessage(MessageBuilder.withPayload(payload).build(),
targetClass, hint))
.collect(Collectors.toList());
}
private boolean shouldConvertFromMessage(Message<?> message) {
return !this.inputClass.isAssignableFrom(Message.class)
&& !this.inputClass.isAssignableFrom(message.getPayload().getClass())

View File

@@ -49,6 +49,8 @@ public class StreamFunctionProperties {
private Map<String, List<String>> outputBindings = new HashMap<>();
private boolean batchMode;
private boolean composeTo;
private boolean composeFrom;
@@ -110,7 +112,13 @@ public class StreamFunctionProperties {
public void setInputBindings(Map<String, List<String>> inputBindings) {
this.inputBindings = inputBindings;
}
public boolean isBatchMode() {
return this.batchMode;
}
public void setBatchMode(boolean batchMode) {
this.batchMode = batchMode;
}
}

View File

@@ -33,7 +33,7 @@ public class InputDestination extends AbstractDestination {
* destination (e.g., Processor.INPUT).
* @param message message to send
*/
public void send(Message<byte[]> message) {
public void send(Message<?> message) {
this.getChannel().send(message);
}

View File

@@ -0,0 +1,257 @@
/*
* 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.function;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
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.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Gary Russel
* @author Oleg Zhurakousky
*
* @since 3.0
*/
public class FunctionBatchingTests {
@Test
public void testMessageBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
MessageBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
}
}
@Test
public void testListPayloadConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ListPayloadNotBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes())
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
}
}
@Test
public void testSimpleBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
SimpleBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
}
}
@Test
public void testNestedBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
NestedBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
}
}
@EnableAutoConfiguration
public static class SimpleBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class ListPayloadNotBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class NestedBatchConfiguration {
@Bean
public Function<List<List<Person>>, Person> func() {
return x -> x.get(0).get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class MessageBatchConfiguration {
@Bean
public Function<Message<List<Person>>, Person> func() {
return x -> x.getPayload().get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
}

View File

@@ -0,0 +1,836 @@
/*
* 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.function;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamMessageConverter;
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.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.cloud.stream.function.pojo.Baz;
import org.springframework.cloud.stream.function.pojo.ErrorBaz;
import org.springframework.cloud.stream.messaging.Processor;
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.converter.MessageConverter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
* @author Tolga Kavukcu
* @author Gary Russell
*
*/
public class FunctionInvokerTests {
private static String testWithFluxedConsumerValue;
@Test
public void testSimpleEchoConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
SimpleEchoConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
@Test
public void testFluxPojoFunction() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(SimpleFluxFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload()).isEqualTo("Person: bob".getBytes());
}
}
@Test
public void testFluxMessagePojoFunction() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
SimpleFluxMessageFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload()).isEqualTo("Person: bob".getBytes());
}
}
@Test
public void testFunctionHonorsOutboundBindingContentType() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ConverterDoesNotProduceCTConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.output.contentType=text/plain")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes())
.setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)
.toString()).isEqualTo("text/plain");
}
}
@Test
public void testFunctionHonorsConverterSetContentType() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ConverterInjectingCTConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.output.contentType=text/plain")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes())
.setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)
.toString()).isEqualTo("ping/pong");
}
}
@Test
public void testSameMessageTypesAreNotConverted() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(MyFunctionsConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false")) {
Message<Foo> inputMessage = new GenericMessage<>(new Foo());
StreamFunctionProperties functionProperties = createStreamFunctionProperties();
functionProperties.setDefinition("messageToMessageSameType");
FunctionInvoker<Foo, Foo> messageToMessageSameType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
Message<Foo> outputMessage = messageToMessageSameType
.apply(Flux.just(inputMessage)).blockFirst();
assertThat(inputMessage).isSameAs(outputMessage);
functionProperties.setDefinition("pojoToPojoSameType");
FunctionInvoker<Foo, Foo> pojoToPojoSameType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
outputMessage = pojoToPojoSameType.apply(Flux.just(inputMessage))
.blockFirst();
assertThat(inputMessage.getPayload()).isEqualTo(outputMessage.getPayload());
functionProperties.setDefinition("messageToMessageNoType");
FunctionInvoker<Foo, Foo> messageToMessageNoType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
outputMessage = messageToMessageNoType.apply(Flux.just(inputMessage))
.blockFirst();
assertThat(outputMessage).isInstanceOf(Message.class);
functionProperties.setDefinition("withException");
FunctionInvoker<Foo, Foo> withException = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
Flux<Message<Foo>> fluxOfMessages = Flux
.just(new GenericMessage<>(new ErrorFoo()), inputMessage);
Message<Foo> resultMessage = withException.apply(fluxOfMessages).blockFirst();
assertThat(resultMessage.getPayload()).isNotInstanceOf(ErrorFoo.class);
}
}
@Test
public void testNativeEncodingEnabled() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(MyFunctionsConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false")) {
Message<Baz> inputMessage = new GenericMessage<>(new Baz());
StreamFunctionProperties functionProperties = createStreamFunctionPropertiesWithNativeEncoding();
functionProperties.setDefinition("pojoToPojoNonEmptyPojo");
FunctionInvoker<Baz, Baz> pojoToPojoSameType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
Message<Baz> outputMessage = pojoToPojoSameType.apply(Flux.just(inputMessage))
.blockFirst();
assertThat(inputMessage.getPayload()).isEqualTo(outputMessage.getPayload());
Message<Baz> inputMessageWithBaz = new GenericMessage<>(new Baz());
functionProperties.setDefinition("messageToMessageNoType");
FunctionInvoker<Baz, Baz> messageToMessageNoType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
outputMessage = messageToMessageNoType.apply(Flux.just(inputMessageWithBaz))
.blockFirst();
assertThat(outputMessage).isInstanceOf(Message.class);
functionProperties.setDefinition("withExceptionNativeEncodingEnabled");
FunctionInvoker<Baz, Baz> withException = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
Flux<Message<Baz>> fluxOfMessages = Flux
.just(new GenericMessage<>(new ErrorBaz()), inputMessage);
Message<Baz> resultMessage = withException.apply(fluxOfMessages).blockFirst();
assertThat(resultMessage.getPayload()).isNotInstanceOf(ErrorFoo.class);
}
}
@Test
public void testWithOutNativeEncodingEnabled() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(MyFunctionsConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false")) {
Message<Baz> inputMessage = new GenericMessage<>(new Baz());
StreamFunctionProperties functionProperties = createStreamFunctionProperties();
functionProperties.setDefinition("pojoToPojoNonEmptyPojo");
FunctionInvoker<Baz, Baz> pojoToPojoSameType = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
Message<Baz> outputMessage = pojoToPojoSameType.apply(Flux.just(inputMessage))
.blockFirst();
assertThat(outputMessage).isNotNull();
assertThat(inputMessage.getPayload())
.isNotEqualTo(outputMessage.getPayload());
}
}
@Test
public void testWithFluxedConsumer() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(MyFunctionsConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false")) {
String value = "Hello";
Message<String> inputMessage = new GenericMessage<>(value);
StreamFunctionProperties functionProperties = createStreamFunctionProperties();
functionProperties.setDefinition("fluxConsumer");
FunctionInvoker<String, Void> fluxedConsumer = new FunctionInvoker<>(
functionProperties,
context.getBean(FunctionCatalog.class),
context.getBean(FunctionInspector.class),
context.getBean(CompositeMessageConverterFactory.class));
fluxedConsumer.apply(Flux.just(inputMessage)).blockFirst();
assertThat(testWithFluxedConsumerValue).isEqualTo(value);
}
}
@Test
public void testListPayloadConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ListPayloadNotBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes())
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
@Test
public void testSimpleBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
SimpleBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
@Test
public void testNestedBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
NestedBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
@Test
public void testMessageBatchConfiguration() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
MessageBatchConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=func",
"--spring.cloud.stream.bindings.input.consumer.batch-mode=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
private StreamFunctionProperties createStreamFunctionProperties() {
StreamFunctionProperties functionProperties = new StreamFunctionProperties();
functionProperties.setInputDestinationName("input");
functionProperties.setOutputDestinationName("output");
BindingServiceProperties bindingServiceProperties = new BindingServiceProperties();
bindingServiceProperties.getConsumerProperties("input").setMaxAttempts(3);
try {
Field f = ReflectionUtils.findField(StreamFunctionProperties.class,
"bindingServiceProperties");
f.setAccessible(true);
f.set(functionProperties, bindingServiceProperties);
return functionProperties;
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
private StreamFunctionProperties createStreamFunctionPropertiesWithNativeEncoding() {
StreamFunctionProperties functionProperties = new StreamFunctionProperties();
functionProperties.setInputDestinationName("input");
functionProperties.setOutputDestinationName("output");
BindingServiceProperties bindingServiceProperties = new BindingServiceProperties();
bindingServiceProperties.getConsumerProperties("input").setMaxAttempts(3);
bindingServiceProperties.getProducerProperties("output")
.setUseNativeEncoding(true);
try {
Field bspField = ReflectionUtils.findField(StreamFunctionProperties.class,
"bindingServiceProperties");
bspField.setAccessible(true);
bspField.set(functionProperties, bindingServiceProperties);
return functionProperties;
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class SimpleEchoConfiguration {
@Bean
public Function<Person, Person> func() {
return x -> x;
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class SimpleFluxFunctionConfiguration {
@Bean
public Function<Flux<Person>, Flux<String>> func() {
return x -> x.map(person -> person.toString());
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person: " + name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class SimpleFluxMessageFunctionConfiguration {
@Bean
public Function<Flux<Message<Person>>, Flux<Message<String>>> func() {
return x -> x.map(personMessage -> {
Person person = personMessage.getPayload();
Message<String> message = MessageBuilder.withPayload(person.toString())
.copyHeaders(personMessage.getHeaders()).build();
return message;
});
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person: " + name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class ConverterDoesNotProduceCTConfiguration {
@Bean
public Function<String, String> func() {
return x -> x;
}
@StreamMessageConverter
public MessageConverter customConverter() {
return new MessageConverter() {
@Override
public Message<?> toMessage(Object payload, MessageHeaders headers) {
return new GenericMessage<byte[]>(((String) payload).getBytes());
}
@Override
public Object fromMessage(Message<?> message, Class<?> targetClass) {
String contentType = message.getHeaders()
.get(MessageHeaders.CONTENT_TYPE).toString();
if (contentType.equals("foo/bar")) {
return new String((byte[]) message.getPayload());
}
return null;
}
};
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class ConverterInjectingCTConfiguration {
@Bean
public Function<String, String> func() {
return x -> x;
}
@StreamMessageConverter
public MessageConverter customConverter() {
return new MessageConverter() {
@Override
public Message<?> toMessage(Object payload, MessageHeaders headers) {
return MessageBuilder.withPayload(((String) payload).getBytes())
.setHeader(MessageHeaders.CONTENT_TYPE, "ping/pong").build();
}
@Override
public Object fromMessage(Message<?> message, Class<?> targetClass) {
String contentType = message.getHeaders()
.get(MessageHeaders.CONTENT_TYPE).toString();
if (contentType.equals("foo/bar")) {
return new String((byte[]) message.getPayload());
}
return null;
}
};
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class ListPayloadNotBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class SimpleBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class NestedBatchConfiguration {
@Bean
public Function<List<List<Person>>, Person> func() {
return x -> x.get(0).get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
@EnableBinding(Processor.class)
public static class MessageBatchConfiguration {
@Bean
public Function<Message<List<Person>>, Person> func() {
return x -> x.getPayload().get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class MyFunctionsConfiguration {
@Bean
public Consumer<Flux<String>> fluxConsumer() {
return f -> f.subscribe(v -> {
System.out.println("Consuming flux: " + v);
testWithFluxedConsumerValue = v;
});
}
@Bean
public Function<Message<Foo>, Message<Bar>> messageToMessageDifferentType() {
return x -> MessageBuilder.withPayload(new Bar()).copyHeaders(x.getHeaders())
.build();
}
@Bean
public Function<Message<?>, Message<?>> messageToMessageAnyType() {
return x -> MessageBuilder.withPayload(new Bar()).copyHeaders(x.getHeaders())
.build();
}
@Bean
public Function<Message<?>, Message<?>> messageToMessageNoType() {
return x -> MessageBuilder.withPayload(new Bar()).copyHeaders(x.getHeaders())
.build();
}
@Bean
public Function<Message<Foo>, Message<Foo>> messageToMessageSameType() {
return x -> x;
}
@Bean
public Function<Foo, Foo> pojoToPojoSameType() {
return x -> x;
}
@Bean
public Function<Baz, Baz> pojoToPojoNonEmptyPojo() {
return x -> x;
}
@Bean
public Function<Foo, Foo> withException() {
return x -> {
if (x instanceof ErrorFoo) {
System.out.println("Throwing exception ");
throw new RuntimeException("Boom!");
}
else {
System.out.println("All is good ");
return x;
}
};
}
@Bean
public Function<Baz, Baz> withExceptionNativeEncodingEnabled() {
return x -> {
if (x instanceof ErrorBaz) {
System.out.println("Throwing exception ");
throw new RuntimeException("Boom!");
}
else {
System.out.println("All is good ");
return x;
}
};
}
}
private static class Foo {
}
private static class ErrorFoo extends Foo {
}
private static class Bar {
}
}