GH-229: Expose method & methodArgs into ctx from RetryOperationsInterceptor
Fixes: #229 Issue link: https://github.com/spring-projects/spring-retry/issues/229 The logic in the target `RetryPolicy` might be based on the method and its arguments we retry. * Expose `method` & `methodArgs` `RetryContext` attributes from an internal implementation of the `MethodInvocationRetryCallback` in the `RetryOperationsInterceptor` * Document these attributes
This commit is contained in:
17
README.md
17
README.md
@@ -184,6 +184,8 @@ By default, the context is stored in a `ThreadLocal`.
|
||||
JEP 444 recommends that `ThreadLocal` should be avoided when using virtual threads, available in Java 21 and beyond.
|
||||
To store the contexts in a `Map` instead of a `ThreadLocal`, call `RetrySynchronizationManager.setUseThreadLocal(false)`.
|
||||
|
||||
Also, the `RetryOperationsInterceptor` exposes `RetryOperationsInterceptor.METHOD` and `RetryOperationsInterceptor.METHOD_ARGS` attributes with `MethodInvocation.getMethod()` and `new Args(invocation.getArguments())` values, respectively, into the `RetryContext`.
|
||||
|
||||
### Using `RecoveryCallback`
|
||||
|
||||
When a retry is exhausted, the `RetryOperations` can pass control to a different
|
||||
@@ -572,8 +574,8 @@ class Service {
|
||||
}
|
||||
```
|
||||
|
||||
Version 1.2 introduced the ability to use expressions for certain properties. The
|
||||
following example show how to use expressions this way:
|
||||
Version 1.2 introduced the ability to use expressions for certain properties.
|
||||
The following example show how to use expressions this way:
|
||||
|
||||
```java
|
||||
|
||||
@@ -595,17 +597,12 @@ public void service3() {
|
||||
}
|
||||
```
|
||||
|
||||
Since Spring Retry 1.2.5, for `exceptionExpression`, templated expressions (`#{...}`) are
|
||||
deprecated in favor of simple expression strings
|
||||
(`message.contains('this can be retried')`).
|
||||
Since Spring Retry 1.2.5, for `exceptionExpression`, templated expressions (`#{...}`) are deprecated in favor of simple expression strings (`message.contains('this can be retried')`).
|
||||
|
||||
Expressions can contain property placeholders, such as `#{${max.delay}}` or
|
||||
`#{@exceptionChecker.${retry.method}(#root)}`. The following rules apply:
|
||||
Expressions can contain property placeholders, such as `#{${max.delay}}` or `#{@exceptionChecker.${retry.method}(#root)}`. The following rules apply:
|
||||
|
||||
- `exceptionExpression` is evaluated against the thrown exception as the `#root` object.
|
||||
- `maxAttemptsExpression` and the `@BackOff` expression attributes are evaluated once,
|
||||
during initialization. There is no root object for the evaluation but they can reference
|
||||
other beans in the context
|
||||
- `maxAttemptsExpression` and the `@BackOff` expression attributes are evaluated once, during initialization. There is no root object for the evaluation, but they can reference other beans in the context
|
||||
|
||||
Starting with version 2.0, expressions in `@Retryable`, `@CircuitBreaker`, and `BackOff` can be evaluated once, during application initialization, or at runtime.
|
||||
With earlier versions, evaluation was always performed during initialization (except for `Retryable.exceptionExpression` which is always evaluated at runtime).
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.springframework.retry.support.Args;
|
||||
import org.springframework.retry.support.RetrySynchronizationManager;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link MethodInterceptor} that can be used to automatically retry calls to a method
|
||||
@@ -43,6 +42,13 @@ import org.springframework.util.StringUtils;
|
||||
* intercepted is also transactional, then use the ordering hints in the advice
|
||||
* declarations to ensure that this one is before the transaction interceptor in the
|
||||
* advice chain.
|
||||
* <p>
|
||||
* An internal {@link MethodInvocationRetryCallback} implementation exposes a
|
||||
* {@value RetryOperationsInterceptor#METHOD} attribute into the provided
|
||||
* {@link RetryContext} with a value from {@link MethodInvocation#getMethod()}. In
|
||||
* addition, the arguments of this method are exposed into a
|
||||
* {@value RetryOperationsInterceptor#METHOD_ARGS} attribute as an {@link Args} instance
|
||||
* wrapper.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Dave Syer
|
||||
@@ -50,6 +56,18 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class RetryOperationsInterceptor implements MethodInterceptor {
|
||||
|
||||
/**
|
||||
* The {@link RetryContext} attribute name for the
|
||||
* {@link MethodInvocation#getMethod()}.
|
||||
*/
|
||||
public static final String METHOD = "method";
|
||||
|
||||
/**
|
||||
* The {@link RetryContext} attribute name for the
|
||||
* {@code new Args(invocation.getArguments())}.
|
||||
*/
|
||||
public static final String METHOD_ARGS = "methodArgs";
|
||||
|
||||
private RetryOperations retryOperations = new RetryTemplate();
|
||||
|
||||
@Nullable
|
||||
@@ -78,7 +96,11 @@ public class RetryOperationsInterceptor implements MethodInterceptor {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
|
||||
context.setAttribute(RetryContext.NAME, this.label);
|
||||
context.setAttribute("ARGS", new Args(invocation.getArguments()));
|
||||
Args args = new Args(invocation.getArguments());
|
||||
context.setAttribute(METHOD, invocation.getMethod());
|
||||
context.setAttribute(METHOD_ARGS, args);
|
||||
// TODO remove this attribute in the next major/minor version
|
||||
context.setAttribute("ARGS", args);
|
||||
|
||||
/*
|
||||
* If we don't copy the invocation carefully it won't keep a reference to
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.retry.listener.MethodInvocationRetryListenerSupport;
|
||||
import org.springframework.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -53,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
* @author Gary Russell
|
||||
* @author Stéphane Nicoll
|
||||
* @author Henning Pöttker
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class RetryOperationsInterceptorTests {
|
||||
|
||||
@@ -121,6 +123,12 @@ public class RetryOperationsInterceptorTests {
|
||||
this.service.service();
|
||||
assertThat(count).isEqualTo(2);
|
||||
assertThat(this.context.getAttribute(RetryContext.NAME)).isEqualTo("FOO");
|
||||
assertThat(this.context.getAttribute(RetryOperationsInterceptor.METHOD)).isNotNull()
|
||||
.extracting("name")
|
||||
.isEqualTo("service");
|
||||
assertThat(this.context.getAttribute(RetryOperationsInterceptor.METHOD_ARGS)).isNotNull()
|
||||
.extracting("args")
|
||||
.isEqualTo(new Object[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -206,13 +214,13 @@ public class RetryOperationsInterceptorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutsideTransaction() throws Exception {
|
||||
public void testOutsideTransaction() {
|
||||
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
|
||||
ClassUtils.addResourcePathToPackagePath(getClass(), "retry-transaction-test.xml"));
|
||||
Object object = context.getBean("bean");
|
||||
assertThat(object).isInstanceOf(Service.class);
|
||||
Service bean = (Service) object;
|
||||
bean.doTansactional();
|
||||
bean.doTransactional();
|
||||
assertThat(count).isEqualTo(2);
|
||||
// Expect 2 separate transactions...
|
||||
assertThat(transactionCount).isEqualTo(2);
|
||||
@@ -220,7 +228,7 @@ public class RetryOperationsInterceptorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIllegalMethodInvocationType() throws Throwable {
|
||||
public void testIllegalMethodInvocationType() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.interceptor.invoke(new MethodInvocation() {
|
||||
@Override
|
||||
public Method getMethod() {
|
||||
@@ -253,7 +261,7 @@ public class RetryOperationsInterceptorTests {
|
||||
|
||||
void service() throws Exception;
|
||||
|
||||
void doTansactional();
|
||||
void doTransactional();
|
||||
|
||||
}
|
||||
|
||||
@@ -269,18 +277,18 @@ public class RetryOperationsInterceptorTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void doTansactional() {
|
||||
public void doTransactional() {
|
||||
if (TransactionSynchronizationManager.isActualTransactionActive() && !this.enteredTransaction) {
|
||||
transactionCount++;
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new org.springframework.transaction.support.TransactionSynchronizationAdapter() {
|
||||
@Override
|
||||
public void beforeCompletion() {
|
||||
ServiceImpl.this.enteredTransaction = false;
|
||||
}
|
||||
});
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
|
||||
@Override
|
||||
public void beforeCompletion() {
|
||||
ServiceImpl.this.enteredTransaction = false;
|
||||
}
|
||||
|
||||
});
|
||||
this.enteredTransaction = true;
|
||||
}
|
||||
count++;
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:aop="http://www.springframework.org/schema/aop"
|
||||
xmlns:tx="http://www.springframework.org/schema/tx"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
|
||||
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
|
||||
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
|
||||
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">
|
||||
|
||||
<aop:config>
|
||||
<aop:pointcut id="transactional"
|
||||
expression="execution(* org.springframework..RetryOperationsInterceptorTests.Service.doTansactional(..))" />
|
||||
expression="execution(* org.springframework..RetryOperationsInterceptorTests.Service.doTransactional(..))" />
|
||||
<aop:advisor pointcut-ref="transactional"
|
||||
advice-ref="retryAdvice" order="-1"/>
|
||||
<aop:advisor pointcut-ref="transactional" advice-ref="txAdvice" order="0"/>
|
||||
@@ -23,7 +22,7 @@
|
||||
<bean id="retryAdvice"
|
||||
class="org.springframework.retry.interceptor.RetryOperationsInterceptor"/>
|
||||
|
||||
<tx:advice id="txAdvice" transaction-manager="transactionManager">
|
||||
<tx:advice id="txAdvice">
|
||||
<tx:attributes>
|
||||
<tx:method name="*" />
|
||||
</tx:attributes>
|
||||
|
||||
Reference in New Issue
Block a user