java中常见重试(retry)方案 (含AOP)
2020-01-17 本文已影响0人
suxin1932
别忘记 异步程序中的重试方案设计
2.重试的几种解决方案
对于重试是有场景限制的,不是什么场景都适合重试,
比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。
比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。
几种重试实现.png
2.1 原生代码侵入性实现重试
package com.zy.eureka.retry.v1;
public interface IEmployeeService {
String getName(Long id);
}
package com.zy.eureka.retry.v1;
import com.zy.eureka.retry.RpcService;
import org.springframework.stereotype.Service;
@Service
public class EmployeeServiceImplRetryV1 implements IEmployeeService {
private static final int RETRY_TIMES = 3;
@Override
public String getName(Long id) {
int times = 0;
while (times < RETRY_TIMES) {
try {
return RpcService.getInstance().getName(id);
} catch (Exception e) {
times++;
System.out.println("times ------------> " + times);
if (times >= RETRY_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
}
2.2 jdk动态代理实现
当业务中需要重试的方法越来越多时, 则需要抽取, 可采用动态代理
package com.zy.eureka.retry.v2;
public interface ITeacherService {
void teach(String subjectName);
}
package com.zy.eureka.retry.v2;
import com.zy.eureka.retry.RpcService;
public class TeacherServiceImplRetryV2 implements ITeacherService {
@Override
public void teach(String subjectName) {
RpcService.getInstance().teach(subjectName);
}
}
package com.zy.eureka.retry.v2;
import lombok.AllArgsConstructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 基于 jdk 动态代理实现重试功能, 适用于有接口的业务
*/
@AllArgsConstructor
public class JdkProxy implements InvocationHandler {
private final Object target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;
while (times < 3) {
try {
return method.invoke(target, args);
} catch (Throwable e) {
times ++;
System.out.println("times >>>>>>>>> " + times);
if (times >= 3) {
throw new RuntimeException(e);
}
}
}
return null;
}
/**
* 获取动态代理对象
* @param realObj 真实对象
* @return
*/
public static Object getProxy(Object realObj) {
InvocationHandler handler = new JdkProxy(realObj);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realObj.getClass().getInterfaces(), handler);
}
}
测试加参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
jdk proxy test.png
jdk proxy 生成的代理类.png
2.3 cglib动态代理实现
如果被代理的方法没有实现接口, 则必须要采用 cglib 动态代理了
package com.zy.eureka.retry.v3;
import com.zy.eureka.retry.RpcService;
public class ProgrammerServiceImpl {
public void program(String language) {
RpcService.getInstance().program(language);
}
}
package com.zy.eureka.retry.v3;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
int times = 0;
while (times < 3) {
try {
//通过代理子类调用父类的方法
return methodProxy.invokeSuper(object, args);
} catch (Throwable e) {
times++;
System.out.println("times >>>>>>>>> " + times);
if (times >= 3) {
throw new RuntimeException(e);
}
}
}
return null;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> tClass) {
return (T) Enhancer.create(tClass, this);
}
}
测试
测试时, 可在测试类(如下文的MyServiceImplTest)中, 加入静态代码块即可
static{
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\demos\\spring-cloud-dubbo\\spring-cloud-dubbo-eureka");
}
cglib生成的类分析.png
// 需要说明的是:
本例中用的是 net.sf.cglib 包下的依赖, 会生成 3 个文件.
若是采用 Spring 来间接实现, 则只有生成 1 个代理 文件.
// 调用过程:
代理对象调用 this.program 方法 ->
调用拦截器 ->
methodProxy.invokeSuper ->
CGLIB$program$0 ->
被代理对象program 方法
而我们在自定义的 com.zy.eureka.retry.v3.CglibProxy#intercept 中调用了
net.sf.cglib.proxy.MethodProxy#invokeSuper 方法.
// net.sf.cglib.proxy.MethodProxy
// 其静态内部类: net.sf.cglib.proxy.MethodProxy.FastClassInfo
private static class FastClassInfo {
FastClass f1; // 被代理类FastClass
FastClass f2; // 代理类FastClass
int i1; // 被代理类的方法签名(index)
int i2; // 代理类的方法签名
private FastClassInfo() {
}
}
上面代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。
FastClass并不是跟代理类一块生成的,而是在第一次执行MethodProxy invoke/invokeSuper时生成的并放在了缓存中。
// FastClass机制
Cglib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机制,
它的原理简单来说就是:
为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。
这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,
这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。
总结一下JDK动态代理和Gglib动态代理的区别
1.JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。
2.JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,
Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
3.JDK调用代理方法,是通过反射机制调用,
Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。
2.4 利用AOP自定义注解简化开发
package com.zy.eureka.retry.v4;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RetryAnno {
/**
* 重试次数
*
* @return
*/
int times() default 0;
/**
* 两次重试之间的间隔时间, 单位: ms
* @return
*/
long internal() default 100L;
}
package com.zy.eureka.retry.v4;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 3)
public class RetryAspect {
@Around(value = "@annotation(retryAnno)")
public Object process(ProceedingJoinPoint point, RetryAnno retryAnno) throws Throwable {
RetryAnno retry = Objects.nonNull(retryAnno) ? retryAnno : point.getTarget().getClass().getAnnotation(RetryAnno.class);
if (Objects.isNull(retry)) {
return point.proceed();
}
int times = retry.times();
if (times <= 1) {
return point.proceed();
}
int i = 0;
while (i < times) {
try {
return point.proceed();
} catch (Throwable e) {
i++;
System.out.println(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + " retry times is ======>>> " + i);
if (i >= times) {
throw new RuntimeException(e);
}
TimeUnit.MILLISECONDS.sleep(retry.internal());
}
}
return null;
}
}
package com.zy.eureka.retry.v4;
import com.zy.eureka.retry.RpcService;
import org.springframework.stereotype.Service;
@Service
public class CommentServiceImpl {
@RetryAnno(times = 3, internal = 2000)
public void comment(String foodName) {
RpcService.getInstance().comment(foodName);
}
}
2.5 基于 spring-retry 实现
pom.xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<!-- 如果是 spring-boot项目, 这里版本号可省略, 会自动适配版本号 -->
<!-- <version>1.2.5.RELEASE</version>-->
</dependency>
package com.zy.eureka.retry.v5;
import com.zy.eureka.retry.RpcService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class NearbyServiceImpl {
/**
* 重试
* @param address
* @return
*/
@Retryable(maxAttempts = 3, value = NullPointerException.class, backoff = @Backoff(delay = 2000L, multiplier = 2))
public boolean nearby(String address) {
System.out.println("根据这里调了几次, 可以看到重试了多少次...||||||||||||| >>>>>>>>>>> ||||||||||||||");
return RpcService.getInstance().nearby(address);
}
/**
* 降级机制: 如果最终仍然失败, 将会调用这里的方法
* @param e
* @return
*/
@Recover
public boolean degreeNearby(NullPointerException e) {
return false;
}
}
--------------------------------2.1--2.5的公共部分--------------------------------
package com.zy.eureka.retry;
import java.util.Objects;
public class RpcService {
private static final RpcService RPC_SERVICE = new RpcService();
public static RpcService getInstance() {
return RPC_SERVICE;
}
/**
* 配合测试 v1 版本的重试: 原始的代码侵入性高的
*
* @param id
* @return
*/
public String getName(Long id) {
throw new RuntimeException(String.format("failed to get employee's %s name...", id));
// return String.format("%s->%s", id, UUID.randomUUID().toString());
}
/**
* 配合测试 v2 版本的重试: 基于 jdk 动态代理
*
* @param subjectName
*/
public void teach(String subjectName) {
throw new RuntimeException(String.format("failed to teach subject: %s ...", subjectName));
}
/**
* 配合测试 v3版本的重试: 基于 cglib 动态代理
*
* @param language
*/
public void program(String language) {
throw new RuntimeException(String.format("%s programmer failed to code ...", language));
}
/**
* 配合测试 v4 版本的重试: 基于 AOP 实现, 简化开发
*
* @param foodName
*/
public void comment(String foodName) {
throw new RuntimeException(String.format("failed to comment %s ...", foodName));
}
/**
* 配合测试 v5 版本的重试: 基于 spring-retry 框架实现
*
* @param address
* @return
*/
public boolean nearby(String address) {
if (Objects.isNull(address)) {
throw new NullPointerException(String.format("failed to judge empty address is nearby, ...", address));
}
throw new RuntimeException(String.format("failed to judge address %s is nearby ...", address));
}
}
测试类
package com.zy.eureka.limit;
import com.zy.eureka.retry.v1.IEmployeeService;
import com.zy.eureka.retry.v2.ITeacherService;
import com.zy.eureka.retry.v2.JdkProxy;
import com.zy.eureka.retry.v2.TeacherServiceImplRetryV2;
import com.zy.eureka.retry.v3.CglibProxy;
import com.zy.eureka.retry.v3.ProgrammerServiceImpl;
import com.zy.eureka.retry.v4.CommentServiceImpl;
import com.zy.eureka.retry.v5.NearbyServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@EnableRetry
public class MyServiceImplTest {
@Autowired
private IEmployeeService employeeService;
@Autowired
private CommentServiceImpl commentService;
@Autowired
private NearbyServiceImpl nearbyService;
/**
* 测试 v1 版本的重试: 原始的代码侵入性高的
* @return
*/
@Test
public void fn01() {
System.out.println(employeeService.getName(1L));
}
/**
* 测试 v2 版本的重试: 基于 jdk 动态代理
*/
@Test
public void fn02() {
ITeacherService proxyTeacherService = (ITeacherService) JdkProxy.getProxy(new TeacherServiceImplRetryV2());
proxyTeacherService.teach("english");
}
/**
* 测试 v3版本的重试: 基于 cglib 动态代理
*/
@Test
public void fn03() {
ProgrammerServiceImpl programmerService = new CglibProxy().getProxy(ProgrammerServiceImpl.class);
programmerService.program("php");
}
/**
* 测试 v4 版本的重试: 基于 AOP 实现, 简化开发
*/
@Test
public void fn04() {
commentService.comment("banana");
}
/**
* 测试 v5 版本的重试: 基于 spring-retry 框架实现
*/
@Test
public void fn05() {
System.out.println("-----------------------------");
System.out.println(nearbyService.nearby(null));
System.out.println("-----------------------------");
}
}
参考资源
https://houbb.github.io/2018/08/08/retry (全面的总结)
https://blog.csdn.net/u011116672/article/details/77823867
https://blog.csdn.net/liuxiao723846/article/details/78866879
https://www.cnblogs.com/monkey0307/p/8328821.html (cglib动态代理实现原理)