GH-3192: pub-sub DSL for broker-backed channels (#3193)

* GH-3192: pub-sub DSL for broker-backed channels

Fixes https://github.com/spring-projects/spring-integration/issues/3192

* Introduce a `BroadcastCapableChannel` abstract to indicate those `SubscribableChannel`
implementations which can provide a pub-sub functionality
* Implement a `BroadcastCapableChannel` in broker-baked channels with pub-sub option
* Introduce a `BaseIntegrationFlowDefinition.publishSubscribeChannel()` based
on the `BroadcastCapableChannel` and `BroadcastPublishSubscribeSpec` to let to
configure sub-flow subscribers in fluent manner

* * Add some JavaDocs and document new feature

* * Show the channel bean definition in the doc
* Fix typo
This commit is contained in:
Artem Bilan
2020-02-25 13:01:38 -05:00
committed by GitHub
parent 7dbdbdee3f
commit 87c8e47a88
11 changed files with 265 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,22 +25,25 @@ import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer;
import org.springframework.integration.amqp.support.AmqpHeaderMapper; import org.springframework.integration.amqp.support.AmqpHeaderMapper;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.dispatcher.AbstractDispatcher; import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.integration.dispatcher.BroadcastingDispatcher; import org.springframework.integration.dispatcher.BroadcastingDispatcher;
/** /**
* The {@link AbstractSubscribableAmqpChannel} extension for pub-sub semantics based on the {@link FanoutExchange}.
*
* @author Mark Fisher * @author Mark Fisher
* @author Gary Russell * @author Gary Russell
* @author Artem Bilan * @author Artem Bilan
* *
* @since 2.1 * @since 2.1
*/ */
public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel { public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel implements BroadcastCapableChannel {
private volatile FanoutExchange exchange;
private final Queue queue = new AnonymousQueue(); private final Queue queue = new AnonymousQueue();
private volatile FanoutExchange exchange;
private volatile Binding binding; private volatile Binding binding;
/** /**
@@ -53,6 +56,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
*/ */
public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container, public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container,
AmqpTemplate amqpTemplate) { AmqpTemplate amqpTemplate) {
super(channelName, container, amqpTemplate, true); super(channelName, container, amqpTemplate, true);
} }
@@ -69,6 +73,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
*/ */
public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container, public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container,
AmqpTemplate amqpTemplate, AmqpHeaderMapper outboundMapper, AmqpHeaderMapper inboundMapper) { AmqpTemplate amqpTemplate, AmqpHeaderMapper outboundMapper, AmqpHeaderMapper inboundMapper) {
super(channelName, container, amqpTemplate, true, outboundMapper, inboundMapper); super(channelName, container, amqpTemplate, true, outboundMapper, inboundMapper);
} }
@@ -104,7 +109,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
@Override @Override
protected AbstractDispatcher createDispatcher() { protected AbstractDispatcher createDispatcher() {
BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true); BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true);
broadcastingDispatcher.setBeanFactory(this.getBeanFactory()); broadcastingDispatcher.setBeanFactory(getBeanFactory());
return broadcastingDispatcher; return broadcastingDispatcher;
} }

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2020 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.integration.channel;
import org.springframework.messaging.SubscribableChannel;
/**
* A {@link SubscribableChannel} variant for implementations with broadcasting capabilities.
*
* @author Artem Bilan
*
* @since 5.3
*/
public interface BroadcastCapableChannel extends SubscribableChannel {
/**
* Return a state of this channel in regards of broadcasting capabilities.
* @return the state of this channel in regards of broadcasting capabilities.
*/
default boolean isBroadcast() {
return true;
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.util.ErrorHandler;
* @author Gary Russell * @author Gary Russell
* @author Artem Bilan * @author Artem Bilan
*/ */
public class PublishSubscribeChannel extends AbstractExecutorChannel { public class PublishSubscribeChannel extends AbstractExecutorChannel implements BroadcastCapableChannel {
private ErrorHandler errorHandler; private ErrorHandler errorHandler;
@@ -45,6 +45,14 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel {
private int minSubscribers; private int minSubscribers;
/**
* Create a PublishSubscribeChannel that will invoke the handlers in the
* message sender's thread.
*/
public PublishSubscribeChannel() {
this(null);
}
/** /**
* Create a PublishSubscribeChannel that will use an {@link Executor} * Create a PublishSubscribeChannel that will use an {@link Executor}
* to invoke the handlers. If this is null, each invocation will occur in * to invoke the handlers. If this is null, each invocation will occur in
@@ -56,14 +64,6 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel {
this.dispatcher = new BroadcastingDispatcher(executor); this.dispatcher = new BroadcastingDispatcher(executor);
} }
/**
* Create a PublishSubscribeChannel that will invoke the handlers in the
* message sender's thread.
*/
public PublishSubscribeChannel() {
this(null);
}
@Override @Override
public String getComponentType() { public String getComponentType() {
@@ -135,7 +135,7 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel {
@Override @Override
public final void onInit() { public final void onInit() {
super.onInit(); super.onInit();
BeanFactory beanFactory = this.getBeanFactory(); BeanFactory beanFactory = getBeanFactory();
BroadcastingDispatcher dispatcherToUse = getDispatcher(); BroadcastingDispatcher dispatcherToUse = getDispatcher();
if (this.executor != null) { if (this.executor != null) {
Assert.state(dispatcherToUse.getHandlerCount() == 0, Assert.state(dispatcherToUse.getHandlerCount() == 0,

View File

@@ -35,6 +35,7 @@ import org.springframework.beans.factory.config.DestructionAwareBeanPostProcesso
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.aggregator.AggregatingMessageHandler;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel; import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.FluxMessageChannel; import org.springframework.integration.channel.FluxMessageChannel;
@@ -294,6 +295,25 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
return addComponents(spec.getComponentsToRegister()).channel(spec); return addComponents(spec.getComponentsToRegister()).channel(spec);
} }
/**
* The {@link BroadcastCapableChannel} {@link #channel}
* method specific implementation to allow the use of the 'subflow' subscriber capability.
* @param broadcastCapableChannel the {@link BroadcastCapableChannel} to subscriber sub-flows to.
* @param publishSubscribeChannelConfigurer the {@link Consumer} to specify
* {@link BroadcastPublishSubscribeSpec} 'subflow' definitions.
* @return the current {@link BaseIntegrationFlowDefinition}.
* @since 5.3
*/
public B publishSubscribeChannel(BroadcastCapableChannel broadcastCapableChannel,
Consumer<BroadcastPublishSubscribeSpec> publishSubscribeChannelConfigurer) {
Assert.notNull(publishSubscribeChannelConfigurer, "'publishSubscribeChannelConfigurer' must not be null");
BroadcastPublishSubscribeSpec spec = new BroadcastPublishSubscribeSpec(broadcastCapableChannel);
publishSubscribeChannelConfigurer.accept(spec);
return addComponents(spec.getComponentsToRegister())
.channel(broadcastCapableChannel);
}
/** /**
* Populate the {@code Wire Tap} EI Pattern specific * Populate the {@code Wire Tap} EI Pattern specific
* {@link org.springframework.messaging.support.ChannelInterceptor} implementation * {@link org.springframework.messaging.support.ChannelInterceptor} implementation

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020 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.integration.dsl;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.messaging.MessageChannel;
import org.springframework.util.Assert;
/**
* An {@link IntegrationComponentSpec} for configuring sub-flow subscribers on the
* provided {@link BroadcastCapableChannel}.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 5.3
*/
public class BroadcastPublishSubscribeSpec
extends IntegrationComponentSpec<BroadcastPublishSubscribeSpec, BroadcastCapableChannel>
implements ComponentsRegistration {
private final Map<Object, String> subscriberFlows = new LinkedHashMap<>();
private int order;
protected BroadcastPublishSubscribeSpec(BroadcastCapableChannel channel) {
Assert.state(channel.isBroadcast(),
() -> "the " + channel +
" must be in the 'broadcast' state for using from this 'BroadcastPublishSubscribeSpec'");
this.target = channel;
}
/**
* Configure a {@link IntegrationFlow} to configure as a subscriber
* for the current {@link BroadcastCapableChannel}.
* @param subFlow the {@link IntegrationFlow} to configure as a subscriber
* for the current {@link BroadcastCapableChannel}.
* @return the current spec
*/
public BroadcastPublishSubscribeSpec subscribe(IntegrationFlow subFlow) {
Assert.notNull(subFlow, "'subFlow' must not be null");
IntegrationFlowBuilder flowBuilder =
IntegrationFlows.from(this.target)
.bridge(consumer -> consumer.order(this.order++));
MessageChannel subFlowInput = subFlow.getInputChannel();
if (subFlowInput == null) {
subFlow.configure(flowBuilder);
}
else {
flowBuilder.channel(subFlowInput);
}
this.subscriberFlows.put(flowBuilder.get(), null);
return _this();
}
@Override
public Map<Object, String> getComponentsToRegister() {
return this.subscriberFlows;
}
}

View File

@@ -21,10 +21,11 @@ import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.messaging.MessageChannel;
import org.springframework.util.Assert;
/** /**
* The {@link PublishSubscribeChannelSpec} extension to configure as a general flow callback for sub-flows
* as subscribers.
*
* @author Artem Bilan * @author Artem Bilan
* @author Gary Russell * @author Gary Russell
* *
@@ -32,15 +33,15 @@ import org.springframework.util.Assert;
*/ */
public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSubscribeSpec> { public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSubscribeSpec> {
private final Map<Object, String> subscriberFlows = new LinkedHashMap<>(); private final BroadcastPublishSubscribeSpec delegate;
private int order;
protected PublishSubscribeSpec() { protected PublishSubscribeSpec() {
this.delegate = new BroadcastPublishSubscribeSpec(this.channel);
} }
protected PublishSubscribeSpec(@Nullable Executor executor) { protected PublishSubscribeSpec(@Nullable Executor executor) {
super(executor); super(executor);
this.delegate = new BroadcastPublishSubscribeSpec(this.channel);
} }
@Override @Override
@@ -49,21 +50,7 @@ public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSub
} }
public PublishSubscribeSpec subscribe(IntegrationFlow subFlow) { public PublishSubscribeSpec subscribe(IntegrationFlow subFlow) {
Assert.notNull(subFlow, "'subFlow' must not be null"); this.delegate.subscribe(subFlow);
IntegrationFlowBuilder flowBuilder =
IntegrationFlows.from(this.channel)
.bridge(consumer -> consumer.order(this.order++));
MessageChannel subFlowInput = subFlow.getInputChannel();
if (subFlowInput == null) {
subFlow.configure(flowBuilder);
}
else {
flowBuilder.channel(subFlowInput);
}
this.subscriberFlows.put(flowBuilder.get(), null);
return _this(); return _this();
} }
@@ -71,7 +58,7 @@ public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSub
public Map<Object, String> getComponentsToRegister() { public Map<Object, String> getComponentsToRegister() {
Map<Object, String> objects = new LinkedHashMap<>(); Map<Object, String> objects = new LinkedHashMap<>();
objects.putAll(super.getComponentsToRegister()); objects.putAll(super.getComponentsToRegister());
objects.putAll(this.subscriberFlows); objects.putAll(this.delegate.getComponentsToRegister());
return objects; return objects;
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.SmartLifecycle; import org.springframework.context.SmartLifecycle;
import org.springframework.integration.MessageDispatchingException; import org.springframework.integration.MessageDispatchingException;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.context.IntegrationProperties; import org.springframework.integration.context.IntegrationProperties;
import org.springframework.integration.dispatcher.AbstractDispatcher; import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.integration.dispatcher.BroadcastingDispatcher; import org.springframework.integration.dispatcher.BroadcastingDispatcher;
@@ -37,10 +38,13 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException; import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* An {@link AbstractJmsChannel} implementation for message-driven subscriptions.
* Also implements a {@link BroadcastCapableChannel} to represent possible pub-sub semantics
* when configured against JMS topic.
*
* @author Mark Fisher * @author Mark Fisher
* @author Gary Russell * @author Gary Russell
* @author Artem Bilan * @author Artem Bilan
@@ -48,7 +52,7 @@ import org.springframework.util.Assert;
* @since 2.0 * @since 2.0
*/ */
public class SubscribableJmsChannel extends AbstractJmsChannel public class SubscribableJmsChannel extends AbstractJmsChannel
implements SubscribableChannel, SmartLifecycle { implements BroadcastCapableChannel, SmartLifecycle {
private final AbstractMessageListenerContainer container; private final AbstractMessageListenerContainer container;
@@ -87,17 +91,22 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
return this.dispatcher.removeHandler(handler); return this.dispatcher.removeHandler(handler);
} }
@Override
public boolean isBroadcast() {
return this.container.isPubSubDomain();
}
@Override @Override
public void onInit() { public void onInit() {
if (this.initialized) { if (this.initialized) {
return; return;
} }
super.onInit(); super.onInit();
boolean isPubSub = this.container.isPubSubDomain(); boolean isPubSub = isBroadcast();
this.configureDispatcher(isPubSub); configureDispatcher(isPubSub);
MessageListener listener = new DispatchingMessageListener( MessageListener listener =
this.getJmsTemplate(), this.dispatcher, new DispatchingMessageListener(getJmsTemplate(), this.dispatcher, this, isPubSub,
this, isPubSub, this.getMessageBuilderFactory()); getMessageBuilderFactory());
this.container.setMessageListener(listener); this.container.setMessageListener(listener);
if (!this.container.isActive()) { if (!this.container.isActive()) {
this.container.afterPropertiesSet(); this.container.afterPropertiesSet();
@@ -108,7 +117,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
private void configureDispatcher(boolean isPubSub) { private void configureDispatcher(boolean isPubSub) {
if (isPubSub) { if (isPubSub) {
BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true); BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true);
broadcastingDispatcher.setBeanFactory(this.getBeanFactory()); broadcastingDispatcher.setBeanFactory(getBeanFactory());
this.dispatcher = broadcastingDispatcher; this.dispatcher = broadcastingDispatcher;
} }
else { else {
@@ -118,8 +127,8 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
} }
if (this.maxSubscribers == null) { if (this.maxSubscribers == null) {
this.maxSubscribers = this.getIntegrationProperty(isPubSub ? this.maxSubscribers = this.getIntegrationProperty(isPubSub ?
IntegrationProperties.CHANNELS_MAX_BROADCAST_SUBSCRIBERS : IntegrationProperties.CHANNELS_MAX_BROADCAST_SUBSCRIBERS :
IntegrationProperties.CHANNELS_MAX_UNICAST_SUBSCRIBERS, IntegrationProperties.CHANNELS_MAX_UNICAST_SUBSCRIBERS,
Integer.class); Integer.class);
} }
this.dispatcher.setMaxSubscribers(this.maxSubscribers); this.dispatcher.setMaxSubscribers(this.maxSubscribers);
@@ -194,6 +203,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
DispatchingMessageListener(JmsTemplate jmsTemplate, DispatchingMessageListener(JmsTemplate jmsTemplate,
MessageDispatcher dispatcher, SubscribableJmsChannel channel, boolean isPubSub, MessageDispatcher dispatcher, SubscribableJmsChannel channel, boolean isPubSub,
MessageBuilderFactory messageBuilderFactory) { MessageBuilderFactory messageBuilderFactory) {
this.jmsTemplate = jmsTemplate; this.jmsTemplate = jmsTemplate;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.channel = channel; this.channel = channel;
@@ -212,8 +222,10 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
converted = converter.fromMessage(message); converted = converter.fromMessage(message);
} }
if (converted != null) { if (converted != null) {
messageToSend = (converted instanceof Message<?>) ? (Message<?>) converted messageToSend =
: this.messageBuilderFactory.withPayload(converted).build(); converted instanceof Message<?>
? (Message<?>) converted
: this.messageBuilderFactory.withPayload(converted).build();
this.dispatcher.dispatch(messageToSend); this.dispatcher.dispatch(messageToSend);
} }
else if (this.logger.isWarnEnabled()) { else if (this.logger.isWarnEnabled()) {
@@ -231,8 +243,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
} }
} }
else { else {
throw new MessageDeliveryException( throw new MessageDeliveryException(messageToSend, exceptionMessage, e);
messageToSend, exceptionMessage, e);
} }
} }
catch (Exception e) { catch (Exception e) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2016-2019 the original author or authors. * Copyright 2016-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.annotation.IntegrationComponentScan; import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.MessagingGateway; import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.Poller; import org.springframework.integration.annotation.Poller;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel; import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.channel.QueueChannel;
@@ -125,6 +126,9 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Autowired @Autowired
private PollableChannel jmsPubSubBridgeChannel; private PollableChannel jmsPubSubBridgeChannel;
@Autowired
private PollableChannel jmsPubSubBridgeChannel2;
@Autowired @Autowired
@Qualifier("jmsOutboundGateway.handler") @Qualifier("jmsOutboundGateway.handler")
private MessageHandler jmsOutboundGatewayHandler; private MessageHandler jmsOutboundGatewayHandler;
@@ -254,6 +258,11 @@ public class JmsTests extends ActiveMQMultiContextTests {
.isNotNull() .isNotNull()
.extracting(Message::getPayload) .extracting(Message::getPayload)
.isEqualTo("foo"); .isEqualTo("foo");
received = this.jmsPubSubBridgeChannel2.receive(5000);
assertThat(received)
.isNotNull()
.extracting(Message::getPayload)
.isEqualTo("foo");
} }
@Test @Test
@@ -351,10 +360,20 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Bean @Bean
public IntegrationFlow pubSubFlow() { public IntegrationFlow pubSubFlow() {
return IntegrationFlows return f -> f
.from(Jms.publishSubscribeChannel(jmsConnectionFactory()) .publishSubscribeChannel(jmsPublishSubscribeChannel(),
.destination("pubsub")) pubsub -> pubsub
.channel(c -> c.queue("jmsPubSubBridgeChannel")) .subscribe(subFlow -> subFlow
.channel(c -> c.queue("jmsPubSubBridgeChannel")))
.subscribe(subFlow -> subFlow
.channel(c -> c.queue("jmsPubSubBridgeChannel2"))));
}
@Bean
public BroadcastCapableChannel jmsPublishSubscribeChannel() {
// TODO reconsider target generic type for channel implementation to return from this kind of specs
return (BroadcastCapableChannel) Jms.publishSubscribeChannel(jmsConnectionFactory())
.destination("pubsub")
.get(); .get();
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.MessageDispatchingException; import org.springframework.integration.MessageDispatchingException;
import org.springframework.integration.channel.AbstractMessageChannel; import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.ChannelUtils; import org.springframework.integration.channel.ChannelUtils;
import org.springframework.integration.context.IntegrationProperties; import org.springframework.integration.context.IntegrationProperties;
import org.springframework.integration.dispatcher.BroadcastingDispatcher; import org.springframework.integration.dispatcher.BroadcastingDispatcher;
@@ -40,13 +41,15 @@ import org.springframework.integration.util.ErrorHandlingTaskExecutor;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler; import org.springframework.util.ErrorHandler;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* An {@link AbstractMessageChannel} implementation with {@link BroadcastCapableChannel}
* aspect to provide a pub-sub semantics to consume messages fgrom Redis topic.
*
* @author Oleg Zhurakousky * @author Oleg Zhurakousky
* @author Gary Russell * @author Gary Russell
* @author Artem Bilan * @author Artem Bilan
@@ -55,7 +58,7 @@ import org.springframework.util.StringUtils;
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public class SubscribableRedisChannel extends AbstractMessageChannel public class SubscribableRedisChannel extends AbstractMessageChannel
implements SubscribableChannel, SmartLifecycle { implements BroadcastCapableChannel, SmartLifecycle {
private final RedisMessageListenerContainer container = new RedisMessageListenerContainer(); private final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
@@ -67,10 +70,6 @@ public class SubscribableRedisChannel extends AbstractMessageChannel
private final BroadcastingDispatcher dispatcher = new BroadcastingDispatcher(true); private final BroadcastingDispatcher dispatcher = new BroadcastingDispatcher(true);
private volatile Integer maxSubscribers;
private volatile boolean initialized;
// defaults // defaults
private Executor taskExecutor = new SimpleAsyncTaskExecutor(); private Executor taskExecutor = new SimpleAsyncTaskExecutor();
@@ -78,6 +77,10 @@ public class SubscribableRedisChannel extends AbstractMessageChannel
private MessageConverter messageConverter = new SimpleMessageConverter(); private MessageConverter messageConverter = new SimpleMessageConverter();
private volatile Integer maxSubscribers;
private volatile boolean initialized;
public SubscribableRedisChannel(RedisConnectionFactory connectionFactory, String topicName) { public SubscribableRedisChannel(RedisConnectionFactory connectionFactory, String topicName) {
Assert.notNull(connectionFactory, "'connectionFactory' must not be null"); Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
Assert.hasText(topicName, "'topicName' must not be empty"); Assert.hasText(topicName, "'topicName' must not be empty");

View File

@@ -742,6 +742,40 @@ public IntegrationFlow subscribersFlow() {
You can achieve the same result with separate `IntegrationFlow` `@Bean` definitions, but we hope you find the sub-flow style of logic composition useful. You can achieve the same result with separate `IntegrationFlow` `@Bean` definitions, but we hope you find the sub-flow style of logic composition useful.
We find that it results in shorter (and so more readable) code. We find that it results in shorter (and so more readable) code.
Starting with version 5.3, a `BroadcastCapableChannel`-based `publishSubscribeChannel()` implementation is provided to configure sub-flow subscribers on broker-backed message channels.
For example we now can configure several subscribers as sub-flows on the `Jms.publishSubscribeChannel()`:
====
[source,java]
----
@Bean
public BroadcastCapableChannel jmsPublishSubscribeChannel() {
return Jms.publishSubscribeChannel(jmsConnectionFactory())
.destination("pubsub")
.get();
}
@Bean
public IntegrationFlow pubSubFlow() {
return f -> f
.publishSubscribeChannel(jmsPublishSubscribeChannel(),
pubsub -> pubsub
.subscribe(subFlow -> subFlow
.channel(c -> c.queue("jmsPubSubBridgeChannel1")))
.subscribe(subFlow -> subFlow
.channel(c -> c.queue("jmsPubSubBridgeChannel2"))));
}
@Bean
public BroadcastCapableChannel jmsPublishSubscribeChannel(ConnectionFactory jmsConnectionFactory) {
return (BroadcastCapableChannel) Jms.publishSubscribeChannel(jmsConnectionFactory)
.destination("pubsub")
.get();
}
----
====
A similar `publish-subscribe` sub-flow composition provides the `.routeToRecipients()` method. A similar `publish-subscribe` sub-flow composition provides the `.routeToRecipients()` method.
Another example is using `.discardFlow()` instead of `.discardChannel()` on the `.filter()` method. Another example is using `.discardFlow()` instead of `.discardChannel()` on the `.filter()` method.

View File

@@ -53,6 +53,10 @@ See <<./graph.adoc#integration-graph,Integration Graph>> for more information.
In the aggregator, when the `MessageGroupProcessor` returns a `Message`, the `MessageBuilder.popSequenceDetails()` is performed on the output message if the `sequenceDetails` matches the header in the first message of the group. In the aggregator, when the `MessageGroupProcessor` returns a `Message`, the `MessageBuilder.popSequenceDetails()` is performed on the output message if the `sequenceDetails` matches the header in the first message of the group.
See <<./aggregator.adoc#aggregator-api,Aggregator Programming Model>> for more information. See <<./aggregator.adoc#aggregator-api,Aggregator Programming Model>> for more information.
A new `publishSubscribeChannel()` operator, based on the `BroadcastCapableChannel` and `BroadcastPublishSubscribeSpec`, was added into Java DSL.
This fluent API has its advantage when we configure sub-flows as pub-sub subscribers for broker-backed channels like `SubscribableJmsChannel`, `SubscribableRedisChannel` etc.
See <<./dsl.adoc#java-dsl-subflows,Sub-flows support>> for more information.
[[x5.3-amqp]] [[x5.3-amqp]]
=== AMQP Changes === AMQP Changes