龙空技术网

接口方法上的注解为何失效?探究Java中的神秘BUG

奋斗小蜗牛88 143

前言:

此刻各位老铁们对“java调用接口异常重试”大体比较关注,大家都需要了解一些“java调用接口异常重试”的相关内容。那么小编同时在网上收集了一些对于“java调用接口异常重试””的相关文章,希望大家能喜欢,同学们一起来了解一下吧!

背景

最近有一个小伙伴提了一个issues 指出@Retryable注解在接口上不生效 · Issue #I7VGS8 · aizuda/easy-retry - Gitee.com 首先我们复现issues问题.

问题复现

1. 新建一个接口并添加@Retryable注解

public interface LocalRetryService {  @Retryable(scene = "localRetryWithAnnoOnInterface", retryStrategy = RetryType.ONLY_LOCAL)  void localRetryWithAnnoOnInterface(String params);}

实现接口并执行一个异常的代码

@Componentpublic class LocalRetryServiceImpl implements LocalRetryService {    @Override    public void localRetryWithAnnoOnInterface(final String params) {    		double i = 1 / 0;    }}

2. 观察日志是否触发异常

通过观察日志并未触发重试

> 想要知道为什么会出现这个问题就得了解一下啊注解的继承问题?

# 注解的继承问题

经过测试得出以下结论

那为啥注解在接口上没作用?

Spring 的动态代理主要分为两种,一种是JDK 动态代理 ;一种是CGLIB 动态代理;

JDK 动态代理

JDK 动态代理主要是针对实现了某个接口的类。该方式基于反射的机制实现,会生成一个实现相同接口的代理类,然后通过对方法的充写,实现对代码的增强。

在该方式中接口中的注解无法被实现类继承,AOP 中的切点无法匹配上实现类,所以也就不会为实现类创建代理,所以我们使用的类其实是未被代理的原始类,自然也就不会被增强了。

CGLIB 动态代理

1. 不存在继承关系 AOP可进行有效拦截(CGLIB动态代理)

2. 存在继承关系 有父类和子类 ,切点注解在父类方法。若子类重写父类的方法将不会被拦截,而未重写的方法可以被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  * @date 2023-08-23 *///@Componentpublic 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特别的录制了视频教程还在持续的录制中有兴趣可以看看。

视频地址:

开源不易,路过的小伙伴点点Star:

标签: #java调用接口异常重试