問題引入
現有一個接口,調用4次后才可已返回正常結果
public class RetryService {
private AtomicLong times = new AtomicLong();
public String hello () {
long l = times.incrementAndGet();
if (l % 4 == 0) {
throw new RuntimeException();
}
return "hello world";
}
}
解決方案
方式一: 硬核捕獲
public void method1 () {
for(int i = 0;i< 4;i++) {
try{
retryService.hello();
} catch (Exception) {
log.error("接口請求失敗!");
}
}
throw new RuntimeException("處理失敗");
}
方式二: 動態代理
調用
@Test
public void test2 () {
// jdk 反向代理使用
IRetryService retryService = new RetryService();
IRetryService proxy = (IRetryService) RetryInvocationHandler.getProxy(retryService);
String hello = proxy.hello();
System.out.println(hello);
// cglib 反向代理使用
CglibRetryProxy cglibRetryProxy = new CglibRetryProxy();
Object cglibProxy = cglibRetryProxy.getCglibProxy(retryService);
}
JDK 動態代理
package com.example.chaoming.exercise.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(Object object) {
this.subject = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;
while (times <4) {
try {
return method.invoke(subject,args);
} catch (Exception e) {
times++;
System.out.println("失敗第"+times+"次");
if (times >= 4) {
throw new RuntimeException();
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 獲取動態代理
* @param realSubject
* @return
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),realSubject.getClass().getInterfaces(),handler);
}
}
spring 注入代理 工具類
package com.example.chaoming.exercise.jdk;
import com.sun.javafx.fxml.PropertyNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
@Component
public class RetryServiceProxyHandler {
@Autowired
private ConfigurableApplicationContext applicationContext;
public Object getProxy (Class clazz) {
// 1. 從bean 中獲取對象
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
Map<String,Object> beans = beanFactory.getBeansOfType(clazz);
Set<Map.Entry<String,Object>> entries = beans.entrySet();
if (entries.size()<= 0) {
throw new PropertyNotFoundException();
}
Object bean = null;
// 2. 判斷該對象的代理對象是否存在
Object source = beans.entrySet().iterator().next().getValue();
String proxyBeanName = clazz.getSimpleName();
boolean exist = beanFactory.containsBean(proxyBeanName);
if (exist) {
bean = beanFactory.getBean(proxyBeanName);
return bean;
}
// 3. 不存在則生成代理對象
bean = RetryInvocationHandler.getProxy(source);
// 4. 將bean 注入到spring 容器中
beanFactory.registerSingleton(proxyBeanName,bean);
return bean;
}
}
CGlib 動態代理
package com.example.chaoming.exercise.jdk;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibRetryProxy implements MethodInterceptor {
private Object target;
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0;
while (times <4) {
try {
return method.invoke(target,objects);
} catch (Exception e) {
times++;
System.out.println("失敗第"+times+"次");
if (times >= 4) {
throw new RuntimeException();
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
// 獲取動態代理對象
public Object getCglibProxy (Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
Object o = enhancer.create();
return o;
}
}
方式三:Spring Aop實現
- 定義注解
package com.example.chaoming.exercise.jdk.aop;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
// 重試次數
int retryTimes() default 3;
// 重試時間間隔
int retryInterval() default 1;
}
- 處理切面邏輯
package com.example.chaoming.exercise.jdk.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cglib.proxy.MethodInterceptor;
import java.lang.reflect.Method;
@Aspect
public class RetryAspect {
@Pointcut("@annotation(com.example.chaoming.exercise.jdk.aop.Retryable)")
private void retryMethod (){}
@Around("retryMethod()")
public void retry(ProceedingJoinPoint point) throws InterruptedException {
Retryable retryable = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(Retryable.class);
int interval = retryable.retryInterval();
int times = retryable.retryTimes();
Throwable exception = new RuntimeException();
for (int retryTime = 1; retryTime < times; retryTime++) {
try {
point.proceed();
} catch (Throwable throwable) {
exception = throwable;
System.out.println("發生調用異常");
}
Thread.sleep(interval * 1000);
}
throw new RuntimeException("重試機會耗盡");
}
}
- 使用我們自定義的注解
@Test
@Retryable(retryTimes = 3,retryInterval = 1)
public void method1 () {
for(int i = 0;i< 6;i++) {
try{
String hello = hello();
System.out.println(hello);
} catch (Exception e) {
System.out.println("接口請求失敗");
}
}
throw new RuntimeException("處理失敗");
}
方式四:Spring 自帶重試工具
使用步驟
- 引入pom
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<scope>test</scope>
</dependency>
- 在啟動類或者配置類 打上注解 @EnableRetry
- 使用
@Service
public class RetryServiceImpl implements RetryService {
private static final Logger LOGGER = LoggerFactory.getLogger(RetryServiceImpl.class);
private AtomicInteger count = new AtomicInteger(1);
@Override
@Retryable(value = { RemoteAccessException.class }, maxAttemptsExpression = "${retry.maxAttempts:10}",
backoff = @Backoff(delayExpression = "${retry.backoff:1000}"))
public void retry() {
LOGGER.info("start to retry : " + count.getAndIncrement());
throw new RemoteAccessException("here " + count.get());
}
@Recover
public void recover(RemoteAccessException t) {
LOGGER.info("SampleRetryService.recover:{}", t.getClass().getName());
}
}
方式五:Gavua 重試用法
特點:相比Spring Retry,Guava Retry具有更強的靈活性,可以根據返回值校驗來判斷是否需要進行重試。
步驟
- 引入pom
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
- 創建一個Retryer實例
使用這個實例對需要重試的方法進行調用,可以通過很多方法來設置重試機制,比如使用retryIfException來對所有異常進行重試,使用retryIfExceptionOfType方法來設置對指定異常進行重試,使用retryIfResult來對不符合預期的返回結果進行重試,使用retryIfRuntimeException方法來對所有RuntimeException進行重試。
還有五個以with開頭的方法,用來對重試策略/等待策略/阻塞策略/單次任務執行時間限制/自定義監聽器進行設置,以實現更加強大的異常處理。
通過跟Spring AOP的結合,可以實現比Spring Retry更加強大的重試功能。
仔細對比之下,Guava Retry可以提供的特性有:
可以設置任務單次執行的時間限制,如果超時則拋出異常。
可以設置重試監聽器,用來執行額外的處理工作。
可以設置任務阻塞策略,即可以設置當前重試完成,下次重試開始前的這段時間做什么事情。
可以通過停止重試策略和等待策略結合使用來設置更加靈活的策略,比如指數等待時長並最多10次調用,隨機等待時長並永不停止等等。
public static void main(String[] args) throws Exception {
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.equalTo(false))
.retryIfException()
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withRetryListener(new MyRetryListener<>())
.build();
try {
retryer.call(()->{
//call something
return true;
});
} catch (Exception e) {
e.printStackTrace();
}
}
總結
本文由淺入深的對多種重試的姿勢進行了360度無死角教學,從最簡單的手動重試,到使用靜態代理,再到JDK動態代理和CGLib動態代理,再到Spring AOP,都是手工造輪子的過程,最后介紹了兩種目前比較好用的輪子,一個是Spring Retry,使用起來簡單粗暴,與Spring框架天生搭配,一個注解搞定所有事情,另一個便是Guava Retry,不依賴於Spring框架,自成體系,使用起來更加靈活強大。
個人認為,大部分場景下,Spring Retry提供的重試機制已經足夠強大,如果不需要Guava Retry提供的額外靈活性,使用Spring Retry就很棒了。當然,具體情況具體分析,但沒有必要的情況下,不鼓勵重復造輪子,研究別人怎么造輪子 比較重要。