Make spring.cloud.function.stream.endpoint optional for stream apps
If there is only one function you shouldn't have to set any configuration to get a stream app to run. This also implementation supports multiple functions, trying to guess which one to use based on the type of the incoming message payload. In principle that could be strategized as a simple router function (e.g. to look for a header with a function name). If there are functions and consumers in the same app, they will subscribe to the same input channel (and hence by default Spring Integration will load balance between them). This could also probably use some more features, to specify the desired behaviour. If user *does* supply spring.cloud.function.stream.endpoint then it is used and overrides all other possible routes.
This commit is contained in:
@@ -25,14 +25,15 @@ import java.util.function.Consumer;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ListableBeanFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||||
|
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.function.context.FunctionInspector;
|
import org.springframework.cloud.function.context.FunctionInspector;
|
||||||
import org.springframework.cloud.function.invoker.AbstractFunctionInvoker;
|
|
||||||
import org.springframework.cloud.function.registry.FunctionCatalog;
|
import org.springframework.cloud.function.registry.FunctionCatalog;
|
||||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||||
import org.springframework.cloud.stream.binder.Binder;
|
import org.springframework.cloud.stream.binder.Binder;
|
||||||
@@ -46,17 +47,13 @@ import org.springframework.context.annotation.Conditional;
|
|||||||
import org.springframework.context.annotation.ConfigurationCondition;
|
import org.springframework.context.annotation.ConfigurationCondition;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mark Fisher
|
* @author Mark Fisher
|
||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@EnableConfigurationProperties(StreamConfigurationProperties.class)
|
@EnableConfigurationProperties(StreamConfigurationProperties.class)
|
||||||
@ConditionalOnClass({ Binder.class, AbstractFunctionInvoker.class })
|
@ConditionalOnClass({ Binder.class })
|
||||||
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
public class StreamConfiguration {
|
public class StreamConfiguration {
|
||||||
|
|
||||||
@@ -64,16 +61,12 @@ public class StreamConfiguration {
|
|||||||
@EnableBinding(Source.class)
|
@EnableBinding(Source.class)
|
||||||
protected static class SupplierConfiguration {
|
protected static class SupplierConfiguration {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private StreamConfigurationProperties properties;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty("spring.cloud.stream.bindings.output.destination")
|
public SupplierInvokingMessageProducer<Object> supplierInvoker(
|
||||||
public SupplierInvokingMessageProducer<Object> invoker(FunctionCatalog registry) {
|
ListableBeanFactory beanFactory, FunctionCatalog registry) {
|
||||||
String name = properties.getEndpoint();
|
String[] names = beanFactory.getBeanNamesForType(Supplier.class, false,
|
||||||
long interval = properties.getInterval();
|
false);
|
||||||
Supplier<Flux<Object>> supplier = registry.lookupSupplier(name);
|
return new SupplierInvokingMessageProducer<Object>(registry, names);
|
||||||
return new SupplierInvokingMessageProducer<Object>(supplier, interval);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,14 +78,14 @@ public class StreamConfiguration {
|
|||||||
private StreamConfigurationProperties properties;
|
private StreamConfigurationProperties properties;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty("spring.cloud.stream.bindings.input.destination")
|
public StreamListeningFunctionInvoker functionInvoker(
|
||||||
public AbstractFunctionInvoker<?, ?> invoker(FunctionCatalog registry, FunctionInspector functionInspector,
|
ListableBeanFactory beanFactory, FunctionCatalog registry,
|
||||||
|
FunctionInspector functionInspector,
|
||||||
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
||||||
String name = properties.getEndpoint();
|
String[] names = beanFactory.getBeanNamesForType(Function.class, false,
|
||||||
Function<Flux<?>, Flux<?>> function = registry.lookupFunction(name);
|
false);
|
||||||
Assert.notNull(function, "no such function: " + name);
|
return new StreamListeningFunctionInvoker(registry, functionInspector,
|
||||||
return new StreamListeningFunctionInvoker(name, function, functionInspector,
|
compositeMessageConverterFactory, properties.getEndpoint(), names);
|
||||||
compositeMessageConverterFactory);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +97,14 @@ public class StreamConfiguration {
|
|||||||
private StreamConfigurationProperties properties;
|
private StreamConfigurationProperties properties;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty("spring.cloud.stream.bindings.input.destination")
|
public StreamListeningConsumerInvoker consumerInvoker(
|
||||||
public StreamListeningConsumerInvoker invoker(FunctionCatalog registry, FunctionInspector functionInspector,
|
ListableBeanFactory beanFactory, FunctionCatalog registry,
|
||||||
|
FunctionInspector functionInspector,
|
||||||
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
@Lazy CompositeMessageConverterFactory compositeMessageConverterFactory) {
|
||||||
String name = properties.getEndpoint();
|
String[] names = beanFactory.getBeanNamesForType(Consumer.class, false,
|
||||||
Consumer<Flux<?>> consumer = registry.lookupConsumer(name);
|
false);
|
||||||
return new StreamListeningConsumerInvoker(name, consumer, functionInspector,
|
return new StreamListeningConsumerInvoker(registry, functionInspector,
|
||||||
compositeMessageConverterFactory);
|
compositeMessageConverterFactory, properties.getEndpoint(), names);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,21 +139,24 @@ public class StreamConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||||
String functionName = context.getEnvironment().getProperty("spring.cloud.function.stream.endpoint");
|
AnnotatedTypeMetadata metadata) {
|
||||||
if (!StringUtils.hasText(functionName)) {
|
if (context.getBeanFactory().getBeanNamesForType(type, false,
|
||||||
return ConditionOutcome.noMatch("no endpoint function name available");
|
false).length > 0) {
|
||||||
|
String endpoint = new RelaxedPropertyResolver(context.getEnvironment(),
|
||||||
|
"spring.cloud.function.stream.").getProperty("endpoint");
|
||||||
|
if (endpoint != null && !type
|
||||||
|
.isAssignableFrom(context.getBeanFactory().getType(endpoint))) {
|
||||||
|
return ConditionOutcome.noMatch(String.format(
|
||||||
|
"explicit endpoint of type other than %s detected", type));
|
||||||
|
}
|
||||||
|
return ConditionOutcome
|
||||||
|
.match(String.format("bean of type %s detected", type));
|
||||||
|
|
||||||
}
|
}
|
||||||
if (functionName.indexOf(',') != -1) {
|
return ConditionOutcome
|
||||||
// for now we will just check the first, but later may support:
|
.noMatch(String.format("no bean of type %s detected", type));
|
||||||
// supplier[,function]+ or [function,]+consumer
|
|
||||||
functionName = functionName.substring(0, functionName.indexOf(','));
|
|
||||||
}
|
|
||||||
Class<?> beanType = context.getBeanFactory().getType(functionName);
|
|
||||||
if (type.isAssignableFrom(beanType)) {
|
|
||||||
return ConditionOutcome.match(String.format("bean '%s' is a %s", functionName, type));
|
|
||||||
}
|
|
||||||
return ConditionOutcome.noMatch(String.format("bean '%s' is not a %s", functionName, type));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -16,64 +16,88 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.function.stream;
|
package org.springframework.cloud.function.stream;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||||
import org.springframework.cloud.function.context.FunctionInspector;
|
import org.springframework.cloud.function.context.FunctionInspector;
|
||||||
|
import org.springframework.cloud.function.registry.FunctionCatalog;
|
||||||
import org.springframework.cloud.stream.annotation.Input;
|
import org.springframework.cloud.stream.annotation.Input;
|
||||||
import org.springframework.cloud.stream.annotation.StreamListener;
|
import org.springframework.cloud.stream.annotation.StreamListener;
|
||||||
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
|
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
|
||||||
import org.springframework.cloud.stream.messaging.Processor;
|
import org.springframework.cloud.stream.messaging.Sink;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.converter.MessageConverter;
|
import org.springframework.messaging.converter.MessageConverter;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mark Fisher
|
* @author Mark Fisher
|
||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
public class StreamListeningConsumerInvoker implements SmartInitializingSingleton {
|
public class StreamListeningConsumerInvoker implements SmartInitializingSingleton {
|
||||||
|
|
||||||
private final Consumer<Flux<?>> consumer;
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
private final FunctionInspector functionInspector;
|
private final FunctionInspector functionInspector;
|
||||||
|
|
||||||
private final CompositeMessageConverterFactory converterFactory;
|
private final CompositeMessageConverterFactory converterFactory;
|
||||||
|
|
||||||
private MessageConverter converter;
|
private MessageConverter converter;
|
||||||
|
|
||||||
private Class<?> inputType;
|
private final FunctionCatalog functionCatalog;
|
||||||
|
|
||||||
public StreamListeningConsumerInvoker(String name, Consumer<Flux<?>> consumer, FunctionInspector functionInspector,
|
private final String defaultEndpoint;
|
||||||
CompositeMessageConverterFactory converterFactory) {
|
|
||||||
this.consumer = consumer;
|
private final String[] names;
|
||||||
this.name = name;
|
|
||||||
|
public StreamListeningConsumerInvoker(FunctionCatalog functionCatalog,
|
||||||
|
FunctionInspector functionInspector,
|
||||||
|
CompositeMessageConverterFactory converterFactory, String defaultEndpoint,
|
||||||
|
String... names) {
|
||||||
|
this.functionCatalog = functionCatalog;
|
||||||
this.functionInspector = functionInspector;
|
this.functionInspector = functionInspector;
|
||||||
this.converterFactory = converterFactory;
|
this.converterFactory = converterFactory;
|
||||||
|
this.defaultEndpoint = defaultEndpoint;
|
||||||
|
this.names = names;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterSingletonsInstantiated() {
|
public void afterSingletonsInstantiated() {
|
||||||
this.converter = this.converterFactory.getMessageConverterForAllRegistered();
|
this.converter = this.converterFactory.getMessageConverterForAllRegistered();
|
||||||
this.inputType = this.functionInspector.getInputType(this.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@StreamListener
|
@StreamListener
|
||||||
public void handle(@Input(Processor.INPUT) Flux<Message<?>> input) {
|
public void handle(@Input(Sink.INPUT) Flux<Message<?>> input) {
|
||||||
this.consumer.accept(input.map(convertInput()));
|
input.groupBy(this::select)
|
||||||
|
.filter(group -> functionCatalog.lookupConsumer(group.key()) != null)
|
||||||
|
.subscribe(group -> process(group.key(), group));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Message<?>, Object> convertInput() {
|
private void process(String name, Flux<Message<?>> flux) {
|
||||||
|
functionCatalog.lookupConsumer(name)
|
||||||
|
.accept(flux.map(message -> convertInput(name).apply(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String select(Message<?> input) {
|
||||||
|
String name = defaultEndpoint;
|
||||||
|
if (name == null) {
|
||||||
|
for (String candidate : names) {
|
||||||
|
Class<?> inputType = functionInspector.getInputType(candidate);
|
||||||
|
if (this.converter.fromMessage(input, inputType) != null) {
|
||||||
|
name = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Message<?>, Object> convertInput(String name) {
|
||||||
|
Class<?> inputType = functionInspector.getInputType(name);
|
||||||
return m -> {
|
return m -> {
|
||||||
if (this.inputType.isAssignableFrom(m.getPayload().getClass())) {
|
if (inputType.isAssignableFrom(m.getPayload().getClass())) {
|
||||||
return m.getPayload();
|
return m.getPayload();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return converter.fromMessage(m, this.inputType);
|
return this.converter.fromMessage(m, inputType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ package org.springframework.cloud.function.stream;
|
|||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||||
import org.springframework.cloud.function.context.FunctionInspector;
|
import org.springframework.cloud.function.context.FunctionInspector;
|
||||||
import org.springframework.cloud.function.invoker.AbstractFunctionInvoker;
|
import org.springframework.cloud.function.registry.FunctionCatalog;
|
||||||
import org.springframework.cloud.stream.annotation.Input;
|
import org.springframework.cloud.stream.annotation.Input;
|
||||||
import org.springframework.cloud.stream.annotation.Output;
|
import org.springframework.cloud.stream.annotation.Output;
|
||||||
import org.springframework.cloud.stream.annotation.StreamListener;
|
import org.springframework.cloud.stream.annotation.StreamListener;
|
||||||
@@ -30,51 +28,79 @@ import org.springframework.cloud.stream.converter.CompositeMessageConverterFacto
|
|||||||
import org.springframework.cloud.stream.messaging.Processor;
|
import org.springframework.cloud.stream.messaging.Processor;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.converter.MessageConverter;
|
import org.springframework.messaging.converter.MessageConverter;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mark Fisher
|
* @author Mark Fisher
|
||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
public class StreamListeningFunctionInvoker extends AbstractFunctionInvoker<Flux<?>, Flux<?>>
|
public class StreamListeningFunctionInvoker implements SmartInitializingSingleton {
|
||||||
implements SmartInitializingSingleton {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
private final FunctionInspector functionInspector;
|
private final FunctionInspector functionInspector;
|
||||||
|
|
||||||
|
private final FunctionCatalog functionCatalog;
|
||||||
|
|
||||||
private final CompositeMessageConverterFactory converterFactory;
|
private final CompositeMessageConverterFactory converterFactory;
|
||||||
|
|
||||||
private MessageConverter converter;
|
private MessageConverter converter;
|
||||||
|
|
||||||
private Class<?> inputType;
|
private final String defaultEndpoint;
|
||||||
|
|
||||||
public StreamListeningFunctionInvoker(String name, Function<Flux<?>, Flux<?>> function, FunctionInspector functionInspector,
|
private final String[] names;
|
||||||
CompositeMessageConverterFactory converterFactory) {
|
|
||||||
super(function);
|
public StreamListeningFunctionInvoker(FunctionCatalog functionCatalog,
|
||||||
this.name = name;
|
FunctionInspector functionInspector,
|
||||||
|
CompositeMessageConverterFactory converterFactory, String defaultEndpoint,
|
||||||
|
String... names) {
|
||||||
|
this.functionCatalog = functionCatalog;
|
||||||
this.functionInspector = functionInspector;
|
this.functionInspector = functionInspector;
|
||||||
this.converterFactory = converterFactory;
|
this.converterFactory = converterFactory;
|
||||||
|
this.defaultEndpoint = defaultEndpoint;
|
||||||
|
this.names = names;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterSingletonsInstantiated() {
|
public void afterSingletonsInstantiated() {
|
||||||
this.converter = this.converterFactory.getMessageConverterForAllRegistered();
|
this.converter = this.converterFactory.getMessageConverterForAllRegistered();
|
||||||
this.inputType = this.functionInspector.getInputType(this.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@StreamListener
|
@StreamListener
|
||||||
@Output(Processor.OUTPUT)
|
@Output(Processor.OUTPUT)
|
||||||
public Flux<?> handle(@Input(Processor.INPUT) Flux<Message<?>> input) {
|
public Flux<?> handle(@Input(Processor.INPUT) Flux<Message<?>> input) {
|
||||||
return this.doInvoke(input.map(convertInput()));
|
return input.groupBy(this::select)
|
||||||
|
.filter(group -> functionCatalog.lookupFunction(group.key()) != null)
|
||||||
|
.flatMap(group -> process(group.key(), group));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Message<?>, Object> convertInput() {
|
private Flux<?> process(String name, Flux<Message<?>> flux) {
|
||||||
|
return (Flux<?>) functionCatalog.lookupFunction(name)
|
||||||
|
.apply(flux.map(message -> convertInput(name).apply(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String select(Message<?> input) {
|
||||||
|
String name = defaultEndpoint;
|
||||||
|
if (name == null) {
|
||||||
|
for (String candidate : names) {
|
||||||
|
Class<?> inputType = functionInspector.getInputType(candidate);
|
||||||
|
if (this.converter.fromMessage(input, inputType) != null) {
|
||||||
|
name = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Message<?>, Object> convertInput(String name) {
|
||||||
|
Class<?> inputType = functionInspector.getInputType(name);
|
||||||
return m -> {
|
return m -> {
|
||||||
if (this.inputType.isAssignableFrom(m.getPayload().getClass())) {
|
if (inputType.isAssignableFrom(m.getPayload().getClass())) {
|
||||||
return m.getPayload();
|
return m.getPayload();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return this.converter.fromMessage(m, this.inputType);
|
return this.converter.fromMessage(m, inputType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,9 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.function.stream;
|
package org.springframework.cloud.function.stream;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.cloud.function.support.FluxSupplier;
|
import org.springframework.cloud.function.registry.FunctionCatalog;
|
||||||
import org.springframework.cloud.function.support.FunctionUtils;
|
|
||||||
import org.springframework.cloud.stream.messaging.Source;
|
import org.springframework.cloud.stream.messaging.Source;
|
||||||
import org.springframework.integration.endpoint.MessageProducerSupport;
|
import org.springframework.integration.endpoint.MessageProducerSupport;
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
@@ -33,24 +31,30 @@ import reactor.core.publisher.Flux;
|
|||||||
*/
|
*/
|
||||||
public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
|
public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
|
||||||
|
|
||||||
private final Supplier<Flux<T>> supplier;
|
private final FunctionCatalog functionCatalog;
|
||||||
|
|
||||||
public SupplierInvokingMessageProducer(Supplier<?> supplier, long interval) {
|
private final String[] names;
|
||||||
Assert.notNull(supplier, "Supplier must not be null");
|
|
||||||
if (!FunctionUtils.isFluxSupplier(supplier)) {
|
public SupplierInvokingMessageProducer(FunctionCatalog registry, String... names) {
|
||||||
supplier = (interval > 0)
|
this.functionCatalog = registry;
|
||||||
? new FluxSupplier<>(supplier, Duration.ofMillis(interval))
|
this.names = names;
|
||||||
: new FluxSupplier<>(supplier);
|
|
||||||
}
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Supplier<Flux<T>> unchecked = (Supplier<Flux<T>>) supplier;
|
|
||||||
this.supplier = unchecked;
|
|
||||||
this.setOutputChannelName(Source.OUTPUT);
|
this.setOutputChannelName(Source.OUTPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doStart() {
|
protected void doStart() {
|
||||||
this.supplier.get()
|
supplier()
|
||||||
.subscribe(m -> this.sendMessage(MessageBuilder.withPayload(m).build()));
|
.subscribe(m -> this.sendMessage(MessageBuilder.withPayload(m).build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Flux<?> supplier() {
|
||||||
|
Supplier<Flux<?>> supplier = null;
|
||||||
|
Flux<?> result = Flux.empty();
|
||||||
|
for (String name : names) {
|
||||||
|
supplier = functionCatalog.lookupSupplier(name);
|
||||||
|
Assert.notNull(supplier, "Supplier must not be null");
|
||||||
|
result = Flux.merge(result, supplier.get());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ import reactor.core.publisher.Flux;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = FluxPojoStreamingConsumerTests.StreamingSinkTest.class, properties = {
|
@SpringBootTest(classes = FluxPojoStreamingConsumerTests.StreamingSinkTest.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.function.stream.endpoint=sinkConsumer" })
|
|
||||||
public class FluxPojoStreamingConsumerTests {
|
public class FluxPojoStreamingConsumerTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ import reactor.core.publisher.Flux;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = FluxStreamingConsumerTests.StreamingSinkTest.class, properties = {
|
@SpringBootTest(classes = FluxStreamingConsumerTests.StreamingSinkTest.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.function.stream.endpoint=sinkConsumer" })
|
|
||||||
public class FluxStreamingConsumerTests {
|
public class FluxStreamingConsumerTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = PojoStreamingConsumerTests.StreamingSinkTest.class, properties = {
|
@SpringBootTest(classes = PojoStreamingConsumerTests.StreamingSinkTest.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.function.stream.endpoint=sinkConsumer" })
|
|
||||||
public class PojoStreamingConsumerTests {
|
public class PojoStreamingConsumerTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = StreamingConsumerTests.StreamingSinkTest.class, properties = {
|
@SpringBootTest(classes = StreamingConsumerTests.StreamingSinkTest.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.function.stream.endpoint=sinkConsumer" })
|
|
||||||
public class StreamingConsumerTests {
|
public class StreamingConsumerTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = FluxPojoStreamingFunctionTests.StreamingFunctionApplication.class, properties = {
|
@SpringBootTest(classes = FluxPojoStreamingFunctionTests.StreamingFunctionApplication.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.stream.bindings.output.destination=data-out", "spring.cloud.function.stream.endpoint=uppercase" })
|
|
||||||
public class FluxPojoStreamingFunctionTests {
|
public class FluxPojoStreamingFunctionTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = FluxStreamingFunctionTests.StreamingFunctionApplication.class, properties = {
|
@SpringBootTest(classes = FluxStreamingFunctionTests.StreamingFunctionApplication.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.stream.bindings.output.destination=data-out",
|
|
||||||
"spring.cloud.function.stream.endpoint=uppercase" })
|
|
||||||
public class FluxStreamingFunctionTests {
|
public class FluxStreamingFunctionTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = PojoStreamingFunctionTests.StreamingFunctionApplication.class, properties = {
|
@SpringBootTest(classes = PojoStreamingFunctionTests.StreamingFunctionApplication.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.stream.bindings.output.destination=data-out", "spring.cloud.function.stream.endpoint=uppercase" })
|
|
||||||
public class PojoStreamingFunctionTests {
|
public class PojoStreamingFunctionTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = StreamingFunctionTests.StreamingFunctionApplication.class, properties = {
|
@SpringBootTest(classes = StreamingFunctionTests.StreamingFunctionApplication.class)
|
||||||
"spring.cloud.stream.bindings.input.destination=data-in",
|
|
||||||
"spring.cloud.stream.bindings.output.destination=data-out",
|
|
||||||
"spring.cloud.function.stream.endpoint=uppercase" })
|
|
||||||
public class StreamingFunctionTests {
|
public class StreamingFunctionTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.function.stream.mixed;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.cloud.stream.messaging.Processor;
|
||||||
|
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Marius Bogoevici
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(classes = PojoStreamingExplicitEndpointTests.StreamingFunctionApplication.class, properties = {
|
||||||
|
"spring.cloud.function.stream.endpoint=uppercase",
|
||||||
|
"logging.level.org.springframework.integration=DEBUG", "debug=TRUE" })
|
||||||
|
public class PojoStreamingExplicitEndpointTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Processor processor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MessageCollector messageCollector;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test() throws Exception {
|
||||||
|
processor.input()
|
||||||
|
.send(MessageBuilder.withPayload("{\"name\":\"hello\"}").build());
|
||||||
|
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
assertThat(result.getPayload()).isInstanceOf(Foo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public static class StreamingFunctionApplication {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Foo, Foo> uppercase() {
|
||||||
|
return f -> new Foo(f.getName().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Supplier<Foo> foos() {
|
||||||
|
return () -> new Foo("world");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class Foo {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
Foo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Foo(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.function.stream.mixed;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.cloud.stream.messaging.Processor;
|
||||||
|
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Marius Bogoevici
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(classes = PojoStreamingMixedTests.StreamingFunctionApplication.class)
|
||||||
|
public class PojoStreamingMixedTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Processor processor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MessageCollector messageCollector;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
List<Bar> collector;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test() throws Exception {
|
||||||
|
processor.input()
|
||||||
|
.send(MessageBuilder.withPayload("{\"name\":\"hello\"}").build());
|
||||||
|
processor.input()
|
||||||
|
.send(MessageBuilder.withPayload("{\"name\":\"world\"}").build());
|
||||||
|
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
assertThat(result.getPayload()).isInstanceOf(Foo.class);
|
||||||
|
// 2 subscribers to the same channel so input messages are applied as round robin
|
||||||
|
assertThat(collector).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public static class StreamingFunctionApplication {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Foo, Foo> uppercase() {
|
||||||
|
return f -> new Foo(f.getName().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public List<Bar> collector() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Consumer<Bar> sink(final List<Bar> list) {
|
||||||
|
return s -> list.add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class Foo {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
Foo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Foo(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class Bar {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
Bar() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bar(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* @author Marius Bogoevici
|
* @author Marius Bogoevici
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(classes = StreamSupplierTests.StreamingFunctionApplication.class, properties = {
|
@SpringBootTest(classes = StreamSupplierTests.StreamingFunctionApplication.class)
|
||||||
"spring.cloud.stream.bindings.output.destination=data-out",
|
|
||||||
"spring.cloud.function.stream.endpoint=simpleSupplier" })
|
|
||||||
public class StreamSupplierTests {
|
public class StreamSupplierTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
Reference in New Issue
Block a user