背景
最近有一个小伙伴提了一个issues 指出 @Retryable注解在接口上不生效
。首先我们复现issues问题.
问题复现
- 新建一个接口并添加@Retryable注解
public interface LocalRetryService {
@Retryable(scene = "localRetryWithAnnoOnInterface", retryStrategy = RetryType.ONLY_LOCAL)
void localRetryWithAnnoOnInterface(String params);
}
实现接口并执行一个异常的代码
@Component
public class LocalRetryServiceImpl implements LocalRetryService {
@Override
public void localRetryWithAnnoOnInterface(final String params) {
double i = 1 / 0;
}
}
- 观察日志是否触发异常
通过观察日志并未触发重试
想要知道为什么会出现这个问题就得了解一下啊注解的继承问题?
注解的继承问题
经过测试得出以下结论
| 类的类型 | 作用位置 | 是否可以获取 |
|---|---|---|
| 接口 | 类上 | ❌ |
| 接口 | 方法上 | ❌ |
| 抽象类 | 类上 | ✅ |
| 抽象类 | 方法上且子类重写 | ❌ |
| 抽象类 | 方法上且子类不重写 | ✅ |
| 普通类 | 类上 | ✅ |
| 普通类 | 方法上 | ✅ |
那为啥注解在接口上没作用?
Spring 的动态代理主要分为两种,一种是JDK 动态代理 ;一种是CGLIB 动态代理;
JDK 动态代理
JDK 动态代理主要是针对实现了某个接口的类。该方式基于反射的机制实现,会生成一个实现相同接口的代理类,然后通过对方法的充写,实现对代码的增强。
在该方式中接口中的注解无法被实现类继承,AOP 中的切点无法匹配上实现类,所以也就不会为实现类创建代理,所以我们使用的类其实是未被代理的原始类,自然也就不会被增强了。
CGLIB 动态代理
- 不存在继承关系 AOP可进行有效拦截(CGLIB动态代理)
- 存在继承关系 有父类和子类 ,切点注解在父类方法。若子类重写父类的方法将不会被拦截,而未重写的方法可以被AOP拦截。
具体方案
我们知道事务的注解@Transactional和Spring Retry的注解@Retryable都是支持在接口的方法和抽象类的方法上, 不妨先学习一下他们是如何实现
先阅读一下Spring Retry的关于这块的源码
public class RetryConfiguration extends AbstractPointcutAdvisor implements IntroductionAdvisor, BeanFactoryAware {
...
private final class AnnotationClassOrMethodFilter extends AnnotationClassFilter {
private final AnnotationMethodsResolver methodResolver;
AnnotationClassOrMethodFilter(Class<? extends Annotation> annotationType) {
super(annotationType, true);
this.methodResolver = new AnnotationMethodsResolver(annotationType);
}
@Override
public boolean matches(Class<?> clazz) {
return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz);
}
}
private static class AnnotationMethodsResolver {
private Class<? extends Annotation> annotationType;
public AnnotationMethodsResolver(Class<? extends Annotation> annotationType) {
this.annotationType = annotationType;
}
public boolean hasAnnotatedMethods(Class<?> clazz) {
final AtomicBoolean found = new AtomicBoolean(false);
ReflectionUtils.doWithMethods(clazz,
new MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException,
IllegalAccessException {
if (found.get()) {
return;
}
Annotation annotation = AnnotationUtils.findAnnotation(method,
annotationType);
if (annotation != null) { found.set(true); }
}
});
return found.get();
}
}
}
原来如此,他们是自动生成一个增强类,通过Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);和super.matches(clazz)配置类和方法上注解,说到这里我们知道原理,下面按照这种方式实现我们自己的增强器和拦截器
仿造Spring Retry实现
新增EasyRetryPointcutAdvisor增强器
/**
* @author www.byteblogs.com
* @date 2023-08-23
*/
//@Component
public class EasyRetryPointcutAdvisor extends AbstractPointcutAdvisor implements IntroductionAdvisor, BeanFactoryAware, InitializingBean {
private Advice advice;
private Pointcut pointcut;
private BeanFactory beanFactory;
@Autowired
private EasyRetryInterceptor easyRetryInterceptor;
@Override
public void afterPropertiesSet() throws Exception {
Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
retryableAnnotationTypes.add(Retryable.class);
this.pointcut = buildPointcut(retryableAnnotationTypes);
this.advice = buildAdvice();
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
/**
* Set the {@code BeanFactory} to be used when looking up executors by qualifier.
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public ClassFilter getClassFilter() {
return pointcut.getClassFilter();
}
@Override
public Class<?>[] getInterfaces() {
return new Class[] { Retryable.class };
}
@Override
public void validateInterfaces() throws IllegalArgumentException {
}
@Override
public Advice getAdvice() {
return this.advice;
}
protected Advice buildAdvice() {
return easyRetryInterceptor;
}
/**
* Calculate a pointcut for the given retry annotation types, if any.
* @param retryAnnotationTypes the retry annotation types to introspect
* @return the applicable Pointcut object, or {@code null} if none
*/
protected Pointcut buildPointcut(Set<Class<? extends Annotation>> retryAnnotationTypes) {
ComposablePointcut result = null;
for (Class<? extends Annotation> retryAnnotationType : retryAnnotationTypes) {
Pointcut filter = new AnnotationClassOrMethodPointcut(retryAnnotationType);
if (result == null) {
result = new ComposablePointcut(filter);
}
else {
result.union(filter);
}
}
return result;
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
private final class AnnotationClassOrMethodPointcut extends StaticMethodMatcherPointcut {
private final MethodMatcher methodResolver;
AnnotationClassOrMethodPointcut(Class<? extends Annotation> annotationType) {
this.methodResolver = new AnnotationMethodMatcher(annotationType);
setClassFilter(new AnnotationClassOrMethodFilter(annotationType));
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return getClassFilter().matches(targetClass) || this.methodResolver.matches(method, targetClass);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AnnotationClassOrMethodPointcut)) {
return false;
}
AnnotationClassOrMethodPointcut otherAdvisor = (AnnotationClassOrMethodPointcut) other;
return ObjectUtils.nullSafeEquals(this.methodResolver, otherAdvisor.methodResolver);
}
}
private final class AnnotationClassOrMethodFilter extends AnnotationClassFilter {
private final AnnotationMethodsResolver methodResolver;
AnnotationClassOrMethodFilter(Class<? extends Annotation> annotationType) {
super(annotationType, true);
this.methodResolver = new AnnotationMethodsResolver(annotationType);
}
@Override
public boolean matches(Class<?> clazz) {
return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz);
}
}
private static class AnnotationMethodsResolver {
private Class<? extends Annotation> annotationType;
public AnnotationMethodsResolver(Class<? extends Annotation> annotationType) {
this.annotationType = annotationType;
}
public boolean hasAnnotatedMethods(Class<?> clazz) {
final AtomicBoolean found = new AtomicBoolean(false);
ReflectionUtils.doWithMethods(clazz, method -> {
if (found.get()) {
return;
}
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
if (annotation != null) {
found.set(true);
}
});
return found.get();
}
}
}
实现拦截器EasyRetryInterceptor
public class EasyRetryInterceptor implements MethodInterceptor, AfterAdvice, Serializable, Ordered {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static String retryErrorMoreThresholdTextMessageFormatter =
"<font face=\"微软雅黑\" color=#ff0000 size=4>{}环境 重试组件异常</font> \r\n" +
"> 名称:{} \r\n" +
"> 时间:{} \r\n" +
"> 异常:{} \n"
;
@Autowired
@Qualifier("localRetryStrategies")
private RetryStrategy retryStrategy;
@Autowired
private EasyRetryAlarmFactory easyRetryAlarmFactory;
@Autowired
private StandardEnvironment standardEnvironment;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String traceId = UUID.randomUUID().toString();
LogUtils.debug(log,"Start entering the around method traceId:[{}]", traceId);
Retryable retryable = getAnnotationParameter(invocation.getMethod());
String executorClassName = invocation.getThis().getClass().getName();
String methodEntrance = getMethodEntrance(retryable, executorClassName);
if (StrUtil.isBlank(RetrySiteSnapshot.getMethodEntrance())) {
RetrySiteSnapshot.setMethodEntrance(methodEntrance);
}
Throwable throwable = null;
Object result = null;
RetryerResultContext retryerResultContext;
try {
result = invocation.proceed();
} catch (Throwable t) {
throwable = t;
} finally {
LogUtils.debug(log,"Start retrying. traceId:[{}] scene:[{}] executorClassName:[{}]", traceId, retryable.scene(), executorClassName);
// 入口则开始处理重试
retryerResultContext = doHandlerRetry(invocation, traceId, retryable, executorClassName, methodEntrance, throwable);
}
LogUtils.debug(log,"Method return value is [{}]. traceId:[{}]", result, traceId, throwable);
// 若是重试完成了, 则判断是否返回重试完成后的数据
if (Objects.nonNull(retryerResultContext)) {
// 重试成功直接返回结果 若注解配置了isThrowException=false 则不抛出异常
if (retryerResultContext.getRetryResultStatusEnum().getStatus().equals(RetryResultStatusEnum.SUCCESS.getStatus())
|| !retryable.isThrowException()) {
// 若返回值是NULL且是基本类型则返回默认值
Method method = invocation.getMethod();
if (Objects.isNull(retryerResultContext.getResult()) && method.getReturnType().isPrimitive()) {
return Defaults.defaultValue(method.getReturnType());
}
return retryerResultContext.getResult();
}
}
if (throwable != null) {
throw throwable;
} else {
return result;
}
}
private RetryerResultContext doHandlerRetry(MethodInvocation invocation, String traceId, Retryable retryable, String executorClassName, String methodEntrance, Throwable throwable) {
if (!RetrySiteSnapshot.isMethodEntrance(methodEntrance)
|| RetrySiteSnapshot.isRunning()
|| Objects.isNull(throwable)
// 重试流量不开启重试
|| RetrySiteSnapshot.isRetryFlow()
// 下游响应不重试码,不开启重试
|| RetrySiteSnapshot.isRetryForStatusCode()
) {
if (!RetrySiteSnapshot.isMethodEntrance(methodEntrance)) {
LogUtils.debug(log, "Non-method entry does not enable local retries. traceId:[{}] [{}]", traceId, RetrySiteSnapshot.getMethodEntrance());
} else if (RetrySiteSnapshot.isRunning()) {
LogUtils.debug(log, "Existing running retry tasks do not enable local retries. traceId:[{}] [{}]", traceId, EnumStage.valueOfStage(RetrySiteSnapshot.getStage()));
} else if (Objects.isNull(throwable)) {
LogUtils.debug(log, "No exception, no local retries. traceId:[{}]", traceId);
} else if (RetrySiteSnapshot.isRetryFlow()) {
LogUtils.debug(log, "Retry traffic does not enable local retries. traceId:[{}] [{}]", traceId, RetrySiteSnapshot.getRetryHeader());
} else if (RetrySiteSnapshot.isRetryForStatusCode()) {
LogUtils.debug(log, "Existing exception retry codes do not enable local retries. traceId:[{}]", traceId);
} else {
LogUtils.debug(log, "Unknown situations do not enable local retry scenarios. traceId:[{}]", traceId);
}
return null;
}
return openRetry(invocation, traceId, retryable, executorClassName, throwable);
}
private RetryerResultContext openRetry(MethodInvocation point, String traceId, Retryable retryable, String executorClassName, Throwable throwable) {
try {
RetryerResultContext context = retryStrategy.openRetry(retryable.scene(), executorClassName, point.getArguments());
LogUtils.info(log,"local retry result. traceId:[{}] message:[{}]", traceId, context);
if (RetryResultStatusEnum.SUCCESS.getStatus().equals(context.getRetryResultStatusEnum().getStatus())) {
LogUtils.debug(log, "local retry successful. traceId:[{}] result:[{}]", traceId, context.getResult());
}
return context;
} catch (Exception e) {
LogUtils.error(log,"retry component handling exception,traceId:[{}]", traceId, e);
// 预警
sendMessage(e);
} finally {
RetrySiteSnapshot.removeAll();
}
return null;
}
private void sendMessage(Exception e) {
try {
ConfigDTO.Notify notifyAttribute = GroupVersionCache.getNotifyAttribute(NotifySceneEnum.CLIENT_COMPONENT_ERROR.getNotifyScene());
if (Objects.nonNull(notifyAttribute)) {
AlarmContext context = AlarmContext.build()
.text(retryErrorMoreThresholdTextMessageFormatter,
EnvironmentUtils.getActiveProfile(),
EasyRetryProperties.getGroup(),
LocalDateTime.now().format(formatter),
e.getMessage())
.title("retry component handling exception:[{}]", EasyRetryProperties.getGroup())
.notifyAttribute(notifyAttribute.getNotifyAttribute());
Alarm<AlarmContext> alarmType = easyRetryAlarmFactory.getAlarmType(notifyAttribute.getNotifyType());
alarmType.asyncSendMessage(context);
}
} catch (Exception e1) {
LogUtils.error(log, "Client failed to send component exception alert.", e1);
}
}
public String getMethodEntrance(Retryable retryable, String executorClassName) {
if (Objects.isNull(retryable)) {
return StrUtil.EMPTY;
}
return retryable.scene().concat("_").concat(executorClassName);
}
private Retryable getAnnotationParameter(Method method) {
Retryable retryable = null;
if (method.isAnnotationPresent(Retryable.class)) {
//获取当前类的方法上标注的注解对象
retryable = method.getAnnotation(Retryable.class);
}
if (retryable == null) {
//返回当前类或父类或接口方法上标注的注解对象
retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class);
}
// if (retryable == null) {
// //返回当前类或父类或接口上标注的注解对象
// retryable = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Retryable.class);
// }
return retryable;
}
@Override
public int getOrder() {
String order = standardEnvironment
.getProperty("easy-retry.aop.order", String.valueOf(Ordered.HIGHEST_PRECEDENCE));
return Integer.parseInt(order);
}
}
测试改造结果
从测试结果来看,效果还是很不错了,完美的解决了这个问题
一波小广告
EasyRetry是一款基于BASE思想实现的分布式服务重试组件,旨在通过重试机制确保数据的最终一致性。它提供了控制台任务观测、可配置的重试策略、重试后执行回调以及丰富地告警配置等功能。通过这些手段,可以对异常数据进行全面监测和回放,从而在确保系统高可用性的同时,大大提升数据的一致性
为了便于快速上手EasyRetry特别的录制了视频教程还在持续的录制中有兴趣可以看看。 视频地址: www.easyretry.com/pages/a774e…
开源不易,路过的小伙伴点点Star: gitee.com/aizuda/easy…