Introduce a ReceiveMessageAdvice (#3265)

* Introduce a `ReceiveMessageAdvice`

* Deprecate an `AbstractMessageSourceAdvice` in favor of
`default` method in the `MessageSourceMutator`
* Move a `applyReceiveOnlyAdviceChain()` logic into the `AbstractPollingEndpoint`:
now both `PollingConsumer` and `SourcePollingChannelAdapter` can use
`ReceiveMessageAdvice`
* Introduce a `SimpleActiveIdleReceiveMessageAdvice` based already
on the `ReceiveMessageAdvice` and deprecate a `SimpleActiveIdleMessageSourceAdvice`
which is fully replaceable with newly introduced `SimpleActiveIdleReceiveMessageAdvice`
* Add `@SuppressWarnings("deprecation")` for those out-of-the-box `ReceiveMessageAdvice`
implementation which still use an `AbstractMessageSourceAdvice` for
backward compatibility
* Document a new feature and give the `MessageSourceMutator` a new meaning

* * Fix language in the `polling-consumer.adoc`
This commit is contained in:
Artem Bilan
2020-04-28 13:03:30 -04:00
committed by GitHub
parent cfd03f89a0
commit 2d9a5f60f4
16 changed files with 394 additions and 128 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2019 the original author or authors.
* Copyright 2015-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.
@@ -16,34 +16,20 @@
package org.springframework.integration.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.Message;
/**
* Advice for a {@link MessageSource#receive()} method to decide whether a poll
* should be ignored and/or take action after the receive.
*
* @author Gary Russell
* @author Artem Bilan
*
* @since 4.2
*
* @deprecated since 5.3 in favor of {@link MessageSourceMutator}.
*/
public abstract class AbstractMessageSourceAdvice implements MethodInterceptor, MessageSourceMutator {
@Override
public final Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
if (!(target instanceof MessageSource)) {
return invocation.proceed();
}
Message<?> result = null;
if (beforeReceive((MessageSource<?>) target)) {
result = (Message<?>) invocation.proceed();
}
return afterReceive(result, (MessageSource<?>) target);
}
@Deprecated
public abstract class AbstractMessageSourceAdvice implements MessageSourceMutator {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2019 the original author or authors.
* Copyright 2015-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.
@@ -18,6 +18,7 @@ package org.springframework.integration.aop;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.util.CompoundTrigger;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.scheduling.Trigger;
import org.springframework.util.Assert;
@@ -35,7 +36,10 @@ import org.springframework.util.Assert;
* @since 4.3
*
*/
public class CompoundTriggerAdvice extends AbstractMessageSourceAdvice {
@SuppressWarnings("deprecation")
public class CompoundTriggerAdvice
extends AbstractMessageSourceAdvice
implements ReceiveMessageAdvice {
private final CompoundTrigger compoundTrigger;
@@ -47,8 +51,21 @@ public class CompoundTriggerAdvice extends AbstractMessageSourceAdvice {
this.override = overrideTrigger;
}
/**
* @param result the received message.
* @param source the message source.
* @return the message or null
* @deprecated since 5.3 in favor of {@link #afterReceive(Message, Object)}
*/
@Override
@Deprecated
public Message<?> afterReceive(Message<?> result, MessageSource<?> source) {
return afterReceive(result, (Object) source);
}
@Override
@Nullable
public Message<?> afterReceive(@Nullable Message<?> result, Object source) {
if (result == null) {
this.compoundTrigger.setOverride(this.override);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2019 the original author or authors.
* Copyright 2018-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.
@@ -17,19 +17,31 @@
package org.springframework.integration.aop;
import org.springframework.integration.core.MessageSource;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
/**
* An object that can mutate a {@link MessageSource} before and/or after
* A {@link ReceiveMessageAdvice} extension that can mutate a {@link MessageSource} before and/or after
* {@link MessageSource#receive()} is called.
*
* @author Gary Russell
* @author Artem Bilan
*
* @since 5.0.7.
*
* @since 5.0.7
*/
@FunctionalInterface
public interface MessageSourceMutator {
public interface MessageSourceMutator extends ReceiveMessageAdvice {
@Override
default boolean beforeReceive(Object source) {
if (source instanceof MessageSource<?>) {
return beforeReceive((MessageSource<?>) source);
}
else {
throw new IllegalArgumentException(
"The 'MessageSourceMutator' supports only a 'MessageSource' in the before/after hooks: " + source);
}
}
/**
* Subclasses can decide whether to proceed with this poll.
@@ -40,6 +52,18 @@ public interface MessageSourceMutator {
return true;
}
@Override
@Nullable
default Message<?> afterReceive(@Nullable Message<?> result, Object source) {
if (source instanceof MessageSource<?>) {
return afterReceive(result, (MessageSource<?>) source);
}
else {
throw new IllegalArgumentException(
"The 'MessageSourceMutator' supports only a 'MessageSource' in the before/after hooks: " + source);
}
}
/**
* Subclasses can take actions based on the result of the poll; e.g.
* adjust the {@code trigger}. The message can also be replaced with a new one.
@@ -47,6 +71,7 @@ public interface MessageSourceMutator {
* @param source the message source.
* @return a message to continue to process the result, null to discard whatever the poll returned.
*/
Message<?> afterReceive(Message<?> result, MessageSource<?> source);
@Nullable
Message<?> afterReceive(@Nullable Message<?> result, MessageSource<?> source);
}

View File

@@ -0,0 +1,72 @@
/*
* 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.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.integration.core.MessageSource;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.PollableChannel;
/**
* An AOP advice to perform hooks before and/or after a {@code receive()} contract is called.
*
* @author Artem Bilan
*
* @since 5.3
*/
@FunctionalInterface
public interface ReceiveMessageAdvice extends MethodInterceptor {
/**
* Subclasses can decide whether to {@link MethodInvocation#proceed()} or not.
* @param source the source of the message to receive.
* @return true to proceed (default).
*/
default boolean beforeReceive(Object source) {
return true;
}
@Override
@Nullable
default Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
if (!(target instanceof MessageSource) && !(target instanceof PollableChannel)) {
return invocation.proceed();
}
Message<?> result = null;
if (beforeReceive(target)) {
result = (Message<?>) invocation.proceed();
}
return afterReceive(result, target);
}
/**
* Subclasses can take actions based on the result of the {@link MethodInvocation#proceed()}; e.g.
* adjust the {@code trigger}. The message can also be replaced with a new one.
* @param result the received message.
* @param source the source of the message to receive.
* @return a message to continue to process the result, null to discard whatever
* the {@link MethodInvocation#proceed()} returned.
*/
@Nullable
Message<?> afterReceive(@Nullable Message<?> result, Object source);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2019 the original author or authors.
* Copyright 2015-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.
@@ -31,7 +31,11 @@ import org.springframework.messaging.Message;
* @since 4.2
*
* @see DynamicPeriodicTrigger
*
* @deprecated since 5.3 in favor of {@link SimpleActiveIdleReceiveMessageAdvice} with the same
* (but more common) functionality.
*/
@Deprecated
public class SimpleActiveIdleMessageSourceAdvice extends AbstractMessageSourceAdvice {
private final DynamicPeriodicTrigger trigger;

View File

@@ -0,0 +1,80 @@
/*
* 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.aop;
import java.time.Duration;
import org.springframework.integration.util.DynamicPeriodicTrigger;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
A simple advice that polls at one rate when messages exist and another when
* there are no messages.
*
* @author Gary Russell
* @author Artem Bilan
*
* @since 5.3
*
* @see DynamicPeriodicTrigger
*/
public class SimpleActiveIdleReceiveMessageAdvice implements ReceiveMessageAdvice {
private final DynamicPeriodicTrigger trigger;
private volatile Duration idlePollPeriod;
private volatile Duration activePollPeriod;
public SimpleActiveIdleReceiveMessageAdvice(DynamicPeriodicTrigger trigger) {
Assert.notNull(trigger, "'trigger' must not be null");
this.trigger = trigger;
this.idlePollPeriod = trigger.getDuration();
this.activePollPeriod = trigger.getDuration();
}
/**
* Set the poll period when messages are not returned. Defaults to the
* trigger's period.
* @param idlePollPeriod the period in milliseconds.
*/
public void setIdlePollPeriod(long idlePollPeriod) {
this.idlePollPeriod = Duration.ofMillis(idlePollPeriod);
}
/**
* Set the poll period when messages are returned. Defaults to the
* trigger's period.
* @param activePollPeriod the period in milliseconds.
*/
public void setActivePollPeriod(long activePollPeriod) {
this.activePollPeriod = Duration.ofMillis(activePollPeriod);
}
@Override
public Message<?> afterReceive(Message<?> result, Object source) {
if (result == null) {
this.trigger.setDuration(this.idlePollPeriod);
}
else {
this.trigger.setDuration(this.activePollPeriod);
}
return result;
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.integration.endpoint;
import java.time.Duration;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
@@ -28,10 +29,14 @@ import java.util.stream.Collectors;
import org.aopalliance.aop.Advice;
import org.reactivestreams.Subscription;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.integration.aop.ReceiveMessageAdvice;
import org.springframework.integration.channel.ChannelUtils;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.support.MessagingExceptionWrapper;
@@ -72,6 +77,8 @@ public abstract class AbstractPollingEndpoint extends AbstractEndpoint implement
*/
public static final long DEFAULT_POLLING_PERIOD = 10;
private final Collection<Advice> appliedAdvices = new HashSet<>();
private final Object initializationMonitor = new Object();
private Executor taskExecutor = new SyncTaskExecutor();
@@ -173,7 +180,7 @@ public abstract class AbstractPollingEndpoint extends AbstractEndpoint implement
* @return true to only advise the receive operation.
*/
protected boolean isReceiveOnlyAdvice(Advice advice) {
return false;
return advice instanceof ReceiveMessageAdvice;
}
/**
@@ -181,6 +188,37 @@ public abstract class AbstractPollingEndpoint extends AbstractEndpoint implement
* @param chain the advice chain {@code Collection}.
*/
protected void applyReceiveOnlyAdviceChain(Collection<Advice> chain) {
if (!CollectionUtils.isEmpty(chain)) {
Object source = getReceiveMessageSource();
if (source != null) {
if (AopUtils.isAopProxy(source)) {
Advised advised = (Advised) source;
this.appliedAdvices.forEach(advised::removeAdvice);
chain.forEach(advice -> advised.addAdvisor(adviceToReceiveAdvisor(advice)));
}
else {
ProxyFactory proxyFactory = new ProxyFactory(source);
chain.forEach(advice -> proxyFactory.addAdvisor(adviceToReceiveAdvisor(advice)));
source = proxyFactory.getProxy(getBeanClassLoader());
}
this.appliedAdvices.clear();
this.appliedAdvices.addAll(chain);
if (!(isSyncExecutor()) && logger.isWarnEnabled()) {
logger.warn(getComponentName() + ": A task executor is supplied and " + chain.size()
+ "ReceiveMessageAdvice(s) is/are provided. If an advice mutates the source, such "
+ "mutations are not thread safe and could cause unexpected results, especially with "
+ "high frequency pollers. Consider using a downstream ExecutorChannel instead of "
+ "adding an executor to the poller");
}
setReceiveMessageSource(source);
}
}
}
private NameMatchMethodPointcutAdvisor adviceToReceiveAdvisor(Advice advice) {
NameMatchMethodPointcutAdvisor sourceAdvisor = new NameMatchMethodPointcutAdvisor(advice);
sourceAdvisor.addMethodName("receive");
return sourceAdvisor;
}
protected boolean isReactive() {
@@ -191,6 +229,14 @@ public abstract class AbstractPollingEndpoint extends AbstractEndpoint implement
return this.pollingFlux;
}
protected Object getReceiveMessageSource() {
return null;
}
protected void setReceiveMessageSource(Object source) {
}
@Override
protected void onInit() {
synchronized (this.initializationMonitor) {

View File

@@ -57,13 +57,13 @@ public class PollingConsumer extends AbstractPollingEndpoint implements Integrat
*/
public static final long DEFAULT_RECEIVE_TIMEOUT = 1000;
private final PollableChannel inputChannel;
private final MessageHandler handler;
private final List<ChannelInterceptor> channelInterceptors;
private volatile long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
private PollableChannel inputChannel;
private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
public PollingConsumer(PollableChannel inputChannel, MessageHandler handler) {
Assert.notNull(inputChannel, "inputChannel must not be null");
@@ -110,6 +110,16 @@ public class PollingConsumer extends AbstractPollingEndpoint implements Integrat
return this.handler;
}
@Override
protected Object getReceiveMessageSource() {
return this.inputChannel;
}
@Override
protected void setReceiveMessageSource(Object source) {
this.inputChannel = (PollableChannel) source;
}
@Override
protected boolean isReactive() {
return getOutputChannel() instanceof ReactiveStreamsSubscribableChannel &&

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.
@@ -16,21 +16,12 @@
package org.springframework.integration.endpoint;
import java.util.Collection;
import java.util.HashSet;
import org.aopalliance.aop.Advice;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.Lifecycle;
import org.springframework.integration.StaticMessageHeaderAccessor;
import org.springframework.integration.acks.AckUtils;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.aop.MessageSourceMutator;
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
import org.springframework.integration.context.ExpressionCapable;
import org.springframework.integration.core.MessageSource;
@@ -43,7 +34,6 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A Channel Adapter implementation for connecting a
@@ -59,9 +49,7 @@ public class SourcePollingChannelAdapter extends AbstractPollingEndpoint
private final MessagingTemplate messagingTemplate = new MessagingTemplate();
private final Collection<Advice> appliedAdvices = new HashSet<>();
private volatile MessageSource<?> originalSource;
private MessageSource<?> originalSource;
private volatile MessageSource<?> source;
@@ -136,45 +124,19 @@ public class SourcePollingChannelAdapter extends AbstractPollingEndpoint
((NamedComponent) this.source).getComponentType() : "inbound-channel-adapter";
}
@Override
protected boolean isReceiveOnlyAdvice(Advice advice) {
return advice instanceof MessageSourceMutator;
}
@Override
protected void applyReceiveOnlyAdviceChain(Collection<Advice> chain) {
if (!CollectionUtils.isEmpty(chain)) {
if (AopUtils.isAopProxy(this.source)) {
Advised advised = (Advised) this.source;
this.appliedAdvices.forEach(advised::removeAdvice);
chain.forEach(advice -> advised.addAdvisor(adviceToReceiveAdvisor(advice)));
}
else {
ProxyFactory proxyFactory = new ProxyFactory(this.source);
chain.forEach(advice -> proxyFactory.addAdvisor(adviceToReceiveAdvisor(advice)));
this.source = (MessageSource<?>) proxyFactory.getProxy(getBeanClassLoader());
}
this.appliedAdvices.clear();
this.appliedAdvices.addAll(chain);
if (!(isSyncExecutor()) && logger.isWarnEnabled()) {
logger.warn(getComponentName() + ": A task executor is supplied and " + chain.size()
+ "MessageSourceMutator(s) is/are provided. If an advice mutates the source, such "
+ "mutations are not thread safe and could cause unexpected results, especially with "
+ "high frequency pollers. Consider using a downstream ExecutorChannel instead of "
+ "adding an executor to the poller");
}
}
}
@Override
protected boolean isReactive() {
return getOutputChannel() instanceof ReactiveStreamsSubscribableChannel;
}
private NameMatchMethodPointcutAdvisor adviceToReceiveAdvisor(Advice advice) {
NameMatchMethodPointcutAdvisor sourceAdvisor = new NameMatchMethodPointcutAdvisor(advice);
sourceAdvisor.addMethodName("receive");
return sourceAdvisor;
@Override
protected Object getReceiveMessageSource() {
return getMessageSource();
}
@Override
protected final void setReceiveMessageSource(Object source) {
this.source = (MessageSource<?>) source;
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-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.
@@ -35,8 +35,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.Joinpoint;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
@@ -48,9 +47,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.aop.AbstractMessageSourceAdvice;
import org.springframework.integration.aop.CompoundTriggerAdvice;
import org.springframework.integration.aop.SimpleActiveIdleMessageSourceAdvice;
import org.springframework.integration.aop.ReceiveMessageAdvice;
import org.springframework.integration.aop.SimpleActiveIdleReceiveMessageAdvice;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.config.EnableIntegration;
@@ -64,14 +63,14 @@ import org.springframework.integration.util.CompoundTrigger;
import org.springframework.integration.util.DynamicPeriodicTrigger;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Gary Russell
@@ -80,8 +79,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @since 4.1
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class PollerAdviceTests {
@@ -194,10 +192,10 @@ public class PollerAdviceTests {
});
final AtomicInteger count = new AtomicInteger();
class TestSourceAdvice extends AbstractMessageSourceAdvice {
class TestSourceAdvice implements ReceiveMessageAdvice {
@Override
public boolean beforeReceive(MessageSource<?> target) {
public boolean beforeReceive(Object target) {
count.incrementAndGet();
callOrder.add("b");
latch.get().countDown();
@@ -205,7 +203,7 @@ public class PollerAdviceTests {
}
@Override
public Message<?> afterReceive(Message<?> result, MessageSource<?> target) {
public Message<?> afterReceive(Message<?> result, Object target) {
callOrder.add("d");
latch.get().countDown();
return result;
@@ -267,7 +265,7 @@ public class PollerAdviceTests {
latch.countDown();
return m;
});
SimpleActiveIdleMessageSourceAdvice toggling = new SimpleActiveIdleMessageSourceAdvice(trigger);
SimpleActiveIdleReceiveMessageAdvice toggling = new SimpleActiveIdleReceiveMessageAdvice(trigger);
toggling.setActivePollPeriod(11);
toggling.setIdlePollPeriod(12);
adapter.setAdviceChain(Collections.singletonList(toggling));
@@ -282,6 +280,56 @@ public class PollerAdviceTests {
}
}
@Test
public void testActiveIdleAdviceOnQueueChannel() throws Exception {
final CountDownLatch latch = new CountDownLatch(5);
final LinkedList<Long> triggerPeriods = new LinkedList<>();
final DynamicPeriodicTrigger trigger = new DynamicPeriodicTrigger(10);
PollingConsumer pollingConsumer =
new PollingConsumer(new PollableChannel() {
@Override
public Message<?> receive() {
synchronized (triggerPeriods) {
triggerPeriods.add(trigger.getDuration().toMillis());
}
Message<Object> m = null;
if (latch.getCount() % 2 == 0) {
m = new GenericMessage<>("foo");
}
latch.countDown();
return m;
}
@Override
public Message<?> receive(long timeout) {
return receive();
}
@Override
public boolean send(Message<?> message, long timeout) {
return false;
}
}, m -> { });
SimpleActiveIdleReceiveMessageAdvice toggling = new SimpleActiveIdleReceiveMessageAdvice(trigger);
toggling.setActivePollPeriod(11);
toggling.setIdlePollPeriod(12);
pollingConsumer.setAdviceChain(Collections.singletonList(toggling));
pollingConsumer.setTrigger(trigger);
pollingConsumer.setBeanFactory(this.beanFactory);
pollingConsumer.setTaskScheduler(this.threadPoolTaskScheduler);
pollingConsumer.afterPropertiesSet();
pollingConsumer.start();
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
pollingConsumer.stop();
synchronized (triggerPeriods) {
assertThat(triggerPeriods.subList(0, 5)).containsExactly(10L, 12L, 11L, 12L, 11L);
}
}
@Test
public void testCompoundTriggerAdvice() throws Exception {
SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter();
@@ -361,18 +409,18 @@ public class PollerAdviceTests {
}
public static class OtherAdvice extends AbstractMessageSourceAdvice {
public static class OtherAdvice implements ReceiveMessageAdvice {
private int calls;
@Override
public boolean beforeReceive(MessageSource<?> source) {
public boolean beforeReceive(Object source) {
this.calls++;
return true;
}
@Override
public Message<?> afterReceive(Message<?> result, MessageSource<?> source) {
public Message<?> afterReceive(Message<?> result, Object source) {
return result;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2019 the original author or authors.
* Copyright 2018-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.
@@ -18,9 +18,10 @@ package org.springframework.integration.file.remote.aop;
import java.util.List;
import org.springframework.integration.aop.AbstractMessageSourceAdvice;
import org.springframework.integration.aop.MessageSourceMutator;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.file.remote.session.DelegatingSessionFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -35,7 +36,10 @@ import org.springframework.util.Assert;
* @since 5.0.7
*
*/
public class RotatingServerAdvice extends AbstractMessageSourceAdvice {
@SuppressWarnings("deprecation")
public class RotatingServerAdvice
extends org.springframework.integration.aop.AbstractMessageSourceAdvice
implements MessageSourceMutator {
private final RotationPolicy rotationPolicy;
@@ -79,7 +83,8 @@ public class RotatingServerAdvice extends AbstractMessageSourceAdvice {
}
@Override
public Message<?> afterReceive(Message<?> result, MessageSource<?> source) {
@Nullable
public Message<?> afterReceive(@Nullable Message<?> result, MessageSource<?> source) {
this.rotationPolicy.afterReceive(result != null, source);
return result;
}

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.
@@ -31,9 +31,8 @@ import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.integration.aop.AbstractMessageSourceAdvice;
import org.springframework.integration.aop.ReceiveMessageAdvice;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.mongodb.rules.MongoDbAvailable;
import org.springframework.integration.mongodb.rules.MongoDbAvailableTests;
@@ -278,15 +277,10 @@ public class MongoDbInboundChannelAdapterIntegrationTests extends MongoDbAvailab
}
public static final class TestMessageSourceAdvice extends AbstractMessageSourceAdvice {
public static final class TestMessageSourceAdvice implements ReceiveMessageAdvice {
@Override
public boolean beforeReceive(MessageSource<?> source) {
return true;
}
@Override
public Message<?> afterReceive(Message<?> result, MessageSource<?> source) {
public Message<?> afterReceive(Message<?> result, Object source) {
return result;
}

View File

@@ -263,7 +263,7 @@ The following example shows how to declare a delegating session factory:
IMPORTANT: When you use session caching (see <<ftp-session-caching>>), each of the delegates should be cached.
You cannot cache the `DelegatingSessionFactory` itself.
Starting with _version 5.0.7_, the `DelegatingSessionFactory` can be used in conjunction with a `RotatingServerAdvice` to poll multiple servers; see <<ftp-rotating-server-advice>>.
Starting with version 5.0.7, the `DelegatingSessionFactory` can be used in conjunction with a `RotatingServerAdvice` to poll multiple servers; see <<ftp-rotating-server-advice>>.
[[ftp-inbound]]
=== FTP Inbound Channel Adapter
@@ -704,7 +704,7 @@ Notice that, in this example, the message handler downstream of the transformer
[[ftp-rotating-server-advice]]
=== Inbound Channel Adapters: Polling Multiple Servers and Directories
Starting with _version 5.0.7_, the `RotatingServerAdvice` is available; when configured as a poller advice, the inbound adapters can poll multiple servers and directories.
Starting with version 5.0.7, the `RotatingServerAdvice` is available; when configured as a poller advice, the inbound adapters can poll multiple servers and directories.
Configure the advice and add it to the poller's advice chain as normal.
A `DelegatingSessionFactory` is used to select the server see <<ftp-dsf>> for more information.
The advice configuration consists of a list of `RotationPolicy.KeyDirectory` objects.

View File

@@ -145,18 +145,21 @@ These "`around advice`" methods do not have access to any context for the poll -
This is fine for requirements such as making a task transactional or skipping a poll due to some external condition, as discussed earlier.
What if we wish to take some action depending on the result of the `receive` part of the poll or if we want to adjust the poller depending on conditions? For those instances, Spring Integration offers "`Smart`" Polling.
[[smart-polling]]
===== "`Smart`" Polling
Version 4.2 introduced the `AbstractMessageSourceAdvice`.
Any `Advice` objects in the `advice-chain` that subclass this class are applied only to the receive operation.
Version 5.3 introduced the `ReceiveMessageAdvice` interface.
(The `AbstractMessageSourceAdvice` has been deprecated in favor of `default` methods in the `MessageSourceMutator`.)
Any `Advice` objects in the `advice-chain` that implement this interface are applied only to the receive operation - `MessageSource.receive()` and `PollableChannel.receive(timeout)`.
Therefore they can be applied only for the `SourcePollingChannelAdapter` or `PollingConsumer`.
Such classes implement the following methods:
* `beforeReceive(MessageSource<?> source)`
This method is called before the `MessageSource.receive()` method.
* `beforeReceive(Object source)`
This method is called before the `Object.receive()` method.
It lets you examine and reconfigure the source.
Returning `false` cancels this poll (similar to the `PollSkipAdvice` mentioned earlier).
* `Message<?> afterReceive(Message<?> result, MessageSource<?> source)`
* `Message<?> afterReceive(Message<?> result, Object source)`
This method is called after the `receive()` method.
Again, you can reconfigure the source or take any action (perhaps depending on the result, which can be `null` if there was no message created by the source).
You can even return a different message
@@ -164,7 +167,7 @@ You can even return a different message
.Thread safety
[IMPORTANT]
====
If an advice mutates the `MessageSource`, you should not configure the poller with a `TaskExecutor`.
If an advice mutates the the, you should not configure the poller with a `TaskExecutor`.
If an advice mutates the source, such mutations are not thread safe and could cause unexpected results, especially with high frequency pollers.
If you need to process poll results concurrently, consider using a downstream `ExecutorChannel` instead of adding an executor to the poller.
====
@@ -173,21 +176,22 @@ If you need to process poll results concurrently, consider using a downstream `E
[IMPORTANT]
=====
You should understand how the advice chain is processed during initialization.
`Advice` objects that do not extend `AbstractMessageSourceAdvice` are applied to the whole poll process and are all invoked first, in order, before any `AbstractMessageSourceAdvice`.
Then `AbstractMessageSourceAdvice` objects are invoked in order around the `MessageSource` `receive()` method.
If you have, for example, `Advice` objects `a, b, c, d`, where `b` and `d` are `AbstractMessageSourceAdvice`, the objects are applied in the following order: `a, c, b, d`.
Also, if a `MessageSource` is already a `Proxy`, the `AbstractMessageSourceAdvice` is invoked after any existing `Advice` objects.
`Advice` objects that do not implement `ReceiveMessageAdvice` are applied to the whole poll process and are all invoked first, in order, before any `ReceiveMessageAdvice`.
Then `ReceiveMessageAdvice` objects are invoked in order around the source `receive()` method.
If you have, for example, `Advice` objects `a, b, c, d`, where `b` and `d` are `ReceiveMessageAdvice`, the objects are applied in the following order: `a, c, b, d`.
Also, if a source is already a `Proxy`, the `ReceiveMessageAdvice` is invoked after any existing `Advice` objects.
If you wish to change the order, you must wire up the proxy yourself.
=====
===== `SimpleActiveIdleMessageSourceAdvice`
===== `SimpleActiveIdleReceiveMessageAdvice`
This advice is a simple implementation of `AbstractMessageSourceAdvice`.
(The previous `SimpleActiveIdleMessageSourceAdvice` for only `MessageSource` is deprecated.)
This advice is a simple implementation of `ReceiveMessageAdvice`.
When used in conjunction with a `DynamicPeriodicTrigger`, it adjusts the polling frequency, depending on whether or not the previous poll resulted in a message or not.
The poller must also have a reference to the same `DynamicPeriodicTrigger`.
.Important: Async Handoff
IMPORTANT: `SimpleActiveIdleMessageSourceAdvice` modifies the trigger based on the `receive()` result.
IMPORTANT: `SimpleActiveIdleReceiveMessageAdvice` modifies the trigger based on the `receive()` result.
This works only if the advice is called on the poller thread.
It does not work if the poller has a `task-executor`.
To use this advice where you wish to use async operations after the result of a poll, do the async handoff later, perhaps by using an `ExecutorChannel`.
@@ -241,3 +245,10 @@ IMPORTANT: `CompoundTriggerAdvice` modifies the trigger based on the `receive()`
This works only if the advice is called on the poller thread.
It does not work if the poller has a `task-executor`.
To use this advice where you wish to use async operations after the result of a poll, do the async handoff later, perhaps by using an `ExecutorChannel`.
===== MessageSource-only Advices
Some advices might be applied only for the `MessageSource.receive()` and they don't make sense for `PollableChannel`.
For this purpose a `MessageSourceMutator` interface (an extension of the `ReceiveMessageAdvice`) is still present.
With `default` methods it fully replaces already deprecated `AbstractMessageSourceAdvice` and should be used in those implementations where only `MessageSource` proxying is expected.
See <<./ftp.adoc#ftp-rotating-server-advice,Inbound Channel Adapters: Polling Multiple Servers and Directories>> for more information.

View File

@@ -230,7 +230,7 @@ We added convenience methods so that you can more easily do so from a message fl
IMPORTANT: When using session caching (see <<sftp-session-caching>>), each of the delegates should be cached.
You cannot cache the `DelegatingSessionFactory` itself.
Starting with _version 5.0.7_, the `DelegatingSessionFactory` can be used in conjunction with a `RotatingServerAdvice` to poll multiple servers; see <<sftp-rotating-server-advice>>.
Starting with version 5.0.7, the `DelegatingSessionFactory` can be used in conjunction with a `RotatingServerAdvice` to poll multiple servers; see <<sftp-rotating-server-advice>>.
[[sftp-session-caching]]
=== SFTP Session Caching
@@ -703,7 +703,7 @@ Notice that, in this example, the message handler downstream of the transformer
[[sftp-rotating-server-advice]]
=== Inbound Channel Adapters: Polling Multiple Servers and Directories
Starting with _version 5.0.7_, the `RotatingServerAdvice` is available; when configured as a poller advice, the inbound adapters can poll multiple servers and directories.
Starting with version 5.0.7, the `RotatingServerAdvice` is available; when configured as a poller advice, the inbound adapters can poll multiple servers and directories.
Configure the advice and add it to the poller's advice chain as normal.
A `DelegatingSessionFactory` is used to select the server see <<./ftp.adoc#ftp-dsf,Delegating Session Factory>> for more information.
The advice configuration consists of a list of `RotationPolicy.KeyDirectory` objects.

View File

@@ -66,6 +66,12 @@ The `spring-integration-mongodb` module now provides channel adapter implementat
Also, a reactive implementation for MongoDb change stream support is present with the `MongoDbChangeStreamMessageProducer`.
See <<./mongodb.adoc#mongodb,MongoDB Support>> for more information.
[[x5.3-receive-message-advice]]
==== ReceiveMessageAdvice
A special `ReceiveMessageAdvice` has been introduced to proxy exactly `MessageSource.receive()` or `PollableChannel.receive()`.
See <<./polling-consumer.adoc#smart-polling,Smart Polling>> for more information.
[[x5.3-general]]
=== General Changes