@Service
public class OrderServiceProxyImpl 实现 订单服务 {
@Autowired
privateOrderServiceImpl orderService;
@Override
公共 void添加订单 () {
int 次= 1;
同时(次<= 5) {
试试 {
//内心抛异常
int i = 3 / 0;
orderService.addOrder();
} catch(例外 e) {
System.out.println("重试" + 次 + "次");
尝试 {
Thread.sleep(2000);
} c atch (InterruptedException ex) {
ex.printStackTrace();
}
次++ ;
如果(次数 > 5){
RuntimeException("不再重试!");
}
这样重试逻辑由代理类完成,原有业务类的逻辑不需要修改。我以后会考虑的。修改重试逻辑只需要修改这个类即可
代理模式虽然更加优雅,但是如果依赖的服务很多,为每个服务创建一个代理类显然太麻烦。其实重试逻辑是类似的,无非是重试次数和延迟不同而已。如果每个类都写这么长一串相似的代码,显然不太优雅!
3。 JDK动态代理
这时候,动态代理就登场了。只要写一个代理处理类就可以了
公共 类 RetryIncationHandler 实现 调用处理程序 {
私有 final 对象主题;
public RetryIncationHandler(对象主题) {
这个.主题 =主题;
}
@Override
公共 对象 调用(对象代理,方法方法,对象[]参数)投掷 可投掷 {
int 次 = 1; 同时(次<=5) {
试试 {
//内心抛异常
int i = 3 / 0;
return method.invoke(主题,args);
} catch(例外 e) {
System.out.println() "重试【" + 次 + "】次");
尝试 {
Thread.sleep (2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
次++;
if(次> 5) {
抛出 新运行时异常(“不再”试!");
}
}
}
返回 null;
}
公共 静态 对象getProxy(对象realSubject) {
IncationHandler handler = new RetryInitationHandler(realSubject);
返回 Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass( ).getInterfaces(), handler);
}
}
测试:
@RestController
@RequestMapping("/order")
公共 类 订单控制器 {
@Qualifier("orderServiceImpl")
@Autowired
私人OrderService orderService;
@ GetMapping("/addOrder")
public String addOrder() {
OrderService orderServiceProxy = ( OrderService)RetryIncationHandler.getProxy(orderService);
orderServiceProxy.addOrder();
return "add订购";
}
}
动态代理可以将重试逻辑都放到一个,显然比直接使用代理类要方便,也更加优雅。
这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法创建代理对象,这种方式就行不通了
4。 CGLib动态代理
现在已经说到了 JDK 动态代理,那就不得不提 CGLib 动态代理了。使用 JDK 动态代理对被代理的类有要求,不是所有的类都被代理,而 CGLib 动态代理则正好解决了这个问题
@Component
public class CGLibRetryProxyHandler 实现 方法拦截器 {
private 对象目标;
@Override
public 对象 拦截(对象o,方法方法,对象[]对象,方法代理方法代理) 投掷可投掷 {
int次 = 1;
同时(次<= 5) {
尝试 {
// 储备异常
int i = 3 / 0;
返回 method.invoke(target, 对象);
} catch(例外 e) {
System.out.println( "重试【" + 次 + "】次");
尝试 {
Thread.sleep(2000);
} catch(InterruptedException ex) {
例如.printStackTrace();
}
次数++;
if(次数 > 5) {
抛出 新运行时异常(“不再”试!");
}
}
}
返回 null;
}
公共 对象getCglibProxy(对象objectTarget){
这个 .target = objectTarget;
增强器 增强器 = new Enhancer();
enhancer.setSuperclass(objectTarget.getClass());
enhancer.setCallback(this);
对象结果= enhancer.create();
return结果;
}
}
测试:
@GetMapping("/addOrder")
公共字符串addOrder () {
OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
orderServiceProxy.addOrder();
return "addOrder";
}
这个太棒了,完美解决了JDK动态代理带来的缺陷。优雅指数提升了不少。
不过这个方案还存在一个问题,那就是需要对原有逻辑进行侵入式修改,每个代理实例调用的地方都需要进行调整,这样还是会给原有代码带来较多的修改。
5。手动Aop
考虑到以后可能有很多方法也需要重试功能,我们可以通过AOP来实现重试的常用功能:使用AOP为目标调用设置aspect,可以在前后添加一些重试目标方法调用。审判的逻辑
<依赖关系>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
依赖项>
自定义 备注 :
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
//最大重试次数
int retryTime s() 默认 3;
// 重试间隔
int retryInterval()默认 1;
}
@Slf4j
@Aspect
@Component
公共类 重试方面 {
@Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")
private 空隙 retryMethodC 全部 )
public 对象 重试 (ProceedingJoinPoint joinPoint) 抛出 InterruptedException {
// 获取重试次数和重试间隔
MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class) ;
int maxRetryTimes = retry.retryTimes();
int retryInterval = retry.retryInterval();
“ 可抛出错误 = new RuntimeException();
for (int重试次数 = 1; 重试次数 <= maxRetryTimes; re tryTimes++){尝试{
对象结果 = joinPoint.proceed();
log .warn("调用过程中发生异常,开始重试,retryTimes:{}", retryTimes);重试间隔 * 1000L);
“重试已用尽” ,错误);
添加 注释 @MyRetryable
至需要重试的方法:
@Service
公共 类OrderServiceImpl实现 订单服务 {
@Override
@MyRetryable(重试次数=5,重试间隔=2)
公共 void 添加订单 )
//添加订单
}
}
这样就不需要编写重复的代码,而且实现更加优雅:一个注解就可以实现重试。
6。 弹簧-重试
<依赖>
<groupId>org.springframework.retrygroupId >
<artifactId> spring -重试artifactId>
依赖>
启用重试功能:在启动类或配置类中添加@EnableRetry
注解
在需要重试的方法上添加@Retryable
注解
@Slf4j
@服务
公共 类 OrderServiceImpl 实现 OrderService {
@Override
@可重试(maxAttempts = 3,退避=@Backoff(延迟=2000,乘数=2))
公共 void 添加订单 () {
System.out.println("重试...");
int i = 3 / 0;
//添加订单
}
@恢复
public void 恢复(运行时异常e) {
log.error("达到最大重试次数", e);
}
}
该方法调用后会进行重试,最大重试次数为3,第一次重试间隔为2s,之后以2倍大小进行递增,第二次重试间隔为4s,第三次为8秒
Spring的重试机制还支持很多很有用的功能,由三个注解完成:
查看@Retryable注解源码:指定异常重试、次数
public @interface Retryable {
//设置重试拦截器的bean名称
字符串拦截器() default "";
//仅针对特定类型的异常重试。默认值:所有例外
Class extends Throwable>[] value() default {};
//包含或排除哪些异常要重试
类 extends Throwable>[] include() default {};
类 extends Throwable>[] 排除() default {};
/ / l设置本次重试的唯一标志,用于统计输出
String label() default“”;
布尔值 有状态() 默认值 false ;
//最大重试次数,默认是 3 倍
int maxAttempts() 默认 3;
字符串最大尝试表达式() 默认 "";
//设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟次数 @Backoff;
//异常表达式,异常抛出后执行,判断后续是否重试
字符串 异常表达式() 默认 "" ;
String[] 监听器() 默认 { };
}
@Backoff 注: 指定重试回退策略(如果由于网络波动导致调用失败,立即重试可能仍会失败。最好的选择是等待一段时间再尝试稍后再重试。如何决定等待多长时间再重试。通俗的说就是立即重试还是等待一段时间再重试。)
@Recover 注:进行后续工作:当重试达到指定次数时,会调用指定的方法进行log录音
等操作
注:
-
@Recover
注解标记的方法必须与@Retryable
标记的方法在同一个类中
-
重试方法抛出的异常类型需要与recover()
方法参数类型
保持一致
-
recover()
方法返回值需要与重试方法返回值一致
-
recover()
方法中不能再抛出Exception,否则会报无法识别异常的错误
这里还需要提醒的是,由于Spring Retry使用了Aspect增强,因此使用Aspect会不可避免地存在一个陷阱——方法是内部调用的。如果该方法被注释为 @Retryable
如果调用者和被调用者在同一个类中,那么重试会失败
通过上面的简单配置,可以看出Spring Retry的重试机制是比较周密的,比自己写AOP实现强大很多
缺点:
但是还存在一些不足。 Spring的重试机制只支持捕获异常,无法验证返回值
@Retryable
public字符串hello(){
long 当前 = count.incrementAndGet();
System.out.println("第" + 当前+"称为"");
if(当前 % 3!= 0) {
log.warn("通话失败"); "错误";
}
返回 “成功”;
}
因此,即使在方法中添加@Retryable
,也无法实现失败重试
除了使用注解之外,Spring Retry还支持在调用时直接使用代码重试:
@Test
publicvoidnormalSpringRetry() {
//表示需要排除哪些异常retried, key代表异常的字节码,值为true代表需要重试
Map, Boolean>ExceptionMap = new HashMap<>() ;
ExceptionMap.put(HelloRetryException.class,true);
//构建重试模板实例
RetryTemplate retryTemplate = new RetryTemplate();
// 设置重试回退操作策略,主要是设置重试间隔 FixedBackOffPolicy 返回 OffPolicy = 新 固定BackOffPolicy();
长 固定周期时间 = 1000L;
.setBackOffPeriod(fixedPeriodTime) ;
//设置重试策略,主要设置重试次数
int maxRetryTimes = 3;
SimpleRetryPolicy 重试策略 = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
布尔值execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
String hello = helloService.hello();
www.sychzs.cn("调用结果:{ ); },
//RecoverCallBack
retryContext -> {
//RecoveryCallback re转 假;;
-
NeverRetryPolicy
:RetryCallback只允许调用一次,不允许重试
-
AlwaysRetryPolicy
:允许无限次重试,直到成功。该方法逻辑不当会导致死循环
-
SimpleRetryPolicy
:固定重试次数策略,默认最大重试次数为3次,RetryTemplate
使用的默认策略
-
TimeoutRetryPolicy
:超时重试策略,默认超时时间为1秒,在指定超时时间内允许重试
-
ExceptionClassifierRetryPolicy
:设置不同异常的重试策略,与组合重试策略类似,不同的是这里只区分不同异常的重试
-
CircuitBreakerRetryPolicy
:带有断路器功能的重试策略,需要设置三个参数:openTimeout、resetTimeout、delegate
-
CompositeRetryPolicy
:组合重试策略。有两种组合方法。乐观组合重试策略意味着只要一项策略允许,您就可以重试。悲观组合重试策略是指只要其中一项策略不允许,就可以重试。你可以再试一次,但无论是哪种组合,组合中的每一个策略都会被执行
7。番石榴重试
相比Spring Retry,Guava Retry更加灵活,可以根据返回值来决定是否重试
<依赖项>
<groupId>com.github.rholdergroupId >
<artifactId>番石榴-重试artifactId>
<版本>2.0.0 版本
>
依赖>
@Override
public字符串guava重试(整数){ 重试器重试器=RetryerBuilder。newBuilder()
// 不管发生什么异常,重试 。当结果错误时,重试
.retryIfResult(result -> Objects.equals(result, "error"))
//重试等待策略:等待2s再重试
.withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit .SECONDS))
更糟糕不是不是没有。 ))
.withRetryListener(newRetryListener () void 重试(尝试尝试) {
System.out.println("RetryListener: 否。" + attempts.getAttemptNumber() + "呼叫" );
)
.build();
try {
www.sychzs.cn(() - > testGu avaRetry(num));
} catch(异常e ) {
e.printStackTrace(); "test";
}
先创建一个Retryer实例,然后使用这个实例来调用需要重试的方法。设置重试机制的方式有很多种:
-
retryIfException()
:重试所有异常
-
retryIfRuntimeException()
:设置重试指定异常
-
retryIfExceptionOfType()
:重试所有运行时异常
-
retryIfResult()
:重试不符合预期的返回结果
还有withXxx开头的五个方法,用于设置重试策略/等待策略/阻塞策略/单任务执行时限/自定义监听器,实现更强大的异常处理:
-
withRetryListener()
:设置重试监听器以执行附加处理
-
withWaitStrategy()
:重试等待策略
-
withStopStrategy()
:停止重试策略
-
withAttemptTimeLimiter
:设置任务单次执行的时间限制,超时则抛出异常
-
withBlockStrategy()
:设置任务阻塞策略,即可以设置在当前重试完成和下一次重试开始之前的时间段内做什么
总结
从手动重试,到自己使用Spring AOP实现,再到站在巨人的肩膀上使用特别优秀的开源实现Spring Retry和Google guava-retrying。介绍完各种重试实现方法,可以看到以上方法基本满足了大部分场景的需求:
-
如果是基于Spring的项目,使用Spring Retry的注解就可以解决大部分问题
-
如果项目没有使用Spring相关框架,适合使用Google guava-retrying:自成一体的系统,使用起来更灵活、更强大