diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdvice.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdvice.java index 65b039c8f3..350855fb32 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdvice.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -28,7 +28,6 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.support.ErrorMessage; -import org.springframework.util.Assert; /** * Used to advise {@link MessageHandler}s. @@ -50,10 +49,14 @@ public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHan private volatile MessageChannel successChannel; + private volatile String successChannelName; + private volatile Expression onFailureExpression; private volatile MessageChannel failureChannel; + private volatile String failureChannelName; + private final MessagingTemplate messagingTemplate = new MessagingTemplate(); private volatile boolean trapException = false; @@ -64,34 +67,106 @@ public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHan private volatile EvaluationContext evaluationContext; - public void setOnSuccessExpression(String onSuccessExpression) { - Assert.notNull(onSuccessExpression, "'onSuccessExpression' must not be null"); + /** + * Set the expression to evaluate against the message after a successful + * handler invocation. + * @param onSuccessExpression the SpEL expression. + * @since 4.3.7 + */ + public void setOnSuccessExpressionString(String onSuccessExpression) { this.onSuccessExpression = new SpelExpressionParser().parseExpression(onSuccessExpression); } + /** + * Set the expression to evaluate against the message after a successful + * handler invocation. + * @param onSuccessExpression the SpEL expression. + * @since 5.0 + */ + public void setOnSuccessExpression(Expression onSuccessExpression) { + this.onSuccessExpression = onSuccessExpression; + } + + /** + * Set the expression to evaluate against the message after a successful + * handler invocation. + * @param onSuccessExpression the SpEL expression. + * @deprecated in favor of {@link #setOnSuccessExpression(Expression)} + */ + @Deprecated public void setExpressionOnSuccess(Expression onSuccessExpression) { this.onSuccessExpression = onSuccessExpression; } - public void setOnFailureExpression(String onFailureExpression) { - Assert.notNull(onFailureExpression, "'onFailureExpression' must not be null"); + /** + * Set the expression to evaluate against the root message after a failed + * handler invocation. The exception is available as the variable {@code #exception} + * @param onFailureExpression the SpEL expression. + * @since 4.3.7 + */ + public void setOnFailureExpressionString(String onFailureExpression) { this.onFailureExpression = new SpelExpressionParser().parseExpression(onFailureExpression); } + /** + * Set the expression to evaluate against the root message after a failed + * handler invocation. The exception is available as the variable {@code #exception} + * @param onFailureExpression the SpEL expression. + * @since 5.0 + */ + public void setOnFailureExpression(Expression onFailureExpression) { + this.onFailureExpression = onFailureExpression; + } + + /** + * Set the expression to evaluate against the root message after a failed + * handler invocation. The exception is available as the variable {@code #exception} + * @param onFailureExpression the SpEL expression. + * @deprecated in favor of {@link #setOnFailureExpression(Expression)} + */ + @Deprecated public void setExpressionOnFailure(Expression onFailureExpression) { this.onFailureExpression = onFailureExpression; } + /** + * Set the channel to which to send the {@link AdviceMessage} after evaluating the + * success expression. + * @param successChannel the channel. + */ public void setSuccessChannel(MessageChannel successChannel) { - Assert.notNull(successChannel, "'successChannel' must not be null"); this.successChannel = successChannel; } + /** + * Set the channel name to which to send the {@link AdviceMessage} after evaluating + * the success expression. + * @param successChannelName the channel name. + * @since 4.3.7 + */ + public void setSuccessChannelName(String successChannelName) { + this.successChannelName = successChannelName; + } + + /** + * Set the channel to which to send the {@link ErrorMessage} after evaluating the + * failure expression. + * @param failureChannel the channel. + */ public void setFailureChannel(MessageChannel failureChannel) { - Assert.notNull(failureChannel, "'failureChannel' must not be null"); this.failureChannel = failureChannel; } + /** + * Set the channel name to which to send the {@link ErrorMessage} after evaluating the + * failure expression. + * @param failureChannelName the channel name. + * @since 4.3.7 + */ + public void setFailureChannelName(String failureChannelName) { + this.failureChannelName = failureChannelName; + } + /** * If true, any exception will be caught and null returned. * Default false. @@ -112,10 +187,12 @@ public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHan } /** - * If true and an onSuccess expression evaluation fails with an exception, the exception will be thrown to the - * caller. If false, the exception is caught. Default false. Ignored for onFailure expression evaluation - the - * original exception will be propagated (unless trapException is true). - * @param propagateOnSuccessEvaluationFailures The propagateOnSuccessEvaluationFailures to set. + * If true and an onSuccess expression evaluation fails with an exception, the + * exception will be thrown to the caller. If false, the exception is caught. Default + * false. Ignored for onFailure expression evaluation - the original exception will be + * propagated (unless trapException is true). + * @param propagateOnSuccessEvaluationFailures The + * propagateOnSuccessEvaluationFailures to set. */ public void setPropagateEvaluationFailures(boolean propagateOnSuccessEvaluationFailures) { this.propagateOnSuccessEvaluationFailures = propagateOnSuccessEvaluationFailures; @@ -163,6 +240,9 @@ public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHan evalResult = e; evaluationFailed = true; } + if (this.successChannel == null && this.successChannelName != null && getChannelResolver() != null) { + this.successChannel = getChannelResolver().resolveDestination(this.successChannelName); + } if (evalResult != null && this.successChannel != null) { AdviceMessage resultMessage = new AdviceMessage(evalResult, message); this.messagingTemplate.send(this.successChannel, resultMessage); @@ -181,6 +261,9 @@ public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHan evalResult = e; logger.error("Failure expression evaluation failed for " + message + ": " + e.getMessage()); } + if (this.failureChannel == null && this.failureChannelName != null && getChannelResolver() != null) { + this.failureChannel = getChannelResolver().resolveDestination(this.failureChannelName); + } if (evalResult != null && this.failureChannel != null) { MessagingException messagingException = new MessageHandlingExpressionEvaluatingAdviceException(message, "Handler Failed", this.unwrapThrowableIfNecessary(exception), evalResult); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/message/AdviceMessage.java b/spring-integration-core/src/main/java/org/springframework/integration/message/AdviceMessage.java index 67dd3ec339..41e1575ac5 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/message/AdviceMessage.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/message/AdviceMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -53,4 +53,12 @@ public class AdviceMessage extends GenericMessage { return this.inputMessage; } + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + builder.setLength(builder.length() - 1); + builder.append(", inputMessage=").append(this.inputMessage.toString()).append("]"); + return builder.toString(); + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java index 1361470225..04b7561896 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java @@ -473,7 +473,7 @@ public class IntegrationFlowTests { @Bean public Advice expressionAdvice() { ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice(); - advice.setOnSuccessExpression("payload"); + advice.setOnSuccessExpressionString("payload"); advice.setSuccessChannel(this.successChannel); return advice; } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java index 00b3b72635..a01987665b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -143,8 +143,8 @@ public class AdvisedMessageHandlerTests { advice.setBeanFactory(mock(BeanFactory.class)); advice.setSuccessChannel(successChannel); advice.setFailureChannel(failureChannel); - advice.setOnSuccessExpression("'foo'"); - advice.setOnFailureExpression("'bar:' + #exception.message"); + advice.setOnSuccessExpressionString("'foo'"); + advice.setOnFailureExpressionString("'bar:' + #exception.message"); List adviceChain = new ArrayList(); adviceChain.add(advice); @@ -237,8 +237,8 @@ public class AdvisedMessageHandlerTests { advice.setBeanFactory(mock(BeanFactory.class)); advice.setSuccessChannel(successChannel); advice.setFailureChannel(failureChannel); - advice.setOnSuccessExpression("1/0"); - advice.setOnFailureExpression("1/0"); + advice.setOnSuccessExpressionString("1/0"); + advice.setOnFailureExpressionString("1/0"); List adviceChain = new ArrayList(); adviceChain.add(advice); @@ -301,8 +301,8 @@ public class AdvisedMessageHandlerTests { advice.setBeanFactory(mock(BeanFactory.class)); advice.setSuccessChannel(successChannel); advice.setFailureChannel(failureChannel); - advice.setOnSuccessExpression("1/0"); - advice.setOnFailureExpression("1/0"); + advice.setOnSuccessExpressionString("1/0"); + advice.setOnFailureExpressionString("1/0"); List adviceChain = new ArrayList(); adviceChain.add(advice); @@ -722,7 +722,7 @@ public class AdvisedMessageHandlerTests { ExpressionEvaluatingRequestHandlerAdvice expressionAdvice = new ExpressionEvaluatingRequestHandlerAdvice(); expressionAdvice.setBeanFactory(mock(BeanFactory.class)); // MessagingException / RuntimeException - expressionAdvice.setOnFailureExpression("#exception.cause.message"); + expressionAdvice.setOnFailureExpressionString("#exception.cause.message"); expressionAdvice.setReturnFailureExpressionResult(true); final AtomicInteger outerCounter = new AtomicInteger(); adviceChain.add(new AbstractRequestHandlerAdvice() { @@ -769,7 +769,7 @@ public class AdvisedMessageHandlerTests { ExpressionEvaluatingRequestHandlerAdvice expressionAdvice = new ExpressionEvaluatingRequestHandlerAdvice(); expressionAdvice.setBeanFactory(mock(BeanFactory.class)); - expressionAdvice.setOnFailureExpression("#exception.message"); + expressionAdvice.setOnFailureExpressionString("#exception.message"); expressionAdvice.setFailureChannel(errors); adviceChain.add(new RequestHandlerRetryAdvice()); @@ -849,7 +849,7 @@ public class AdvisedMessageHandlerTests { ExpressionEvaluatingRequestHandlerAdvice expressionAdvice = new ExpressionEvaluatingRequestHandlerAdvice(); expressionAdvice.setBeanFactory(mock(BeanFactory.class)); - expressionAdvice.setOnFailureExpression("'foo'"); + expressionAdvice.setOnFailureExpressionString("'foo'"); expressionAdvice.setFailureChannel(errors); Throwable theThrowable = new Throwable("foo"); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java new file mode 100644 index 0000000000..f59fc01845 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 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.advice; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; + +import org.aopalliance.aop.Advice; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.handler.GenericHandler; +import org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice.MessageHandlingExpressionEvaluatingAdviceException; +import org.springframework.integration.message.AdviceMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Gary Russell + * @since 5.0 + * + */ +@RunWith(SpringRunner.class) +public class ExpressionEvaluatingRequestHandlerAdviceTests { + + @Autowired + @Qualifier("advised.input") + private MessageChannel in; + + @Autowired + private EERHAConfig config; + + @Test + public void test() { + this.in.send(new GenericMessage<>("good")); + this.in.send(new GenericMessage<>("junk")); + assertThat(config.successful, instanceOf(AdviceMessage.class)); + assertThat(config.successful.getPayload(), equalTo("good was successful")); + assertThat(config.failed, instanceOf(ErrorMessage.class)); + Object evaluationResult = ((MessageHandlingExpressionEvaluatingAdviceException) config.failed.getPayload()) + .getEvaluationResult(); + assertThat((String) evaluationResult, startsWith("junk was bad, with reason:")); + } + + @Configuration + @EnableIntegration + public static class EERHAConfig { + + @Bean + public IntegrationFlow advised() { + return f -> f.handle((GenericHandler) (payload, headers) -> { + if (payload.equals("good")) { + return null; + } + else { + throw new RuntimeException("some failure"); + } + }, c -> c.advice(expressionAdvice())); + } + + @Bean + public Advice expressionAdvice() { + ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice(); + advice.setSuccessChannelName("success.input"); + advice.setOnSuccessExpressionString("payload + ' was successful'"); + advice.setFailureChannelName("failure.input"); + advice.setOnFailureExpressionString( + "payload + ' was bad, with reason: ' + #exception.cause.message"); + advice.setTrapException(true); + return advice; + } + + private Message successful; + + @Bean + public IntegrationFlow success() { + return f -> f + .handle(m -> this.successful = m); + } + + private Message failed; + + @Bean + public IntegrationFlow failure() { + return f -> f + .handle(m -> this.failed = m); + } + + } + +} diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml index abdcbcfd97..7fea9b0dbc 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml @@ -115,7 +115,7 @@ directory="test"> - + diff --git a/src/reference/asciidoc/handler-advice.adoc b/src/reference/asciidoc/handler-advice.adoc index 26244921a5..a65b8481d7 100644 --- a/src/reference/asciidoc/handler-advice.adoc +++ b/src/reference/asciidoc/handler-advice.adoc @@ -171,18 +171,18 @@ It also adds an `ExponentialBackoffPolicy` where the first retry waits 1 second, - - - - - - - - - - - - + + + + + + + + + + + + 27.058 DEBUG [task-scheduler-1]preSend on channel 'input', message: [Payload=...] @@ -214,7 +214,7 @@ Starting with _version 4.0_, the above configuration can be greatly simplified w - + ---- @@ -225,9 +225,9 @@ You can also define the advice directly within the chain: ---- - - - + + + ---- @@ -391,6 +391,57 @@ When an exception is thrown in the scope of the advice, by default, that excepti `failureExpression` is evaluated. If you wish to suppress throwing the exception, set the `trapException` property to `true`. +.Example - Configuring the Advice with Java DSL +[source, java] +---- +@SpringBootApplication +public class EerhaApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(EerhaApplication.class, args); + MessageChannel in = context.getBean("advised.input", MessageChannel.class); + in.send(new GenericMessage<>("good")); + in.send(new GenericMessage<>("bad")); + context.close(); + } + + @Bean + public IntegrationFlow advised() { + return f -> f.handle((GenericHandler) (payload, headers) -> { + if (payload.equals("good")) { + return null; + } + else { + throw new RuntimeException("some failure"); + } + }, c -> c.advice(expressionAdvice())); + } + + @Bean + public Advice expressionAdvice() { + ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice(); + advice.setSuccessChannelName("success.input"); + advice.setOnSuccessExpression("payload + ' was successful'"); + advice.setFailureChannelName("failure.input"); + advice.setOnFailureExpression( + "payload + ' was bad, with reason: ' + #exception.cause.message"); + advice.setTrapException(true); + return advice; + } + + @Bean + public IntegrationFlow success() { + return f -> f.handle(System.out::println); + } + + @Bean + public IntegrationFlow failure() { + return f -> f.handle(System.out::println); + } + +} +---- + [[custom-advice]] ==== Custom Advice Classes @@ -586,7 +637,7 @@ public class MyAdvisedFilter { @Filter(inputChannel="input", outputChannel="output", adviceChain="adviceChain", discardWithinAdvice="false") public boolean filter(String s) { - return s.contains("good"); + return s.contains("good"); } } ---- @@ -665,16 +716,16 @@ For convenience, the `MetadataStoreSelector` options are configurable directly o [source,xml] ---- - endpoint="" <2> - selector="" <3> - discard-channel="" <4> - metadata-store="" <5> - key-strategy="" <6> - key-expression="" <7> - value-strategy="" <8> - value-expression="" <9> - throw-exception-on-rejection="" /> <10> + id="" <1> + endpoint="" <2> + selector="" <3> + discard-channel="" <4> + metadata-store="" <5> + key-strategy="" <6> + key-expression="" <7> + value-strategy="" <8> + value-expression="" <9> + throw-exception-on-rejection="" /> <10> ---- <1> The id of the `IdempotentReceiverInterceptor` bean.