Spring框架中使用AOP实现自定义重试切面注解功能

目录

一、背景

二、使用@Aspect注解实现

1.@Retry注解

2.@Aspect切面

三、切入AOP流程实现切面逻辑

1.Advice切面处理类

2.Pointcut切面切入点

3.Advisor类

4.Advisor可插拔式通过@Bean注入到Spring工厂

4.1 可插拔式配置类

4.2 可插拔式注解开关

4.3 注入到Spring工厂的实现原理

5.通过BeanPostProcessor接口实现

5.1 实现对应的BeanPostProcessor

5.2 引入BeanPostProcessor实现类

5.3 可插拔式注解配置

5.4 实现原理


一、背景

在平时的开发中,一般我们要使用切面完成某种注解功能都是通过@Aspect和@Component注解搭配,通过@Aspect将对注解进行处理的类逻辑变成切面来使用。这样放在平时的一个项目是没问题,倘若N个项目都有同样的功能,难道我们要把这个切面处理相同的逻辑搬到每个项目吗?如果突然发现需要在原来的逻辑上加上一些功能,难道每个项目都需要再次改一下然后上线吗?这样显得过于粗鲁及臃肿。

因此我们可以将这个功能处理逻辑写成一套可以嵌入到Spring流程中的公用方法,再将这个公用的部分类打包上传到maven私服,这样便可以完成多个项目共用一套处理逻辑,且便于管理。

现在假设有个公共的功能切面:重试机制,我们希望在调用第三方的接口或者对某些数据进行操作时如果抛出异常便记录并且再次调用N次,

二、使用@Aspect注解实现

如果使用@Aspect注解实现只需要写以下几个类便能够完成。

1.@Retry注解

代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {

    /**
     * 重试次数
     *
     * @return
     */
    int times() default 0;

    /**
     * 遇到异常是否抛出
     *
     * @return
     */
    boolean isThrow() default false;

}

我们现在只简单的声明两种属性,一个是次数,一个是碰到异常最终是否抛出。

2.@Aspect切面

代码如下:

@Component
@Aspect
@Slf4j
public class RetryAspect {

    @Around(value = "@annotation(retry)")
    public Object process(ProceedingJoinPoint point, Retry retry) 
            throws Throwable {
        Retry retry = Objects.nonNull(retry) ? retry: 
                point.getTarget().getClass().getAnnotation(Retry.class);
        if (retry != null) {
            int times = retry.times();
            boolean isThrow = retry.isThrow();
            if (times > 0) {
                for (int i = 0; i < times; i++) {
                    try {
                        return point.proceed();
                    } catch (Exception e) {
                        log.error("invoke retry error.");
                        if (i == times - 1 && isThrow) {
                            throw e;
                        }
                    }
                }
            }
        }
        return point.proceed();
    }

}

使用这种方式,使用时只需要在方法上注解一下即可。代码如下:

@Service
public class XXXService {
    @Retry(times = 3)
    public void testRetry() {
        // 执行方法
    }
}

这种的实现方式十分简单,但却极度依赖于项目实现,无法做到多个项目共享使用。

三、切入AOP流程实现切面逻辑

在阅读下文时最好对AOP流程原理有一定的了解,要不然很容易一头雾水。推荐先阅读Spring和AOP的集成与原理实现文章。

在推荐的文章中,我们知道AOP的大致流程图如下:

其中有三个对象需要重点注意:

  1. Advice:可以看成具体执行切面逻辑的地方;
  2. Pointcut:表明具体切入点,如哪些类和方法需要执行对应的切面处理;
  3. Advisor:里面保存了Advice实现类和Pointcut切入点实现类。

因此我们先把这三个重点对象的实现类给完成。

1.Advice切面处理类

前面说了,这个类是具体执行切面逻辑的地方,因此我们把前面的切面实现逻辑放到这个类中实现,代码如下:

@Slf4j
public class RetryInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Retry retry = invocation.getMethod()
                .getDeclaredAnnotation(Retry.class);
        if (retry != null) {
            int times = retry.times();
            boolean isThrow = retry.isThrow();
            if (times > 0) {
                for (int i = 0; i < times; i++) {
                    try {
                        return invocation.proceed();
                    } catch (Exception e) {
                        log.error("invoke retry error.");
                        if (i == times - 1 && isThrow) {
                            throw e;
                        }
                    }
                }
            }
        }
        return invocation.proceed();
    }
}

