GH-1924 Add support for dynamic destinations to StramBridge
Resolves #1924
This commit is contained in:
@@ -661,6 +661,39 @@ Also, note that `streamBridge.send(..)` method takes an `Object` for data. This
|
||||
will go through the same routine when sending output as if it was from any Function or Supplier providing the same level
|
||||
of consistency as with functions. This means the output type conversion, partitioning etc are honored as if it was from the output produced by functions.
|
||||
|
||||
====== StreamBridge and Dynamic Destinations
|
||||
|
||||
`StreamBridge` can also be used for cases when output destination(s) are not known ahead of time similar to the use cases
|
||||
described in <<Routing FROM Consumer>> section.
|
||||
|
||||
Let's look at the example
|
||||
|
||||
[source, java]
|
||||
----
|
||||
@SpringBootApplication
|
||||
@Controller
|
||||
public class WebSourceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WebSourceApplication.class, args);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private StreamBridge streamBridge;
|
||||
|
||||
@RequestMapping
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
public void delegateToSupplier(@RequestBody String body) {
|
||||
System.out.println("Sending " + body);
|
||||
streamBridge.send("myDestiniation", body);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
As you can see the preceding example is very similar to the previous one with the exception of explicit binding instruction provided via
|
||||
`spring.cloud.stream.source` property (which is not provided).
|
||||
Here we're sending data to `myDestiniation` name which does not exist as a binding. Therefore such name will be treated as dynamic destination
|
||||
as described in <<Routing FROM Consumer>> section.
|
||||
|
||||
====== Using reactor API
|
||||
|
||||
@@ -1472,6 +1505,9 @@ public NewDestinationBindingCallback<RabbitProducerProperties> dynamicConfigurer
|
||||
|
||||
NOTE: If you need to support dynamic destinations with multiple binder types, use `Object` for the generic type and cast the `extended` argument as needed.
|
||||
|
||||
Also, please see <<Using StreamBridge>> section to see how yet another option (StreamBridge) can be utilized for similar cases.
|
||||
|
||||
|
||||
[[spring-cloud-stream-overview-error-handling]]
|
||||
=== Error Handling
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.FunctionProperties;
|
||||
@@ -122,7 +121,7 @@ public class FunctionConfiguration {
|
||||
private final static String SOURCE_PROPERY = "spring.cloud.stream.source";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(SOURCE_PROPERY)
|
||||
// @ConditionalOnProperty(SOURCE_PROPERY)
|
||||
public StreamBridge streamBridgeUtils(FunctionCatalog functionCatalog, FunctionRegistry functionRegistry,
|
||||
BindingServiceProperties bindingServiceProperties, ConfigurableApplicationContext applicationContext) {
|
||||
return new StreamBridge(functionCatalog, functionRegistry, bindingServiceProperties, applicationContext);
|
||||
@@ -188,7 +187,7 @@ public class FunctionConfiguration {
|
||||
String integrationFlowName = proxyFactory.getFunctionDefinition() + "_integrationflow";
|
||||
PollableBean pollable = extractPollableAnnotation(functionProperties, context, proxyFactory);
|
||||
|
||||
Type functionType = ((FunctionInvocationWrapper) functionWrapper).getFunctionType();
|
||||
Type functionType = functionWrapper.getFunctionType();
|
||||
IntegrationFlow integrationFlow = integrationFlowFromProvidedSupplier(new PartitionAwareFunctionWrapper(functionWrapper, context, producerProperties),
|
||||
beginPublishingTrigger, pollable, context, taskScheduler, functionType)
|
||||
.route(Message.class, message -> {
|
||||
|
||||
@@ -21,19 +21,24 @@ import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||
import org.springframework.cloud.function.context.FunctionType;
|
||||
import org.springframework.cloud.function.context.catalog.BeanFactoryAwareFunctionRegistry.FunctionInvocationWrapper;
|
||||
import org.springframework.cloud.stream.binder.ProducerProperties;
|
||||
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
|
||||
import org.springframework.cloud.stream.config.BindingProperties;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
@@ -55,7 +60,9 @@ import org.springframework.util.MimeTypeUtils;
|
||||
*/
|
||||
public final class StreamBridge implements SmartInitializingSingleton {
|
||||
|
||||
private final Map<String, DirectWithAttributesChannel> outputChannelsOnly = new HashMap<>();
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final Map<String, MessageChannel> outputChannelsOnly = new HashMap<>();
|
||||
|
||||
private final FunctionCatalog functionCatalog;
|
||||
|
||||
@@ -67,6 +74,9 @@ public final class StreamBridge implements SmartInitializingSingleton {
|
||||
|
||||
private boolean initialized;
|
||||
|
||||
@Autowired
|
||||
private BinderAwareChannelResolver dynamicDestinationResolver;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param functionCatalog instance of {@link FunctionCatalog}
|
||||
@@ -97,6 +107,11 @@ public final class StreamBridge implements SmartInitializingSingleton {
|
||||
* Sends 'data' to an output binding specified by 'bindingName' argument while
|
||||
* using the content type specified by the 'outputContentType' argument to deal
|
||||
* with output type conversion (if necessary).
|
||||
* For typical cases `bindingName` is configured using 'spring.cloud.stream.source' property.
|
||||
* However, this operation also supports sending to dynamic destinations. This means if the name
|
||||
* provided via 'bindingName' does not have a corresponding binding such name will be
|
||||
* treated as dynamic destination.
|
||||
*
|
||||
* @param bindingName the name of the output binding
|
||||
* @param data the data to send
|
||||
* @param outputContentType content type to be used to deal with output type conversion
|
||||
@@ -104,7 +119,15 @@ public final class StreamBridge implements SmartInitializingSingleton {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean send(String bindingName, Object data, MimeType outputContentType) {
|
||||
Assert.isTrue(this.outputChannelsOnly.containsKey(bindingName), "Binding name '" + bindingName + "' does not exist.");
|
||||
if (!this.outputChannelsOnly.containsKey(bindingName)) {
|
||||
logger.info("Binding name '" + bindingName + "' does not exist. This means that value '"
|
||||
+ bindingName + "' will be treated as dynamic destination. If this is not your intention please "
|
||||
+ "provide 'spring.cloud.stream.source' property");
|
||||
this.outputChannelsOnly.put(bindingName, dynamicDestinationResolver.resolveDestination(bindingName));
|
||||
FunctionRegistration<Function<Object, Object>> fr = new FunctionRegistration<>(v -> v, bindingName);
|
||||
this.functionRegistry.register(fr.type(FunctionType.from(Object.class).to(Object.class).message()));
|
||||
}
|
||||
|
||||
FunctionInvocationWrapper functionWrapper = this.functionCatalog.lookup(bindingName, outputContentType.toString());
|
||||
|
||||
BindingProperties bindingProperties = this.bindingServiceProperties.getBindings().get(bindingName);
|
||||
|
||||
@@ -92,7 +92,7 @@ public class StreamBridgeTests {
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
|
||||
Message message = outputDestination.receive(100, "foo-out-0");
|
||||
Message<?> message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(message.getPayload()).isEqualTo("hello foo".getBytes());
|
||||
assertThat(message.getHeaders().get("foo")).isEqualTo("foo");
|
||||
|
||||
@@ -146,6 +146,30 @@ public class StreamBridgeTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDynamicDestination() {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
|
||||
.getCompleteConfiguration(TestConfiguration.class))
|
||||
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
|
||||
|
||||
OutputDestination outputDestination = context.getBean(OutputDestination.class);
|
||||
|
||||
StreamBridge bridge = context.getBean(StreamBridge.class);
|
||||
bridge.send("foo-out-0", "b");
|
||||
bridge.send("bar", "hello");
|
||||
bridge.send("blah", MessageBuilder.withPayload("message").setHeader("foo", "foo").build());
|
||||
|
||||
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("b");
|
||||
|
||||
message = outputDestination.receive(100, "bar");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("hello");
|
||||
|
||||
message = outputDestination.receive(100, "blah");
|
||||
assertThat(new String(message.getPayload())).isEqualTo("message");
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class EmptyConfiguration {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user