INT-3332 GlobalChannelInterceptorBeanPostProcessor

Implicit channel declaration for items downstream
of a `ChannelInterceptor` were not created.

The `BPP` eagerly fetched the interceptors during its
own creation; this caused the context initialization to fail
because the channel initializer hasn't run yet.

Defer creation of the interceptor beans until they are
actually needed.

Also, when using `@Configuration`, the channelInitializer
is no longer the first bean in the bean factory.

INT-3332 Use SmartLifeCycle to Apply Interceptors

Instead of using a bean post processor, the interceptor
processor now performs the channel interception when beans
in phase Integer.MIN_VALUE are started - after all beans
have been instantiated.

Polishing
This commit is contained in:
Gary Russell
2014-03-19 15:10:34 +02:00
committed by Artem Bilan
parent 23a26ca38e
commit b92134fae7
12 changed files with 363 additions and 73 deletions

View File

@@ -17,20 +17,27 @@
package org.springframework.integration.channel.interceptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.OrderComparator;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
@@ -40,59 +47,85 @@ import org.springframework.util.StringUtils;
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Artem Bilan
* @author Gary Russell
* @since 2.0
*/
final class GlobalChannelInterceptorBeanPostProcessor implements BeanPostProcessor, InitializingBean {
final class GlobalChannelInterceptorProcessor implements BeanFactoryAware, SmartLifecycle {
private static final Log logger = LogFactory.getLog(GlobalChannelInterceptorBeanPostProcessor.class);
private static final Log logger = LogFactory.getLog(GlobalChannelInterceptorProcessor.class);
private final OrderComparator comparator = new OrderComparator();
private volatile List<GlobalChannelInterceptorWrapper> channelInterceptors;
private final Set<GlobalChannelInterceptorWrapper> positiveOrderInterceptors = new LinkedHashSet<GlobalChannelInterceptorWrapper>();
private final Set<GlobalChannelInterceptorWrapper> negativeOrderInterceptors = new LinkedHashSet<GlobalChannelInterceptorWrapper>();
private ListableBeanFactory beanFactory;
GlobalChannelInterceptorBeanPostProcessor(List<GlobalChannelInterceptorWrapper> channelInterceptors) {
this.channelInterceptors = channelInterceptors;
}
private volatile boolean processed;
@Override
public void afterPropertiesSet() throws Exception {
for (GlobalChannelInterceptorWrapper channelInterceptor : this.channelInterceptors) {
if (channelInterceptor.getOrder() >= 0) {
this.positiveOrderInterceptors.add(channelInterceptor);
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ListableBeanFactory.class, beanFactory);
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
public synchronized void start() {
if (!this.processed) {
Collection<GlobalChannelInterceptorWrapper> interceptors = this.beanFactory.getBeansOfType(GlobalChannelInterceptorWrapper.class).values();
if (CollectionUtils.isEmpty(interceptors)) {
logger.debug("No global channel interceptors.");
}
else {
this.negativeOrderInterceptors.add(channelInterceptor);
for (GlobalChannelInterceptorWrapper channelInterceptor : interceptors) {
if (channelInterceptor.getOrder() >= 0) {
this.positiveOrderInterceptors.add(channelInterceptor);
}
else {
this.negativeOrderInterceptors.add(channelInterceptor);
}
}
Map<String, ChannelInterceptorAware> channels = this.beanFactory.getBeansOfType(ChannelInterceptorAware.class);
for (Entry<String, ChannelInterceptorAware> entry : channels.entrySet()) {
this.addMatchingInterceptors(entry.getValue(), entry.getKey());
}
}
this.processed = true;
}
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
public void stop() {
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ChannelInterceptorAware && bean instanceof MessageChannel) {
if (logger.isDebugEnabled()) {
logger.debug("Applying global interceptors on channel '" + beanName + "'");
}
this.addMatchingInterceptors((ChannelInterceptorAware) bean, beanName);
}
return bean;
public boolean isRunning() {
return false;
}
@Override
public int getPhase() {
return Integer.MIN_VALUE;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public void stop(Runnable callback) {
}
/**
* Adds any interceptor whose pattern matches against the channel's name.
*/
private void addMatchingInterceptors(ChannelInterceptorAware channel, String beanName) {
if (logger.isDebugEnabled()) {
logger.debug("Applying global interceptors on channel '" + beanName + "'");
}
List<GlobalChannelInterceptorWrapper> tempInterceptors = new ArrayList<GlobalChannelInterceptorWrapper>();
for (GlobalChannelInterceptorWrapper globalChannelInterceptorWrapper : this.positiveOrderInterceptors) {
String[] patterns = globalChannelInterceptorWrapper.getPatterns();
@@ -103,7 +136,11 @@ final class GlobalChannelInterceptorBeanPostProcessor implements BeanPostProcess
}
Collections.sort(tempInterceptors, this.comparator);
for (GlobalChannelInterceptorWrapper next : tempInterceptors) {
channel.addInterceptor(next.getChannelInterceptor());
ChannelInterceptor channelInterceptor = next.getChannelInterceptor();
if (!(channelInterceptor instanceof VetoCapableInterceptor)
|| ((VetoCapableInterceptor) channelInterceptor).shouldIntercept(beanName, channel)) {
channel.addInterceptor(channelInterceptor);
}
}
tempInterceptors.clear();
@@ -117,7 +154,11 @@ final class GlobalChannelInterceptorBeanPostProcessor implements BeanPostProcess
Collections.sort(tempInterceptors, comparator);
if (!tempInterceptors.isEmpty()) {
for (int i = tempInterceptors.size() - 1; i >= 0; i--) {
channel.addInterceptor(0, tempInterceptors.get(i).getChannelInterceptor());
ChannelInterceptor channelInterceptor = tempInterceptors.get(i).getChannelInterceptor();
if (!(channelInterceptor instanceof VetoCapableInterceptor)
|| ((VetoCapableInterceptor) channelInterceptor).shouldIntercept(beanName, channel)) {
channel.addInterceptor(0, channelInterceptor);
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2014 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.integration.channel.interceptor;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.messaging.support.ChannelInterceptor;
/**
* {@link ChannelInterceptor}s implementing this interface can veto
* global interception of a particular channel name. Could be used, for example,
* when an interceptor itself writes to an output channel (which should
* not be intercepted with this interceptor).
*
* @author Gary Russell
* @since 4.0
*
*/
public interface VetoCapableInterceptor {
/**
* @param beanName The channel name.
* @return false if the intercept wishes to veto the interception.
*/
boolean shouldIntercept(String beanName, ChannelInterceptorAware channel);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -20,6 +20,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.Lifecycle;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.integration.core.MessageSelector;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
@@ -35,9 +36,10 @@ import org.springframework.util.Assert;
* to a secondary target while still sending the original message to the main channel.
*
* @author Mark Fisher
* @author Gary Russell
*/
@ManagedResource
public class WireTap extends ChannelInterceptorAdapter implements Lifecycle {
public class WireTap extends ChannelInterceptorAdapter implements Lifecycle, VetoCapableInterceptor {
private static final Log logger = LogFactory.getLog(WireTap.class);
@@ -133,4 +135,9 @@ public class WireTap extends ChannelInterceptorAdapter implements Lifecycle {
return message;
}
@Override
public boolean shouldIntercept(String beanName, ChannelInterceptorAware channel) {
return !this.channel.equals(channel);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2014 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.
@@ -31,6 +31,7 @@ import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.channel.interceptor.GlobalChannelInterceptorWrapper;
import org.springframework.integration.config.IntegrationConfigUtils;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.util.xml.DomUtils;
/**
@@ -39,6 +40,7 @@ import org.springframework.util.xml.DomUtils;
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author David Turanski
* @author Gary Russell
* @since 2.0
*/
public class GlobalChannelInterceptorParser extends AbstractBeanDefinitionParser {
@@ -49,14 +51,13 @@ public class GlobalChannelInterceptorParser extends AbstractBeanDefinitionParser
private static final String REF_ATTRIBUTE = "ref";
private static final String GLOBAL_POST_PROCESSOR_CLASSNAME = "GlobalChannelInterceptorBeanPostProcessor";
private static final String GLOBAL_INTERCEPTOR_PROCESSOR_CLASSNAME = "GlobalChannelInterceptorProcessor";
private final ManagedList<RuntimeBeanReference> globalInterceptors = new ManagedList<RuntimeBeanReference>();
private volatile boolean postProcessorCreated;
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
this.createAndRegisterGlobalPostProcessorIfNecessary(parserContext);
BeanDefinitionBuilder globalChannelInterceptorBuilder = BeanDefinitionBuilder.genericBeanDefinition(GlobalChannelInterceptorWrapper.class);
@@ -73,14 +74,12 @@ public class GlobalChannelInterceptorParser extends AbstractBeanDefinitionParser
}
private void createAndRegisterGlobalPostProcessorIfNecessary(ParserContext parserContext) {
if (!this.postProcessorCreated) {
BeanDefinitionBuilder postProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition(
BASE_PACKAGE + GLOBAL_POST_PROCESSOR_CLASSNAME);
postProcessorBuilder.addConstructorArgValue(this.globalInterceptors);
BeanDefinition beanDef = postProcessorBuilder.getBeanDefinition();
String beanName = BeanDefinitionReaderUtils.generateBeanName(beanDef, parserContext.getRegistry());
parserContext.registerBeanComponent(new BeanComponentDefinition(beanDef, beanName));
this.postProcessorCreated = true;
if (!parserContext.getRegistry().containsBeanDefinition(IntegrationContextUtils.GLOBAL_CHANNEL_INTERCEPTOR_PROCESSOR_BEAN_NAME)) {
BeanDefinitionBuilder processorBuilder = BeanDefinitionBuilder.genericBeanDefinition(
BASE_PACKAGE + GLOBAL_INTERCEPTOR_PROCESSOR_CLASSNAME);
BeanDefinition beanDef = processorBuilder.getBeanDefinition();
parserContext.registerBeanComponent(new BeanComponentDefinition(beanDef,
IntegrationContextUtils.GLOBAL_CHANNEL_INTERCEPTOR_PROCESSOR_BEAN_NAME));
}
}

View File

@@ -79,7 +79,11 @@ public abstract class IntegrationContextUtils {
public static final String INTEGRATION_DATATYPE_CHANNEL_MESSAGE_CONVERTER_BEAN_NAME = "datatypeChannelMessageConverter";
public static final String INTEGRATION_FIXED_SUBSCRIBER_CHANNEL_BPP_BEAN_NAME = "fixedSubscriberChannelBeanFactoryPostProcessor";
public static final String INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME = "messageBuilderFactory";
public static final String GLOBAL_CHANNEL_INTERCEPTOR_PROCESSOR_BEAN_NAME = "gloabelChannelInterceptorProcessor";
/**
* @param beanFactory BeanFactory for lookup, must not be null.
* @return The {@link MetadataStore} bean whose name is "metadataStore".

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Copyright 2002-2014 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.
@@ -18,6 +18,7 @@ import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.messaging.Message;
@@ -27,73 +28,74 @@ import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
*
*
* @author David Turanski
* @author Gary Russell
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class GlobalWireTapTests {
@Autowired
@Qualifier("channel")
MessageChannel channel;
@Autowired
@Qualifier("random-channel")
MessageChannel anotherChannel;
@Autowired
@Qualifier("wiretap-single")
PollableChannel wiretapSingle;
@Autowired
@Qualifier("wiretap-all")
PollableChannel wiretapAll;
@Test
public void testWireTapsOnTargetedChannel(){
Message<?> message = new GenericMessage<String>("hello");
Message<?> message = new GenericMessage<String>("hello");
channel.send(message);
Message <?> wireTapMessage = wiretapSingle.receive(100);
assertNotNull(wireTapMessage);
//There shoud be two messages on this channel. One for 'channel' and one for 'output'
//There should be three messages on this channel. One for 'channel', one for 'output', and one for 'wiretapSingle'.
wireTapMessage = wiretapAll.receive(100);
int msgCount=0;
while (wireTapMessage != null){
msgCount++;
assertEquals(wireTapMessage.getPayload(),message.getPayload());
wireTapMessage = wiretapAll.receive(100);
}
assertEquals(2,msgCount);
}
assertEquals(3,msgCount);
}
@Test
public void testWireTapsOnRandomChannel(){
Message<?> message = new GenericMessage<String>("hello");
Message<?> message = new GenericMessage<String>("hello");
anotherChannel.send(message);
//This time no message on wiretapSingle
Message <?> wireTapMessage = wiretapSingle.receive(100);
assertNull(wireTapMessage);
//There shoud be two messages on this channel. One for 'channel' and one for 'output'
//There should be two messages on this channel. One for 'channel' and one for 'output'
wireTapMessage = wiretapAll.receive(100);
int msgCount=0;
while (wireTapMessage != null){
msgCount++;
assertEquals(wireTapMessage.getPayload(),message.getPayload());
wireTapMessage = wiretapAll.receive(100);
}
}
assertEquals(2,msgCount);
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<int:wire-tap channel="bar" />
<int:service-activator input-channel="bar" expression="'foo'" />
<int:channel-interceptor>
<bean class="org.springframework.integration.channel.interceptor.ImplicitConsumerChannelTests$Interceptor1">
<property name="channel" ref="foo" />
</bean>
</int:channel-interceptor>
<int:channel-interceptor>
<bean class="org.springframework.integration.channel.interceptor.ImplicitConsumerChannelTests$Interceptor2">
<property name="channel" ref="baz" />
</bean>
</int:channel-interceptor>
<int:channel id="foo" />
<int:channel id="baz" />
</beans>

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2014 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.integration.channel.interceptor;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gary Russell
* @since 4.0
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class ImplicitConsumerChannelTests {
@Autowired
private AbstractMessageChannel bar;
@Autowired
private AbstractMessageChannel foo;
@Autowired
private AbstractMessageChannel baz;
@Test
public void testImplicit() {
// used to fail to load AC (no channel 'bar')
List<ChannelInterceptor> barInterceptors = bar.getChannelInterceptors();
assertEquals(2, barInterceptors.size());
assertThat(barInterceptors.get(0), anyOf(instanceOf(Interceptor1.class), instanceOf(Interceptor2.class)));
assertThat(barInterceptors.get(1), anyOf(instanceOf(Interceptor1.class), instanceOf(Interceptor2.class)));
List<ChannelInterceptor> fooInterceptors = foo.getChannelInterceptors();
assertEquals(2, fooInterceptors.size());
assertThat(fooInterceptors.get(0), anyOf(instanceOf(WireTap.class), instanceOf(Interceptor2.class)));
assertThat(fooInterceptors.get(1), anyOf(instanceOf(WireTap.class), instanceOf(Interceptor2.class)));
List<ChannelInterceptor> bazInterceptors = baz.getChannelInterceptors();
assertEquals(2, bazInterceptors.size());
assertThat(bazInterceptors.get(0), anyOf(instanceOf(WireTap.class), instanceOf(Interceptor1.class)));
assertThat(bazInterceptors.get(1), anyOf(instanceOf(WireTap.class), instanceOf(Interceptor1.class)));
}
public static class Interceptor1 implements ChannelInterceptor, VetoCapableInterceptor {
private MessageChannel channel;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return null;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
}
@Override
public boolean preReceive(MessageChannel channel) {
return false;
}
@Override
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
return null;
}
public void setChannel(MessageChannel channel) {
this.channel = channel;
}
public MessageChannel getChannel() {
return channel;
}
@Override
public boolean shouldIntercept(String beanName, ChannelInterceptorAware channel) {
return !this.channel.equals(channel);
}
}
public static class Interceptor2 implements ChannelInterceptor, VetoCapableInterceptor {
private MessageChannel channel;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return null;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
}
@Override
public boolean preReceive(MessageChannel channel) {
return false;
}
@Override
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
return null;
}
public void setChannel(MessageChannel channel) {
this.channel = channel;
}
public MessageChannel getChannel() {
return channel;
}
@Override
public boolean shouldIntercept(String beanName, ChannelInterceptorAware channel) {
return !this.channel.equals(channel);
}
}
}

View File

@@ -8,6 +8,10 @@
<message-history tracked-components="publishedChannel,input,*AnnotationTestService*"/>
<annotation-config default-publisher-channel="publishedChannel"/>
<annotation-config default-publisher-channel="publishedChannel"/>
<channel-interceptor pattern="none">
<wire-tap channel="bar" />
</channel-interceptor>
<service-activator input-channel="bar" expression="'foo'" />
</beans:beans>

View File

@@ -837,6 +837,17 @@ payload to an Integer.
best of both worlds: 1) the sending of a JMS Message can occur within the transaction while
2) it is still a "fire-and-forget" action thereby preventing any noticeable delay in the main message flow.
</para>
<important>
Starting with <emphasis>version 4.0</emphasis>, it is important to avoid circular references when an
interceptor (such as <classname>WireTap</classname>) references a channel itself. You need to exclude
such channels from those being intercepted by the current interceptor. This can be done with appropriate
<code>patterns</code> or programmatically. If you have a custom <interfacename>ChannelInterceptor</interfacename>
that references a <code>channel</code>, consider implementing <interfacename>VetoCapableInterceptor</interfacename>.
That way, the framework will ask the interceptor if it's OK to intercept each channel that is a candidate based
on the pattern. You can also add runtime protection in the interceptor methods that ensures that the channel is
not one that is referenced by the interceptor. The <classname>WireTap</classname> uses both of these
techniques.
</important>
</section>
<section id="channel-global-wiretap">
<title>Global Wire Tap Configuration</title>

View File

@@ -434,7 +434,7 @@ rt.setConnectionFactory(redisConnectionFactory);]]></programlisting>
<int:queue message-store="redisMessageStore"/>
<int:channel>]]></programlisting>
<para>
The keys are used to store the data have the form <code>&lt;storeBeanName&gt;:&lt;channelId&gt;</code>
The keys that are used to store the data have the form <code>&lt;storeBeanName&gt;:&lt;channelId&gt;</code>
(in the above example, <code>redisMessageStore:somePersistentQueueChannel</code>).
</para>
<para>

View File

@@ -9,6 +9,13 @@
in more details, please see the Issue Tracker tickets that
were resolved as part of the 4.0 development process.
</para>
<para>
Please be sure to also see the
<ulink url="https://github.com/spring-projects/spring-integration/wiki/Spring-Integration-3.0-to-4.0-Migration-Guide"
>Migration Guide</ulink> for important changes that might affect your applications.
Migration guides for all versions back to <emphasis>2.1</emphasis> can be found on the
<ulink url="https://github.com/spring-projects/spring-integration/wiki">Wiki</ulink>.
</para>
<section id="4.0-new-components">
<title>New Components</title>
<section id="4.0-mqtt">