这个类实现了MethodInterceptor接口,这个接口是spring-aop提供的配套接口,相当于AOP版的JDK动态代理接口。

2.Pointcut切面切入点

前面已经完成了切面的具体处理逻辑,现在则确定哪些类和方法需要根据哪个条件来进行切面处理,代码如下:

public class RetryPointcut implements Pointcut {

    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new AnnotationMethodMatcher(Retry.class, true);
    }
}

在这里面使用AnnotationMethodMatcher注解匹配对象来完成SpringAOP中的方法匹配,另外一个true参数代表父类的这个注解方法也需要进行处理。

3.Advisor类

这个类便是持有Advice和Pointcut的类,在AOP流程判断时将会使用到这个类,其代码如下:

public class RetryAnnotationAdvisor extends AbstractPointcutAdvisor 
        implements BeanFactoryAware {

    private Advice advice;

    private Pointcut pointcut;

    public RetryAnnotationAdvisor() {
        advice = new RetryInterceptor();
        pointcut = new RetryPointcut();
    }

    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }

    @Override
    public Advice getAdvice() {
        return advice;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        if (this.advice instanceof BeanFactoryAware) {
            ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
        }
    }
}

可以看到十分简洁,只需要把上两个声明过的类实例化赋值即可,这里实现BeanFactoryAware接口目的只是为了展示Advice万一要使用到Bean工厂,那么则可以在这里赋值进去,如果是其它的属性需要赋值进去也可以通过这种方法赋值,这里加不加都无所谓。

现在SpringAOP所需要的三种重要元素都已经获得,接下来只需要把切面对象注入Spring即可。注入Spring工厂有非常多的方法,等下只展示两种比较通用且可插拔的方式。

4.Advisor可插拔式通过@Bean注入到Spring工厂

4.1 可插拔式配置类

代码如下:

public class RetryRegistrarConfiguration {

    @Bean
    public RetryAnnotationAdvisor retryAnnotationAdvisor() {
        return new RetryAnnotationAdvisor();
    }

}

4.2 可插拔式注解开关

代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RetryRegistrarConfiguration.class)
public @interface EnableRetry {
}

如果把Advisor注入到Bean工厂中,在进行AOP切面处理时将会获取Advisor,然后再使用这个Advisor去处理判断需要进行切面增强的bean。

4.3 注入到Spring工厂的实现原理

在AOP集成原理中我们知道入口类便是AbstractAutoProxyCreator这个类,这个类有个获取Advisor的方法getAdvicesAndAdvisorsForBean,在众多的方法入口中都调用了这个方法来获取Advisor对象,接下来则看下其中部分源码:

public abstract class AbstractAdvisorAutoProxyCreator 
        extends AbstractAutoProxyCreator {
    @Override
    @Nullable
    protected Object[] getAdvicesAndAdvisorsForBean(
            Class<?> beanClass, String beanName, 
            @Nullable TargetSource targetSource) {
       // 调用findEligibleAdvisors方法,获取可适用这个bean的AOP处理对象
       List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
       if (advisors.isEmpty()) {
          return DO_NOT_PROXY;
       }
       return advisors.toArray();
    }
    protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, 
            String beanName) {
       // 这个方法将会获取Spring工厂中的所有Advisor对象,通过@Bean注册到Spring
       // 工厂的Advisor bean将会在这个方法中被获取出来
       List<Advisor> candidateAdvisors = findCandidateAdvisors();
       // 获取到所有的Advisor之后再调用findAdvisorsThatCanApply方法判断哪些
       // Advisor支持这个bean的AOP操作,并将Advice作为这个bean的AOP增强处理
       List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(
               candidateAdvisors, beanClass, beanName);
       // 交给子类实现的方法
       extendAdvisors(eligibleAdvisors);
       if (!eligibleAdvisors.isEmpty()) {
          // 不为空则排序,处理的注解为@Order,@Priority
          eligibleAdvisors = sortAdvisors(eligibleAdvisors);
       }
       return eligibleAdvisors;
    }
    protected List<Advisor> findAdvisorsThatCanApply(
            List<Advisor> candidateAdvisors, Class<?> beanClass, 
            String beanName) {
       ProxyCreationContext.setCurrentProxiedBeanName(beanName);
       try {
          // 这里面是具体判断Advisor是否能处理bean的逻辑,主要的判断方式是根据
          // Advice中MethodMatcher对象的matches方法,而我们最开始使用的对象
          // 类型是AnnotationMethodMatcher,这个类中的matches方法将会判断
          // annotationType,即我们添加的@Retry注解,这样便可以判断某个类或者
          // 方法是否支持某个注解功能了,详细的不做过多描述
          return AopUtils.findAdvisorsThatCanApply(candidateAdvisors,
                  beanClass);
       }
       finally {
          ProxyCreationContext.setCurrentProxiedBeanName(null);
       }
    }
}

