From 37c4bb7cc9e0696371bd413c79ad3bc94556c534 Mon Sep 17 00:00:00 2001 From: byteblogs168 <598092184@qq.com> Date: Tue, 5 Sep 2023 09:29:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=202.3.0=201.=20=E4=BF=AE=E5=A4=8DRetryabl?= =?UTF-8?q?e=E4=BD=9C=E7=94=A8=E5=9C=A8=E6=8E=A5=E5=8F=A3=E4=B8=8A?= =?UTF-8?q?=E6=97=A0=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../intercepter/EasyRetryInterceptor.java | 225 ++++++++++++++++++ .../intercepter/EasyRetryPointcutAdvisor.java | 181 ++++++++++++++ .../client/core/intercepter/RetryAspect.java | 5 +- pom.xml | 2 +- 4 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryInterceptor.java create mode 100644 easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryPointcutAdvisor.java diff --git a/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryInterceptor.java b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryInterceptor.java new file mode 100644 index 00000000..18111ce8 --- /dev/null +++ b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryInterceptor.java @@ -0,0 +1,225 @@ +package com.aizuda.easy.retry.client.core.intercepter; + +import cn.hutool.core.util.StrUtil; +import com.aizuda.easy.retry.client.core.annotation.Retryable; +import com.aizuda.easy.retry.client.core.cache.GroupVersionCache; +import com.aizuda.easy.retry.client.core.config.EasyRetryProperties; +import com.aizuda.easy.retry.client.core.intercepter.RetrySiteSnapshot.EnumStage; +import com.aizuda.easy.retry.client.core.retryer.RetryerResultContext; +import com.aizuda.easy.retry.client.core.strategy.RetryStrategy; +import com.aizuda.easy.retry.common.core.alarm.Alarm; +import com.aizuda.easy.retry.common.core.alarm.AlarmContext; +import com.aizuda.easy.retry.common.core.alarm.EasyRetryAlarmFactory; +import com.aizuda.easy.retry.common.core.enums.NotifySceneEnum; +import com.aizuda.easy.retry.common.core.enums.RetryResultStatusEnum; +import com.aizuda.easy.retry.common.core.log.LogUtils; +import com.aizuda.easy.retry.common.core.util.EnvironmentUtils; +import com.aizuda.easy.retry.server.model.dto.ConfigDTO; +import com.google.common.base.Defaults; +import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.AfterAdvice; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.stereotype.Component; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.UUID; + +/** + * @author www.byteblogs.com + * @date 2023-08-23 + */ +@Slf4j +@Component +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 = + "{}环境 重试组件异常 \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 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); + } + +} diff --git a/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryPointcutAdvisor.java b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryPointcutAdvisor.java new file mode 100644 index 00000000..84edd771 --- /dev/null +++ b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/EasyRetryPointcutAdvisor.java @@ -0,0 +1,181 @@ +package com.aizuda.easy.retry.client.core.intercepter; + +import com.aizuda.easy.retry.client.core.annotation.Retryable; +import org.aopalliance.aop.Advice; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.aop.support.annotation.AnnotationClassFilter; +import org.springframework.aop.support.annotation.AnnotationMethodMatcher; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @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> retryableAnnotationTypes = new LinkedHashSet>(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> retryAnnotationTypes) { + ComposablePointcut result = null; + for (Class 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 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 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 annotationType; + + public AnnotationMethodsResolver(Class 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(); + } + + } + + + +} diff --git a/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/RetryAspect.java b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/RetryAspect.java index c288503b..597bddc9 100644 --- a/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/RetryAspect.java +++ b/easy-retry-client-core/src/main/java/com/aizuda/easy/retry/client/core/intercepter/RetryAspect.java @@ -44,8 +44,9 @@ import java.util.UUID; * @author: www.byteblogs.com * @date : 2022-03-03 11:41 */ -@Aspect -@Component +//@Aspect +//@Component +@Deprecated @Slf4j public class RetryAspect implements Ordered { diff --git a/pom.xml b/pom.xml index 2372249d..2939561d 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 1.8 - 2.2.0 + 2.3.0-SNAPSHOT 1.0.0 4.1.94.Final 5.8.19