From 2abfcef18c7232575c962df511767dd34407f73d Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 24 Apr 2014 14:17:12 +0100 Subject: [PATCH] Add Retryable interface for proxies advising @Retryable beans This is purely a marker interface, but might be useful for other tools looking to apply retry advice (they should usually not bother if the bean already implements Retryable) --- README.md | 2 ++ ...tationAwareRetryOperationsInterceptor.java | 1 - .../retry/annotation/RetryConfiguration.java | 18 +++++++++++- .../RetryOperationsInterceptor.java | 8 +++++- .../retry/interceptor/Retryable.java | 28 +++++++++++++++++++ .../StatefulRetryOperationsInterceptor.java | 8 +++++- .../retry/annotation/EnableRetryTests.java | 15 ++++++++-- 7 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/retry/interceptor/Retryable.java diff --git a/README.md b/README.md index 59e8802..f32d943 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,8 @@ for a random backoff between 100 and 500 milliseconds and up to 12 attempts. The The `@EnableRetry` annotation also looks for beans of type `Sleeper` and other strategies used in the `RetryTemplate` and interceptors to control the beviour of the retry at runtime. +The `@EnableRetry` annotation creates proxies for `@Retryable` beans, and the proxies (so the bean instances in the application) have the `Retryable` interface added to them. This is purely a marker interface, but might be useful for other tools looking to apply retry advice (they should usually not bother if the bean already implements `Retryable`). + ### XML Configuration Here is an example of declarative iteration using Spring AOP to repeat a service call to a method called `remoteCall` (for more detail on how to configure AOP interceptors see the Spring User Guide): diff --git a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java index 9c9da8d..613c0e7 100644 --- a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java @@ -115,7 +115,6 @@ public class AnnotationAwareRetryOperationsInterceptor implements MethodIntercep } else { delegate = getStatelessInterceptor(target, method, retryable); } - // TODO: allow declaring class to specify @Recover methods delegates.put(method, delegate); } } diff --git a/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java index 4faa19e..994b059 100644 --- a/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java +++ b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java @@ -23,6 +23,8 @@ import java.util.Set; import javax.annotation.PostConstruct; import org.aopalliance.aop.Advice; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractPointcutAdvisor; import org.springframework.aop.support.ComposablePointcut; @@ -49,7 +51,7 @@ import org.springframework.retry.policy.RetryContextCache; @SuppressWarnings("serial") @Configuration public class RetryConfiguration extends AbstractPointcutAdvisor implements - BeanFactoryAware { + IntroductionAdvisor, BeanFactoryAware { private Advice advice; @@ -86,6 +88,20 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements } } + @Override + public ClassFilter getClassFilter() { + return pointcut.getClassFilter(); + } + + @Override + public Class[] getInterfaces() { + return new Class[] { org.springframework.retry.interceptor.Retryable.class }; + } + + @Override + public void validateInterfaces() throws IllegalArgumentException { + } + @Override public Advice getAdvice() { return this.advice; diff --git a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java index 43e570d..3c20868 100644 --- a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java @@ -17,6 +17,7 @@ import java.util.Arrays; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.IntroductionInterceptor; import org.springframework.aop.ProxyMethodInvocation; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; @@ -41,7 +42,7 @@ import org.springframework.util.Assert; * @author Rob Harrop * @author Dave Syer */ -public class RetryOperationsInterceptor implements MethodInterceptor { +public class RetryOperationsInterceptor implements IntroductionInterceptor { private RetryOperations retryOperations = new RetryTemplate(); private MethodInvocationRecoverer recoverer; @@ -54,6 +55,11 @@ public class RetryOperationsInterceptor implements MethodInterceptor { public void setRecoverer(MethodInvocationRecoverer recoverer) { this.recoverer = recoverer; } + + @Override + public boolean implementsInterface(Class intf) { + return Retryable.class.isAssignableFrom(intf); + } public Object invoke(final MethodInvocation invocation) throws Throwable { diff --git a/src/main/java/org/springframework/retry/interceptor/Retryable.java b/src/main/java/org/springframework/retry/interceptor/Retryable.java new file mode 100644 index 0000000..463b633 --- /dev/null +++ b/src/main/java/org/springframework/retry/interceptor/Retryable.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2014 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.retry.interceptor; + +/** + * Marker interface for proxies that are providing retryable behaviour. Can be added by + * proxy creators that use the {@link RetryOperationsInterceptor} and + * {@link StatefulRetryOperationsInterceptor}. + * @author Dave Syer + * + */ +public interface Retryable { + +} diff --git a/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java index 50754f8..6a20d37 100644 --- a/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java @@ -22,6 +22,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.IntroductionInterceptor; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -51,7 +52,7 @@ import org.springframework.util.ObjectUtils; * * @author Dave Syer */ -public class StatefulRetryOperationsInterceptor implements MethodInterceptor { +public class StatefulRetryOperationsInterceptor implements IntroductionInterceptor { private transient Log logger = LogFactory.getLog(getClass()); @@ -105,6 +106,11 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier; } + @Override + public boolean implementsInterface(Class intf) { + return Retryable.class.isAssignableFrom(intf); + } + /** * Wrap the method invocation in a stateful retry with the policy and other helpers * provided. If there is a failure the exception will generally be re-thrown. The only diff --git a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java index 851adb5..f86185b 100644 --- a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java +++ b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java @@ -18,16 +18,15 @@ package org.springframework.retry.annotation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; +import org.springframework.aop.support.AopUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.retry.annotation.EnableRetry; -import org.springframework.retry.annotation.Recover; -import org.springframework.retry.annotation.Retryable; import org.springframework.retry.backoff.Sleeper; /** @@ -46,6 +45,16 @@ public class EnableRetryTests { context.close(); } + @Test + public void marker() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + Service service = context.getBean(Service.class); + assertTrue(AopUtils.isCglibProxy(service)); + assertTrue(service instanceof org.springframework.retry.interceptor.Retryable); + context.close(); + } + @Test public void recovery() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(