5.通过BeanPostProcessor接口实现

5.1 实现对应的BeanPostProcessor

实现BeanPostProcessor接口,其源码如下:

public class RetryAdvisingBeanPostProcessor 
        extends AbstractBeanFactoryAwareAdvisingPostProcessor {
    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        // 这里重写setBeanFactory一是为了兼容advisor需要使用bean工厂对象情况
        // 二是将RetryAnnotationAdvisor赋值给advisor对象,如果不需要设置bean
        // 工厂到Advisor中,那么实现InitializingBean接口,在afterPropertiesSet
        // 方法中把RetryAnnotationAdvisor赋值给advisor对象也可以
        super.setBeanFactory(beanFactory);
        RetryAnnotationAdvisor advisor = new RetryAnnotationAdvisor();
        advisor.setBeanFactory(beanFactory);
        this.advisor = advisor;
    }
}

5.2 引入BeanPostProcessor实现类

使用ImportSelector接口来导入上面实现的BeanPostProcessor类,源码如下:

public class RetryImportSelectorRegistrar implements ImportSelector {

    private static final String[] IMPORTS = new String[] {
            RetryAdvisingBeanPostProcessor.class.getName()};

    @Override
    public String[] selectImports(
            AnnotationMetadata importingClassMetadata) {
        return IMPORTS;
    }
}

5.3 可插拔式注解配置

源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RetryImportSelectorRegistrar.class)
public @interface EnableRetry {
}

5.4 实现原理

这种实现方式的原理便是利用bean在初始化之后,再直接对bean进行处理(其实AOP也是实现了BeanPostProcessor接口对bean进行直接处理的),只是这个判断比较直接,不需要再从Spring工厂中获取所有的Advisor然后再判断哪些支持这个bean的处理,而是直接使用本类中的Advisor对象来直接判断,比较适合处理单一功能的注解。比如自定义的@Retry功能比较单一,则可以直接使用这种方式,Spring的@Async注解也是使用这种方式实现的。

具体的实现逻辑在父类AbstractAdvisingBeanPostProcessor中,其关键源码如下:

