Expose the MethodInvocation in the MethodInvocationRetryCallback in order to permit to concrete RetryListener implementations to inspect in detail the method called as well as its arguments. This information can be used in the retry listener for either detailed logging or monitoring for the proxied method invocations.

This commit is contained in:
Marius Grama
2019-04-13 15:09:44 +02:00
committed by Dave Syer
parent c530b686b0
commit b599c23404
8 changed files with 356 additions and 27 deletions

View File

@@ -216,6 +216,35 @@ The open and close callbacks come before and after the entire retry in the simpl
Note that when there is more than one listener, they are in a list, so there is an order. In this case open will be called in the same order while onError and close will be called in reverse order.
### Listeners for reflective method invocations
When dealing with methods annotated with `@Retryable` or with Spring AOP intercepted methods,
spring-retry provides the possibility to inspect in detail the method invocation within the
`RetryListener` implementation. Such a scenario could be particularly useful when there is a need
to monitor how often a certain method call has been retried and expose it with detailed tagging
information (e.g. : class name, method name, or even parameter values in some exotic cases).
All that needs to be done is checking whe
```java
template.registerListener(new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doClose(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
monitoringTags.put(labelTagName, callback.getLabel());
Method method = callback.getInvocation()
.getMethod();
monitoringTags.put(classTagName,
method.getDeclaringClass().getSimpleName());
monitoringTags.put(methodTagName, method.getName());
// register a monitoring counter with appropriate tags
// ...
}
});
```
## Declarative Retry
Sometimes there is some business processing that you know you want to retry every time it happens. The classic example of this is the remote service call. Spring Retry provides an AOP interceptor that wraps a method call in a `RetryOperations` for just this purpose. The `RetryOperationsInterceptor` executes the intercepted method and retries on failure according to the `RetryPolicy` in the provided `RepeatTemplate`.

View File

@@ -218,6 +218,12 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>

View File

@@ -0,0 +1,53 @@
package org.springframework.retry.interceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryOperations;
import org.springframework.util.StringUtils;
/**
* Callback class for a Spring AOP reflective `MethodInvocation` that can be retried using a {@link
* RetryOperations}.
*
* In a concrete {@link org.springframework.retry.RetryListener} implementation, the
* `MethodInvocation` can be analysed for providing insights on the method called as well as its
* parameter values which could then be used for monitoring purposes.
*
* @param <T> the type of object returned by the callback
* @param <E> the type of exception it declares may be thrown
* @see StatefulRetryOperationsInterceptor
* @see RetryOperationsInterceptor
* @see org.springframework.retry.listener.MethodInvocationRetryListenerSupport
* @since 1.3
*/
public abstract class MethodInvocationRetryCallback<T, E extends Throwable>
implements RetryCallback<T, E> {
protected final MethodInvocation invocation;
protected final String label;
/**
* Constructor for the class.
*
* @param invocation the method invocation
* @param label a unique label for statistics reporting.
*/
public MethodInvocationRetryCallback(MethodInvocation invocation, String label) {
this.invocation = invocation;
if (StringUtils.hasText(label)) {
this.label = label;
} else {
this.label = invocation.getMethod().toGenericString();
}
}
public MethodInvocation getInvocation() {
return invocation;
}
public String getLabel() {
return label;
}
}

View File

