INT-4203: Expression RH Advice Improvements

JIRA: https://jira.spring.io/browse/INT-4203

Add channel names for easier use in DSL.

Add documentation (boot) example.

Normalize Expression Setters; add Javadocs

Polishing - SPR-15091

Tiny code style polishing
This commit is contained in:
Gary Russell
2017-01-03 18:35:34 -05:00
committed by Artem Bilan
parent e2dd2c52d2
commit 77fd8a4684
7 changed files with 310 additions and 52 deletions

View File

@@ -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<Object>(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);

View File

@@ -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<T> extends GenericMessage<T> {
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();
}
}

View File

@@ -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;
}

View File

@@ -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<Advice> adviceChain = new ArrayList<Advice>();
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<Advice> adviceChain = new ArrayList<Advice>();
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<Advice> adviceChain = new ArrayList<Advice>();
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");

View File

@@ -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<String>) (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);
}
}
}

View File

@@ -115,7 +115,7 @@
directory="test">
<file:request-handler-advice-chain>
<bean class="org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice">
<property name="onSuccessExpression" value="@fileWriteLatch.countDown()"/>
<property name="onSuccessExpressionString" value="@fileWriteLatch.countDown()"/>
</bean>
</file:request-handler-advice-chain>
</file:outbound-channel-adapter>

View File

@@ -171,18 +171,18 @@ It also adds an `ExponentialBackoffPolicy` where the first retry waits 1 second,
</int:service-activator>
<bean id="retryTemplate" class="org.springframework.retry.support.RetryTemplate">
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.SimpleRetryPolicy">
<property name="maxAttempts" value="4" />
</bean>
</property>
<property name="backOffPolicy">
<bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
<property name="initialInterval" value="1000" />
<property name="multiplier" value="5.0" />
<property name="maxInterval" value="60000" />
</bean>
</property>
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.SimpleRetryPolicy">
<property name="maxAttempts" value="4" />
</bean>
</property>
<property name="backOffPolicy">
<bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
<property name="initialInterval" value="1000" />
<property name="multiplier" value="5.0" />
<property name="maxInterval" value="60000" />
</bean>
</property>
</bean>
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
</int:service-activator>
<int:handler-retry-advice id="retrier" max-attempts="4" recovery-channel="myErrorChannel">
<int:exponential-back-off initial="1000" multiplier="5.0" maximum="60000" />
<int:exponential-back-off initial="1000" multiplier="5.0" maximum="60000" />
</int:handler-retry-advice>
----
@@ -225,9 +225,9 @@ You can also define the advice directly within the chain:
----
<int:service-activator input-channel="input" ref="failer" method="service">
<int:request-handler-advice-chain>
<int:retry-advice id="retrier" max-attempts="4" recovery-channel="myErrorChannel">
<int:exponential-back-off initial="1000" multiplier="5.0" maximum="60000" />
</int:retry-advice>
<int:retry-advice id="retrier" max-attempts="4" recovery-channel="myErrorChannel">
<int:exponential-back-off initial="1000" multiplier="5.0" maximum="60000" />
</int:retry-advice>
</request-handler-advice-chain>
</int:service-activator>
----
@@ -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<String>) (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]
----
<idempotent-receiver
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>
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.