public abstract class AbstractAdvisingBeanPostProcessor 
        extends ProxyProcessorSupport implements BeanPostProcessor {
    // 用来对bean进行处理的Advisor对象,在自定义的BeanPostProcessor 实现类
    // RetryAdvisingBeanPostProcessor中已经赋值过了,其具体类型便是
    // RetryAnnotationAdvisor
    protected Advisor advisor;
    // 是否可适用的缓存,避免后续再去判断方法和类的条件
    private final Map<Class<?>, Boolean> eligibleBeans = 
            new ConcurrentHashMap<>(256);
    @Override
    public Object postProcessAfterInitialization(Object bean, 
            String beanName) {
       if (this.advisor == null || bean instanceof AopInfrastructureBean) {
          // 如果advisor对象为空或者实现了AopInfrastructureBean接口则直接返回
          // AopInfrastructureBean接口的意思为只能使用Spring AOP那一套处理逻辑
          return bean;
       }
       if (bean instanceof Advised) {
          // 如果类型是Advised则可以直接将advisor添加到对象的advisors集合中
          Advised advised = (Advised) bean;
          // 当然这里也是需要判断bean对象是否满足advisor这个对象的代理条件的
          // isEligible将会先判断eligibleBeans集合的缓存,再使用AopUtils
          // 的canApply方法来判断,和AOP操作的类似
          if (!advised.isFrozen() && 
                  isEligible(AopUtils.getTargetClass(bean))) {
             if (this.beforeExistingAdvisors) {
                advised.addAdvisor(0, this.advisor);
             }
             else {
                advised.addAdvisor(this.advisor);
             }
             return bean;
          }
       }
       // 如果是普通的类,也判断一次代理条件
       if (isEligible(bean, beanName)) {
          // 如果满足则使用代理工厂为这个bean获取代理对象
          ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
          if (!proxyFactory.isProxyTargetClass()) {
             evaluateProxyInterfaces(bean.getClass(), proxyFactory);
          }
          proxyFactory.addAdvisor(this.advisor);
          customizeProxyFactory(proxyFactory);
          return proxyFactory.getProxy(getProxyClassLoader());
       }
       // 全部判断失败,说明无需代理增强
       return bean;
    }
    protected boolean isEligible(Class<?> targetClass) {
       // 从缓存集合中获取上一次的处理结果
       Boolean eligible = this.eligibleBeans.get(targetClass);
       if (eligible != null) {
          return eligible;
       }
       if (this.advisor == null) {
          return false;
       }
       // 调用canApply方法来获取是否满足代理条件,canApply的逻辑便不做赘述,大
       // 致流程已经在4.3中描述过,具体的流程还是需要自己去摸索一遍,否则难以
       // 深入了解
       eligible = AopUtils.canApply(this.advisor, targetClass);
       // 缓存判断结果
       this.eligibleBeans.put(targetClass, eligible);
       return eligible;
    }
}

要想搞懂Spring对于某一个功能的具体实现,最关键的还是去专门挑一个实现源码去看。如本例子中使用AOP的AbstractAutoProxyCreator类具体实现是@Transactional注解,因为这个注解不仅仅是一个简单的单一功能,而需要兼顾Spring事务和其它持久性框架,因此实现比较复杂。把Advisor直接注入到Spring工厂中可以把实现和Spring刷新流程解耦。而@Async因为是一个独立且单一的功能注解,因此使用BeanPostProcessor这种方式来实现,简单直观且处理步骤也比较少。

从这几点就可以看出来Spring框架的灵活性,如果一个框架要广泛的使用,必须得考虑其扩展性,满足不同人的不同实现方式,而不是仅局限于自己单一的实现思维。

 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,我们需要定义一个自定义注解 `@RequiresPermissions`,用于标识需要授权访问的方法,例如: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresPermissions { String[] value(); // 权限值 } ``` 然后,我们需要实现一个切面,用于拦截被 `@RequiresPermissions` 标识的方法,并进行权限校验,例如: ```java @Component @Aspect public class PermissionCheckAspect { @Autowired private AuthService authService; @Around("@annotation(requiresPermissions)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermissions requiresPermissions) throws Throwable { // 获取当前用户 User user = authService.getCurrentUser(); if (user == null) { throw new UnauthorizedException("用户未登录"); } // 获取当前用户的权限列表 List<String> permissions = authService.getUserPermissions(user); // 校验权限 for (String permission : requiresPermissions.value()) { if (!permissions.contains(permission)) { throw new ForbiddenException("没有访问权限:" + permission); } } // 执行目标方法 return joinPoint.proceed(); } } ``` 在切面,我们首先通过 `AuthService` 获取当前用户及其权限列表,然后校验当前用户是否拥有被 `@RequiresPermissions` 标识的方法所需的所有权限,如果没有则抛出 `ForbiddenException` 异常,如果有则继续执行目标方法。 最后,我们需要在 Spring 配置文件启用 AOP 自动代理,并扫描切面所在的包,例如: ```xml <aop:aspectj-autoproxy /> <context:component-scan base-package="com.example.aspect" /> ``` 这样,我们就通过 Spring AOP自定义注解模拟实现了类似 Shiro 权限校验的功能

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值