概述
当你的系统依赖外部服务时,总是会有那么一些方法在执行时有可能会失败。方法执行失败了,那自然是再调用一次试试,说不定就调通了呢。为每个方法编写重试代码显然是对程序员的折磨,故下面我们介绍一个清爽的方法重试方案,其最终效果是被标注为@RetryExecution的方法,如果在运行过程中抛了异常,其会被再次尝试运行若干次。
(相同的思路,还可以实现自定义事物、锁、时间统计、异步重试等很多功能)
Spring AOP Proxies
关于Spring AOP的详细介绍参见官方文档,这里我们只介绍AOP Proxies的核心思想。考虑如下场景:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct
call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当我们创建一个对象的实例,通过其实例的引用调用其方法时,我们直接调用了对象的方法。倘若我们想在方法执行前后做一些额外的工作,但是不侵入方法的代码,一个自然的想法就是为对象创建代理。
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
我们为对象创建了动态代理,对象的方法实际上由代理负责执行,由此我们得以通过定义代理的行为,在不侵入原方法的代码的情况下扩展方法的执行行为(e.g. 事物、计时、重试、etc.)。
实现方案
基于如上原理,我们接下来通过扩展AOP API来实现清爽的方法重试方案。
定义重试注解
/**
* Created by benxue on 3/24/16.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface RetryExecution {
int retryTimes() default 1;
}
定义拦截器
这里是被标注的方法实际执行的地方。
/**
* Created by benxue on 3/24/16.
*/
@Component
public class RetryMethodInterceptor implements MethodInterceptor {
private static final Logger logger = LogManager.getLogger(RetryMethodInterceptor.class.getName());
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//cglib代理和jvm代理在获取annotation时是不是有区别?
int retryTimes = invocation.getMethod().getAnnotation(RetryExecution.class).retryTimes();
while (--retryTimes >= 0) {
try {
return invocation.proceed();
} catch (Throwable t) {
logger.error(t);
}
}
return invocation.proceed();
}
}
定义Advisor
Spring容器初始化Bean时,通过Advisor的Pointcut对象判断Bean中的方法是否需要被特殊处理(我们的例子中通过注解类型判断,同时判断结果会缓存下来)。如果需要,当方法通过代理被调用时,其会被转给Advisor的getAdvice方法返回的拦截器执行。
/**
* Created by benxue on 3/24/16.
*/
@Component
public class RetryMethodAdvisor extends AbstractPointcutAdvisor {
private final StaticMethodMatcherPointcut pointcut = new
StaticMethodMatcherPointcut() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.isAnnotationPresent(RetryExecution.class);
}
};
@Autowired
private RetryMethodInterceptor interceptor;
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.interceptor;
}
}
全局配置
下面的配置会使得Spring在初始化时,寻找全部存在的的Advisor,并尝试通过识别出的Advisor为所有存在的Bean的方法配置拦截器。(其实我们强制使用了cglib)
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="proxyTargetClass" value="true"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>
使用示例 && Self Injection
/**
* Created by benxue on 3/16/16.
*/
@Service
public class ServiceImpl implements Service {
ServiceImpl self;
@Autowired
private ApplicationContext applicationContext;
// 在Service初始化完成之后,注入其Reference
@PostConstruct
private void init() {
self = applicationContext.getBean("ServiceImpl", ServiceImpl.class);
}
@Override
public void hello(){
self.hahaha();
}
@Override
public void hi(){
hahaha();
}
@RetryExecution
@Override
public void hahaha() throw RuntimeException(){...}
}
public class Test {
@Autowired
private Service service;
private void test() {
service.hello();// 如果抛出异常,hahaha会被重新执行一次
service.hi(); // hahaha被本地调用,无论是否抛出异常,hahaha都不会被重试
service.hahaha(); // 如果抛出异常,hahaha会被重新执行一次
}
}