diff --git a/README.md b/README.md index 6cd78528..d1c35af2 100644 --- a/README.md +++ b/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). diff --git a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java index 2fa20977..a8eba86d 100644 --- a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java @@ -29,7 +29,6 @@ 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 @@ * 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. + *

+ * 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 @@ */ 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 Object invoke(final MethodInvocation invocation) throws Throwable { 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 diff --git a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java index da688a2d..887d9027 100644 --- a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java +++ b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java @@ -40,6 +40,7 @@ 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 @@ * @author Gary Russell * @author Stéphane Nicoll * @author Henning Pöttker + * @author Artem Bilan */ public class RetryOperationsInterceptorTests { @@ -121,6 +123,12 @@ public void testDefaultInterceptorWithLabel() throws Exception { 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 void testRetryExceptionAfterTooManyAttempts() { } @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 void testOutsideTransaction() throws Exception { } @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 static interface Service { void service() throws Exception; - void doTansactional(); + void doTransactional(); } @@ -269,18 +277,18 @@ public void service() throws Exception { } } - @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++; diff --git a/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml b/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml index 83788d94..a0cd7c26 100644 --- a/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml +++ b/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml @@ -2,16 +2,15 @@ + 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"> + expression="execution(* org.springframework..RetryOperationsInterceptorTests.Service.doTransactional(..))" /> @@ -23,7 +22,7 @@ - +