/* * Copyright 2002-2017 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.handler; import java.io.Serializable; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import org.aopalliance.aop.Advice; import org.springframework.aop.framework.ProxyFactory; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.store.MessageGroup; import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.MessageStore; import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.management.IntegrationManagedResource; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.MessagingException; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** * A {@link MessageHandler} that is capable of delaying the continuation of a * Message flow based on the result of evaluation {@code delayExpression} on an inbound {@link Message} * or a default delay value configured on this handler. Note that the * continuation of the flow is delegated to a {@link TaskScheduler}, and * therefore, the calling thread does not block. The advantage of this approach * is that many delays can be managed concurrently, even very long delays, * without producing a buildup of blocked Threads. * <p> * One thing to keep in mind, however, is that any active transactional context * will not propagate from the original sender to the eventual recipient. This * is a side-effect of passing the Message to the output channel after the * delay with a different Thread in control. * <p> * When this handler's {@code delayExpression} property is configured, that evaluation result value * will take precedence over the handler's {@code defaultDelay} value. * The actual evaluation result value may be a long, a String that can be parsed * as a long, or a Date. If it is a long, it will be interpreted as the length * of time to delay in milliseconds counting from the current time (e.g. a * value of 5000 indicates that the Message can be released as soon as five * seconds from the current time). If the value is a Date, it will be * delayed at least until that Date occurs (i.e. the delay in that case is * equivalent to {@code headerDate.getTime() - new Date().getTime()}). * * @author Mark Fisher * @author Artem Bilan * @author Gary Russell * * @since 1.0.3 */ @ManagedResource @IntegrationManagedResource public class DelayHandler extends AbstractReplyProducingMessageHandler implements DelayHandlerManagement, ApplicationListener<ContextRefreshedEvent> { private final String messageGroupId; private volatile long defaultDelay; private Expression delayExpression; private volatile boolean ignoreExpressionFailures = true; private volatile MessageGroupStore messageStore; private volatile List<Advice> delayedAdviceChain; private final AtomicBoolean initialized = new AtomicBoolean(); private volatile MessageHandler releaseHandler = new ReleaseMessageHandler(); private EvaluationContext evaluationContext; /** * Create a DelayHandler with the given 'messageGroupId' that is used as 'key' for {@link MessageGroup} * to store delayed Messages in the {@link MessageGroupStore}. The sending of Messages after * the delay will be handled by registered in the ApplicationContext default {@link ThreadPoolTaskScheduler}. * * @param messageGroupId The message group identifier. * * @see IntegrationObjectSupport#getTaskScheduler() */ public DelayHandler(String messageGroupId) { Assert.notNull(messageGroupId, "'messageGroupId' must not be null"); this.messageGroupId = messageGroupId; } /** * Create a DelayHandler with the given default delay. The sending of Messages * after the delay will be handled by the provided {@link TaskScheduler}. * * @param messageGroupId The message group identifier. * @param taskScheduler A task scheduler. */ public DelayHandler(String messageGroupId, TaskScheduler taskScheduler) { this(messageGroupId); this.setTaskScheduler(taskScheduler); } /** * Set the default delay in milliseconds. If no {@code delayExpression} property * has been provided, the default delay will be applied to all Messages. If * a delay should <em>only</em> be applied to Messages with evaluation result from * {@code delayExpression}, then set this value to 0. * * @param defaultDelay The default delay in milliseconds. */ public void setDefaultDelay(long defaultDelay) { this.defaultDelay = defaultDelay; } /** * Specify the {@link Expression} that should be checked for a delay period (in * milliseconds) or a Date to delay until. If this property is set, the result of the * expression evaluation (if not null) will take precedence over this handler's * default delay. * * @param delayExpression The delay expression. */ public void setDelayExpression(Expression delayExpression) { this.delayExpression = delayExpression; } /** * Specify the {@code Expression} that should be checked for a delay period (in * milliseconds) or a Date to delay until. If this property is set, the result of the * expression evaluation (if not null) will take precedence over this handler's * default delay. * * @param delayExpression The delay expression. * @since 5.0 */ public void setDelayExpressionString(String delayExpression) { this.delayExpression = EXPRESSION_PARSER.parseExpression(delayExpression); } /** * Specify whether {@code Exceptions} thrown by {@link #delayExpression} evaluation should be * ignored (only logged). In this case case the delayer will fall back to the * to the {@link #defaultDelay}. * If this property is specified as {@code false}, any {@link #delayExpression} evaluation * {@code Exception} will be thrown to the caller without falling back to the to the {@link #defaultDelay}. * Default is {@code true}. * * @param ignoreExpressionFailures true if expression evaluation failures should be ignored. * * @see #determineDelayForMessage */ public void setIgnoreExpressionFailures(boolean ignoreExpressionFailures) { this.ignoreExpressionFailures = ignoreExpressionFailures; } /** * Specify the {@link MessageGroupStore} that should be used to store Messages * while awaiting the delay. * * @param messageStore The message store. */ public void setMessageStore(MessageGroupStore messageStore) { Assert.state(messageStore != null, "MessageStore must not be null"); this.messageStore = messageStore; } /** * Specify the {@code List<Advice>} to advise {@link DelayHandler.ReleaseMessageHandler} proxy. * Usually used to add transactions to delayed messages retrieved from a transactional message store. * * @param delayedAdviceChain The advice chain. * * @see #createReleaseMessageTask */ public void setDelayedAdviceChain(List<Advice> delayedAdviceChain) { Assert.notNull(delayedAdviceChain, "delayedAdviceChain must not be null"); this.delayedAdviceChain = delayedAdviceChain; } @Override public String getComponentType() { return "delayer"; } @Override protected void doInit() { if (this.messageStore == null) { this.messageStore = new SimpleMessageStore(); } else { Assert.isInstanceOf(MessageStore.class, this.messageStore); } this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory()); this.releaseHandler = this.createReleaseMessageTask(); } private MessageHandler createReleaseMessageTask() { ReleaseMessageHandler releaseHandler = new ReleaseMessageHandler(); if (!CollectionUtils.isEmpty(this.delayedAdviceChain)) { ProxyFactory proxyFactory = new ProxyFactory(releaseHandler); for (Advice advice : this.delayedAdviceChain) { proxyFactory.addAdvice(advice); } return (MessageHandler) proxyFactory.getProxy(getApplicationContext().getClassLoader()); } return releaseHandler; } @Override protected boolean shouldCopyRequestHeaders() { return false; } /** * Checks if 'requestMessage' wasn't delayed before * ({@link #releaseMessageAfterDelay} and {@link DelayHandler.DelayedMessageWrapper}). * Than determine 'delay' for 'requestMessage' ({@link #determineDelayForMessage}) * and if {@code delay > 0} schedules 'releaseMessage' task after 'delay'. * * @param requestMessage - the Message which may be delayed. * @return - {@code null} if 'requestMessage' is delayed, * otherwise - 'payload' from 'requestMessage'. * @see #releaseMessage */ @Override protected Object handleRequestMessage(Message<?> requestMessage) { boolean delayed = requestMessage.getPayload() instanceof DelayedMessageWrapper; if (!delayed) { long delay = this.determineDelayForMessage(requestMessage); if (delay > 0) { this.releaseMessageAfterDelay(requestMessage, delay); return null; } } // no delay return delayed ? ((DelayedMessageWrapper) requestMessage.getPayload()).getOriginal() : requestMessage; } private long determineDelayForMessage(Message<?> message) { DelayedMessageWrapper delayedMessageWrapper = null; if (message.getPayload() instanceof DelayedMessageWrapper) { delayedMessageWrapper = (DelayedMessageWrapper) message.getPayload(); } long delay = this.defaultDelay; if (this.delayExpression != null) { Exception delayValueException = null; Object delayValue = null; try { delayValue = this.delayExpression.getValue(this.evaluationContext, delayedMessageWrapper != null ? delayedMessageWrapper.getOriginal() : message); } catch (EvaluationException e) { delayValueException = e; } if (delayValue instanceof Date) { long current = delayedMessageWrapper != null ? delayedMessageWrapper.getRequestDate() : System.currentTimeMillis(); delay = ((Date) delayValue).getTime() - current; } else if (delayValue != null) { try { delay = Long.valueOf(delayValue.toString()); } catch (NumberFormatException e) { delayValueException = e; } } if (delayValueException != null) { if (this.ignoreExpressionFailures) { if (logger.isDebugEnabled()) { logger.debug("Failed to get delay value from 'delayExpression': " + delayValueException.getMessage() + ". Will fall back to default delay: " + this.defaultDelay); } } else { throw new MessageHandlingException(message, "Error occurred during 'delay' value determination", delayValueException); } } } return delay; } private void releaseMessageAfterDelay(final Message<?> message, long delay) { Message<?> delayedMessage = message; DelayedMessageWrapper messageWrapper = null; if (message.getPayload() instanceof DelayedMessageWrapper) { messageWrapper = (DelayedMessageWrapper) message.getPayload(); } else { messageWrapper = new DelayedMessageWrapper(message, System.currentTimeMillis()); delayedMessage = getMessageBuilderFactory() .withPayload(messageWrapper) .copyHeaders(message.getHeaders()) .build(); this.messageStore.addMessageToGroup(this.messageGroupId, delayedMessage); } Runnable releaseTask; if (this.messageStore instanceof SimpleMessageStore) { final Message<?> messageToSchedule = delayedMessage; releaseTask = () -> releaseMessage(messageToSchedule); } else { final UUID messageId = delayedMessage.getHeaders().getId(); releaseTask = () -> { Message<?> messageToRelease = getMessageById(messageId); if (messageToRelease != null) { releaseMessage(messageToRelease); } }; } getTaskScheduler().schedule(releaseTask, new Date(messageWrapper.getRequestDate() + delay)); } private Message<?> getMessageById(UUID messageId) { Message<?> theMessage = ((MessageStore) this.messageStore).getMessage(messageId); if (theMessage == null) { if (logger.isDebugEnabled()) { logger.debug("No message in the Message Store for id: " + messageId + ". Likely another instance has already released it."); } return null; } else { return theMessage; } } private void releaseMessage(Message<?> message) { this.releaseHandler.handleMessage(message); } private void doReleaseMessage(Message<?> message) { if (removeDelayedMessageFromMessageStore(message)) { if (!(this.messageStore instanceof SimpleMessageStore)) { this.messageStore.removeMessagesFromGroup(this.messageGroupId, message); } this.handleMessageInternal(message); } else { if (logger.isDebugEnabled()) { logger.debug("No message in the Message Store to release: " + message + ". Likely another instance has already released it."); } } } private boolean removeDelayedMessageFromMessageStore(Message<?> message) { if (this.messageStore instanceof SimpleMessageStore) { synchronized (this.messageGroupId) { Collection<Message<?>> messages = this.messageStore.getMessageGroup(this.messageGroupId).getMessages(); if (messages.contains(message)) { this.messageStore.removeMessagesFromGroup(this.messageGroupId, message); return true; } else { return false; } } } else { return ((MessageStore) this.messageStore).removeMessage(message.getHeaders().getId()) != null; } } @Override public int getDelayedMessageCount() { return this.messageStore.messageGroupSize(this.messageGroupId); } /** * Used for reading persisted Messages in the 'messageStore' * to reschedule them e.g. upon application restart. * The logic is based on iteration over {@code messageGroup.getMessages()} * and schedules task for 'delay' logic. * This behavior is dictated by the avoidance of invocation thread overload. */ @Override public synchronized void reschedulePersistedMessages() { MessageGroup messageGroup = this.messageStore.getMessageGroup(this.messageGroupId); for (final Message<?> message : messageGroup.getMessages()) { getTaskScheduler().schedule((Runnable) () -> { // This is fine to keep the reference to the message, // because the scheduled task is performed immediately. long delay = determineDelayForMessage(message); if (delay > 0) { releaseMessageAfterDelay(message, delay); } else { releaseMessage(message); } }, new Date()); } } /** * Handles {@link ContextRefreshedEvent} to invoke {@link #reschedulePersistedMessages} * as late as possible after application context startup. * Also it checks {@link #initialized} to ignore * other {@link ContextRefreshedEvent}s which may be published * in the 'parent-child' contexts, e.g. in the Spring-MVC applications. * * @param event - {@link ContextRefreshedEvent} which occurs * after Application context is completely initialized. * * @see #reschedulePersistedMessages */ @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (!this.initialized.getAndSet(true)) { this.reschedulePersistedMessages(); } } /** * Delegate {@link MessageHandler} implementation for 'release Message task'. * Used as 'pointcut' to wrap 'release Message task' with <code>adviceChain</code>. * * @see @createReleaseMessageTask * @see @releaseMessage */ private class ReleaseMessageHandler implements MessageHandler { ReleaseMessageHandler() { super(); } @Override public void handleMessage(Message<?> message) throws MessagingException { DelayHandler.this.doReleaseMessage(message); } } public static final class DelayedMessageWrapper implements Serializable { private static final long serialVersionUID = -4739802369074947045L; private final long requestDate; private final Message<?> original; DelayedMessageWrapper(Message<?> original, long requestDate) { this.original = original; this.requestDate = requestDate; } public long getRequestDate() { return this.requestDate; } public Message<?> getOriginal() { return this.original; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DelayedMessageWrapper that = (DelayedMessageWrapper) o; return this.original.equals(that.original); } @Override public int hashCode() { return this.original.hashCode(); } } }