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");
* 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.rabbit.listener.AbstractMessageListenerContainer;
import org.springframework.integration.amqp.support.AmqpHeaderMapper;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.integration.dispatcher.BroadcastingDispatcher;
/**
* The {@link AbstractSubscribableAmqpChannel} extension for pub-sub semantics based on the {@link FanoutExchange}.
*
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.1
*/
public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel {
private volatile FanoutExchange exchange;
public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel implements BroadcastCapableChannel {
private final Queue queue = new AnonymousQueue();
private volatile FanoutExchange exchange;
private volatile Binding binding;
/**
@@ -53,6 +56,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
*/
public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container,
AmqpTemplate amqpTemplate) {
super(channelName, container, amqpTemplate, true);
}
@@ -69,6 +73,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
*/
public PublishSubscribeAmqpChannel(String channelName, AbstractMessageListenerContainer container,
AmqpTemplate amqpTemplate, AmqpHeaderMapper outboundMapper, AmqpHeaderMapper inboundMapper) {
super(channelName, container, amqpTemplate, true, outboundMapper, inboundMapper);
}
@@ -104,7 +109,7 @@ public class PublishSubscribeAmqpChannel extends AbstractSubscribableAmqpChannel
@Override
protected AbstractDispatcher createDispatcher() {
BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true);
broadcastingDispatcher.setBeanFactory(this.getBeanFactory());
broadcastingDispatcher.setBeanFactory(getBeanFactory());
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 Artem Bilan
*/
public class PublishSubscribeChannel extends AbstractExecutorChannel {
public class PublishSubscribeChannel extends AbstractExecutorChannel implements BroadcastCapableChannel {
private ErrorHandler errorHandler;
@@ -45,6 +45,14 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel {
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}
* 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);
}
/**
* Create a PublishSubscribeChannel that will invoke the handlers in the
* message sender's thread.
*/
public PublishSubscribeChannel() {
this(null);
}
@Override
public String getComponentType() {
@@ -135,7 +135,7 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel {
@Override
public final void onInit() {
super.onInit();
BeanFactory beanFactory = this.getBeanFactory();
BeanFactory beanFactory = getBeanFactory();
BroadcastingDispatcher dispatcherToUse = getDispatcher();
if (this.executor != null) {
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.spel.standard.SpelExpressionParser;
import org.springframework.integration.aggregator.AggregatingMessageHandler;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.FluxMessageChannel;
@@ -294,6 +295,25 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
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
* {@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 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 Gary Russell
*
@@ -32,15 +33,15 @@ import org.springframework.util.Assert;
*/
public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSubscribeSpec> {
private final Map<Object, String> subscriberFlows = new LinkedHashMap<>();
private int order;
private final BroadcastPublishSubscribeSpec delegate;
protected PublishSubscribeSpec() {
this.delegate = new BroadcastPublishSubscribeSpec(this.channel);
}
protected PublishSubscribeSpec(@Nullable Executor executor) {
super(executor);
this.delegate = new BroadcastPublishSubscribeSpec(this.channel);
}
@Override
@@ -49,21 +50,7 @@ public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSub
}
public PublishSubscribeSpec subscribe(IntegrationFlow subFlow) {
Assert.notNull(subFlow, "'subFlow' must not be null");
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);
this.delegate.subscribe(subFlow);
return _this();
}
@@ -71,7 +58,7 @@ public class PublishSubscribeSpec extends PublishSubscribeChannelSpec<PublishSub
public Map<Object, String> getComponentsToRegister() {
Map<Object, String> objects = new LinkedHashMap<>();
objects.putAll(super.getComponentsToRegister());
objects.putAll(this.subscriberFlows);
objects.putAll(this.delegate.getComponentsToRegister());
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");
* 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.integration.MessageDispatchingException;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.context.IntegrationProperties;
import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.integration.dispatcher.BroadcastingDispatcher;
@@ -37,10 +38,13 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
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 Gary Russell
* @author Artem Bilan
@@ -48,7 +52,7 @@ import org.springframework.util.Assert;
* @since 2.0
*/
public class SubscribableJmsChannel extends AbstractJmsChannel
implements SubscribableChannel, SmartLifecycle {
implements BroadcastCapableChannel, SmartLifecycle {
private final AbstractMessageListenerContainer container;
@@ -87,17 +91,22 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
return this.dispatcher.removeHandler(handler);
}
@Override
public boolean isBroadcast() {
return this.container.isPubSubDomain();
}
@Override
public void onInit() {
if (this.initialized) {
return;
}
super.onInit();
boolean isPubSub = this.container.isPubSubDomain();
this.configureDispatcher(isPubSub);
MessageListener listener = new DispatchingMessageListener(
this.getJmsTemplate(), this.dispatcher,
this, isPubSub, this.getMessageBuilderFactory());
boolean isPubSub = isBroadcast();
configureDispatcher(isPubSub);
MessageListener listener =
new DispatchingMessageListener(getJmsTemplate(), this.dispatcher, this, isPubSub,
getMessageBuilderFactory());
this.container.setMessageListener(listener);
if (!this.container.isActive()) {
this.container.afterPropertiesSet();
@@ -108,7 +117,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
private void configureDispatcher(boolean isPubSub) {
if (isPubSub) {
BroadcastingDispatcher broadcastingDispatcher = new BroadcastingDispatcher(true);
broadcastingDispatcher.setBeanFactory(this.getBeanFactory());
broadcastingDispatcher.setBeanFactory(getBeanFactory());
this.dispatcher = broadcastingDispatcher;
}
else {
@@ -118,8 +127,8 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
}
if (this.maxSubscribers == null) {
this.maxSubscribers = this.getIntegrationProperty(isPubSub ?
IntegrationProperties.CHANNELS_MAX_BROADCAST_SUBSCRIBERS :
IntegrationProperties.CHANNELS_MAX_UNICAST_SUBSCRIBERS,
IntegrationProperties.CHANNELS_MAX_BROADCAST_SUBSCRIBERS :
IntegrationProperties.CHANNELS_MAX_UNICAST_SUBSCRIBERS,
Integer.class);
}
this.dispatcher.setMaxSubscribers(this.maxSubscribers);
@@ -194,6 +203,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
DispatchingMessageListener(JmsTemplate jmsTemplate,
MessageDispatcher dispatcher, SubscribableJmsChannel channel, boolean isPubSub,
MessageBuilderFactory messageBuilderFactory) {
this.jmsTemplate = jmsTemplate;
this.dispatcher = dispatcher;
this.channel = channel;
@@ -212,8 +222,10 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
converted = converter.fromMessage(message);
}
if (converted != null) {
messageToSend = (converted instanceof Message<?>) ? (Message<?>) converted
: this.messageBuilderFactory.withPayload(converted).build();
messageToSend =
converted instanceof Message<?>
? (Message<?>) converted
: this.messageBuilderFactory.withPayload(converted).build();
this.dispatcher.dispatch(messageToSend);
}
else if (this.logger.isWarnEnabled()) {
@@ -231,8 +243,7 @@ public class SubscribableJmsChannel extends AbstractJmsChannel
}
}
else {
throw new MessageDeliveryException(
messageToSend, exceptionMessage, e);
throw new MessageDeliveryException(messageToSend, exceptionMessage, 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");
* 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.MessagingGateway;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.QueueChannel;
@@ -125,6 +126,9 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Autowired
private PollableChannel jmsPubSubBridgeChannel;
@Autowired
private PollableChannel jmsPubSubBridgeChannel2;
@Autowired
@Qualifier("jmsOutboundGateway.handler")
private MessageHandler jmsOutboundGatewayHandler;
@@ -254,6 +258,11 @@ public class JmsTests extends ActiveMQMultiContextTests {
.isNotNull()
.extracting(Message::getPayload)
.isEqualTo("foo");
received = this.jmsPubSubBridgeChannel2.receive(5000);
assertThat(received)
.isNotNull()
.extracting(Message::getPayload)
.isEqualTo("foo");
}
@Test
@@ -351,10 +360,20 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Bean
public IntegrationFlow pubSubFlow() {
return IntegrationFlows
.from(Jms.publishSubscribeChannel(jmsConnectionFactory())
.destination("pubsub"))
.channel(c -> c.queue("jmsPubSubBridgeChannel"))
return f -> f
.publishSubscribeChannel(jmsPublishSubscribeChannel(),
pubsub -> pubsub
.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();
}

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");
* 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.integration.MessageDispatchingException;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.BroadcastCapableChannel;
import org.springframework.integration.channel.ChannelUtils;
import org.springframework.integration.context.IntegrationProperties;
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.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
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 Gary Russell
* @author Artem Bilan
@@ -55,7 +58,7 @@ import org.springframework.util.StringUtils;
*/
@SuppressWarnings("rawtypes")
public class SubscribableRedisChannel extends AbstractMessageChannel
implements SubscribableChannel, SmartLifecycle {
implements BroadcastCapableChannel, SmartLifecycle {
private final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
@@ -67,10 +70,6 @@ public class SubscribableRedisChannel extends AbstractMessageChannel
private final BroadcastingDispatcher dispatcher = new BroadcastingDispatcher(true);
private volatile Integer maxSubscribers;
private volatile boolean initialized;
// defaults
private Executor taskExecutor = new SimpleAsyncTaskExecutor();
@@ -78,6 +77,10 @@ public class SubscribableRedisChannel extends AbstractMessageChannel
private MessageConverter messageConverter = new SimpleMessageConverter();
private volatile Integer maxSubscribers;
private volatile boolean initialized;
public SubscribableRedisChannel(RedisConnectionFactory connectionFactory, String topicName) {
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
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.
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.
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.
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]]
=== AMQP Changes