@@ -75,7 +75,8 @@ public class RetryOperationsInterceptor implements MethodInterceptor {
}
final String label = name;
RetryCallback<Object, Throwable> retryCallback = new RetryCallback<Object, Throwable>() {
RetryCallback<Object, Throwable> retryCallback = new MethodInvocationRetryCallback<Object, Throwable>(
invocation, label) {
public Object doWithRetry(RetryContext context) throws Exception {

View File

@@ -168,7 +168,7 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
this.rollbackClassifier);
Object result = this.retryOperations
.execute(new MethodInvocationRetryCallback(invocation, label),
.execute(new StatefulMethodInvocationRetryCallback(invocation, label),
this.recoverer != null
? new ItemRecovererCallback(args, this.recoverer) : null,
retryState);
@@ -204,21 +204,12 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
* @author Dave Syer
*
*/
private static final class MethodInvocationRetryCallback
implements RetryCallback<Object, Throwable> {
private static final class StatefulMethodInvocationRetryCallback
extends MethodInvocationRetryCallback<Object, Throwable> {
private final MethodInvocation invocation;
private String label;
private MethodInvocationRetryCallback(MethodInvocation invocation, String label) {
this.invocation = invocation;
if (StringUtils.hasText(label)) {
this.label = label;
}
else {
this.label = invocation.getMethod().toGenericString();
}
private StatefulMethodInvocationRetryCallback(MethodInvocation invocation,
String label) {
super(invocation, label);
}
@Override

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2006-2007 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
*
* https://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.retry.listener;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.interceptor.MethodInvocationRetryCallback;
/**
* <p>
* Empty method implementation of {@link RetryListener} with focus on the AOP reflective
* method invocations providing convenience retry listener type-safe (with a
* `MethodInvocationRetryCallback` callback parameter) specific methods.
* </p>
* NOTE that this listener performs an action only when dealing with callbacks that are
* instances of {@link MethodInvocationRetryCallback}.
*
* @since 1.3
*/
public class MethodInvocationRetryListenerSupport implements RetryListener {
public <T, E extends Throwable> void close(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
if (callback instanceof MethodInvocationRetryCallback) {
MethodInvocationRetryCallback<T, E> methodInvocationRetryCallback = (MethodInvocationRetryCallback<T, E>) callback;
doClose(context, methodInvocationRetryCallback, throwable);
}
}
public <T, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
if (callback instanceof MethodInvocationRetryCallback) {
MethodInvocationRetryCallback<T, E> methodInvocationRetryCallback = (MethodInvocationRetryCallback<T, E>) callback;
doOnError(context, methodInvocationRetryCallback, throwable);
}
}
public <T, E extends Throwable> boolean open(RetryContext context,
RetryCallback<T, E> callback) {
if (callback instanceof MethodInvocationRetryCallback) {
MethodInvocationRetryCallback<T, E> methodInvocationRetryCallback = (MethodInvocationRetryCallback<T, E>) callback;
return doOpen(context, methodInvocationRetryCallback);
}
// in case that the callback is not for a reflective method invocation
// just go forward with the execution
return true;
}
protected <T, E extends Throwable> void doClose(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
}
protected <T, E extends Throwable> void doOnError(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
}
protected <T, E extends Throwable> boolean doOpen(RetryContext context,
MethodInvocationRetryCallback<T, E> callback) {
return true;
}
}

View File

@@ -16,22 +16,31 @@
package org.springframework.retry.interceptor;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.MethodInvocationRetryListenerSupport;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
@@ -40,13 +49,12 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ClassUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class RetryOperationsInterceptorTests {
private static int count;
private static int transactionCount;
private RetryOperationsInterceptor interceptor;
private Service service;
@@ -55,10 +63,6 @@ public class RetryOperationsInterceptorTests {
private RetryContext context;
private static int count;
private static int transactionCount;
@Before
public void setUp() throws Exception {
this.interceptor = new RetryOperationsInterceptor();
@@ -94,6 +98,42 @@ public class RetryOperationsInterceptorTests {
assertEquals("FOO", this.context.getAttribute(RetryContext.NAME));
}
@Test
public void testDefaultInterceptorWithRetryListenerInspectingTheMethodInvocation()
throws Exception {
final String label = "FOO";
final String classTagName = "class";
final String methodTagName = "method";
final String labelTagName = "label";
final Map<String, String> monitoringTags = new HashMap<String, String>();
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(new SimpleRetryPolicy(2));
template.registerListener(new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doClose(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
monitoringTags.put(labelTagName, callback.getLabel());
Method method = callback.getInvocation().getMethod();
monitoringTags.put(classTagName,
method.getDeclaringClass().getSimpleName());
monitoringTags.put(methodTagName, method.getName());
}
});
this.interceptor.setLabel(label);
this.interceptor.setRetryOperations(template);
((Advised) this.service).addAdvice(this.interceptor);
this.service.service();
assertEquals(2, count);
assertEquals(3, monitoringTags.entrySet().size());
assertThat(monitoringTags, allOf(hasEntry(labelTagName, label),
hasEntry(classTagName,
RetryOperationsInterceptorTests.Service.class.getSimpleName()),
hasEntry(methodTagName, "service")));
}
@Test
public void testDefaultInterceptorWithRecovery() throws Exception {
RetryTemplate template = new RetryTemplate();

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2006-2007 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
*
* https://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.retry.listener;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.interceptor.MethodInvocationRetryCallback;
public class MethodInvocationRetryListenerSupportTests {
@Test
public void testClose() {
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport();
try {
support.close(null, null, null);
}
catch (Exception e) {
fail("Unexpected exception");
}
}
@Test
public void testCloseWithMethodInvocationRetryCallbackShouldCallDoCloseMethod() {
final AtomicInteger callsOnDoCloseMethod = new AtomicInteger(0);
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doClose(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
callsOnDoCloseMethod.incrementAndGet();
}
};
RetryContext context = mock(RetryContext.class);
MethodInvocationRetryCallback callback = mock(
MethodInvocationRetryCallback.class);
support.close(context, callback, null);
assertEquals(1, callsOnDoCloseMethod.get());
}
@Test
public void testCloseWithRetryCallbackShouldntCallDoCloseMethod() {
final AtomicInteger callsOnDoCloseMethod = new AtomicInteger(0);
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doClose(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
callsOnDoCloseMethod.incrementAndGet();
}
};
RetryContext context = mock(RetryContext.class);
RetryCallback callback = mock(RetryCallback.class);
support.close(context, callback, null);
assertEquals(0, callsOnDoCloseMethod.get());
}
@Test
public void testOnError() {
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport();
try {
support.onError(null, null, null);
}
catch (Exception e) {
fail("Unexpected exception");
}
}
@Test
public void testOnErrorWithMethodInvocationRetryCallbackShouldCallDoOnErrorMethod() {
final AtomicInteger callsOnDoOnErrorMethod = new AtomicInteger(0);
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doOnError(RetryContext context,
MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
callsOnDoOnErrorMethod.incrementAndGet();
}
};
RetryContext context = mock(RetryContext.class);
MethodInvocationRetryCallback callback = mock(
MethodInvocationRetryCallback.class);
support.onError(context, callback, null);
assertEquals(1, callsOnDoOnErrorMethod.get());
}
@Test
public void testOpen() {
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport();
assertTrue(support.open(null, null));
}
@Test
public void testOpenWithMethodInvocationRetryCallbackShouldCallDoCloseMethod() {
final AtomicInteger callsOnDoOpenMethod = new AtomicInteger(0);
MethodInvocationRetryListenerSupport support = new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> boolean doOpen(RetryContext context,
MethodInvocationRetryCallback<T, E> callback) {
callsOnDoOpenMethod.incrementAndGet();
return true;
}
};
RetryContext context = mock(RetryContext.class);
MethodInvocationRetryCallback callback = mock(
MethodInvocationRetryCallback.class);
assertTrue(support.open(context, callback));
assertEquals(1, callsOnDoOpenMethod.get());
}
}