Spring Boot -- Spring AOP原理及简单实现
一、AOP基本概念
什么是AOP,AOP英语全名就是Aspect oriented programming,字面意思就是面向切面编程。面向切面的编程是对面向对象编程的补充,面向对象的编程核心模块是类,然而在AOP中核心模块是切面。切面实现了多种类型和对象的模块化管理,比如事物的管理。
上面的解释可以你还是看不懂,那么我们举个例子来说明AOP是来解决什么样的问题。我们都知道传统的OOP是自上而下的逻辑开发:
上面这张图形象生动了描述了我们通过浏览器访问一个接口的函数调用过程,我们发送的http请求首先会根据url匹配到对应的controller,然后controller会去调用对应的的service,service再去调用dao、然后将处理的结果返回给浏览器。
1.1、Filter、Interceptor、AOP
那么现在我们有一个需求,想记录发送http请求的客户端IP以及请求接口信息,最简单的方法就是我们在每一个controller方法中调用一个打印相关信息的函数,这样会存在一个问题,有多少接口,我们就会调用多少次日志打印函数。那么有没有一种简单的方法可以实现日志的记录呢,有当然有,我们可以通过Filter或者Interceptor实现请求拦截功能,记录日志信息。那么该有人问什么是Filter、Interceptor,那它们和我们将要说的AOP有什么区别?
AOP使用的主要是动态代理 , 过滤器使用的主要是函数回调;拦截器使用是反射机制 。一个请求过来,先进行过滤器处理,看程序是否受理该请求 。 过滤器放过后 , 程序中的拦截器进行处理 ,处理完后进入 被 AOP动态代理重新编译过的主要业务类进行处理 。
- Filter:和框架无关,过滤器拦截的是URL,可以控制最初的http请求,但是更细一点的类和方法控制不了。
- Interceptor:拦截器拦截的也是URL,拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。
- AOP: 面向切面拦截的是类的元数据(包、类、方法名、参数等) 相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。
三者功能类似,但各有优势,从过滤器 》拦截器 》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是AOP。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;针对日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
1.2、AOP中的一些概念
上面说了那么多题外话,你应该对AOP有了一个初步的理解,下面我们将更深入的介绍AOP,AOP是一种面向切面的编程思想。这些横切性问题,把它们抽象为一个切面,关注点在切面的编程。
AOP主要应用在日志记录,权限验证,事务管理中。我们首先来看一下Spring官方提供的一些有关AOP的基本概念:
1).通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;通知类型,主要有以下几种:
- Before :前置通知,在连接点方法前调用;对应Spring中@Before注解;
- After :后置通知,在连接点方法后调用;对应Spring中的@After注解;
- AfterReturning:返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常;对应Spring中的@AfterReturning注解;
- AfterThrowing:异常通知,当连接点方法异常时调用;对应Spring中的@AfterThrowing注解;
- Around:环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法;对应Spring中的@Around注解;
2).连接点(Join Point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对象中的方法就是一个连接点;
3).切点(Pointcut): 就是连接点的集合;对应Spring中的@Pointcut注解;
4).切面(Aspect): 切面是通知和切点的结合;对应Spring中的注解@Aspect修饰的一个类;
5).目标对象(Target object):即被代理的对象;
6).代理对象(AOP proxy):包含了目标对象的代码和增强后的代码的那个对象;
我们利用上面这一张图来说一下目标对象和代理对象的关系,代理对象可以看作是目标对象的加强版,它是对目标对象中方法功能的一个扩充。代理对象的实现主要有两种,一种是基于jdk动态代理的,这要求目标对象必须是接口的实现;而另一种实现是基于cglib,即代理对象是继承自目标对象。
1.3、切点匹配表达式
切面是如何拦截到指定的类的元数据(包、类、方法名、参数等) 的呢,这是通过切点匹配表达式实现的。目前Spring支持的切点匹配表达式主要有以下几种:
- execution:可以定义到的最小粒度是方法,修饰符,包名,类名,方法名,Spring AOP主要也是使用这个匹配表达式;
- within:只能定义到类;例如@Pointcut(within(com.jnu.example.*))
- this:当前生成的代理对象的类型匹配;
- target:目标对象类型匹配;
- args:只针对参数;
- annotation:针对注解;
例如: execution (* com.sample.service..*. *(..))
整个表达式可以分为五个部分:
- 1、execution()::表达式主体;
- 2、第一个*号:表示返回类型, *号表示所有的类型;
- 3、包名:表示需要拦截的包名,包名后面的..,表明com.sample.service包、及其子包;
- 4、第二个*号:表示类名,*号表示所有的类;
- 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个点表示任何参数;
- 关于Spring AOP注解的使用,我这里就不介绍了,网上有大量的博客介绍如何使用。我们接下来就带大家来实现一个类似within切点匹配表达式的效果。
二、AOP简单实现
在开始之前,我们先引入一个概念,Spring扩展点和后置处理器,我们知道Spring IOC可以对应用程序中的java bean做一个集中化的管理,从而使我们从繁琐的new Object()中解锁出来。
其核心就是先创建一个bean工厂,也就是我们常说的beanFactory,通过beanFactory来生产出我们应用程序中所需要的java ben,这些java bean大多都是被Spring代理之后的对象。
2.1、后置处理器
今天呢,我跟大家介绍的后置处理器呢,有三个,它们在Spring启动过程中均会被执行,具体详情可以阅读Spring启动过程源码分析:
BeanFactoryPostProcessor:可以插手beanFactory的生命周期,是针对整个工厂生产出来的BeanDefinition作出修改或者注册,作用于BeanDefinition时期;
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.beans.factory.config; import org.springframework.beans.BeansException; @FunctionalInterface public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException; }
BeanPostProcessor:可以插手bean的生命周期,该接口定义了两个方法,分别在bean初始化前后执行(注意这里的初始化是指对象创建之后,属性赋值之前),方法的返回值为一个object,这个object呢就是我们存在于容器的对象了(所以这个位置我们是不是可以对我们的bean做一个动态的修改,替换等等操作,所以这也是我们Spring的扩展点之一,后面结合我么自己手写AOP来详细讲解这个扩展点的应用);
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.beans.factory.config; import org.springframework.beans.BeansException; import org.springframework.lang.Nullable; public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
ImportSelector:借助@Import注解,可以动态实现将一个类是否交由Spring管理,常用作开关操作;
在Spring处理我们的java类的时候,会分成四种情况去处理 :
- 普通类:就是@Component,@Service,@Repository等注解的类
- Import进来的类:这里呢,又分为三种情况:
a)Import一个普通类:@Import(A.class)
b)Import一个Registrar:比如我们的aop @Import(AspectJAutoProxyRegistrar.class)
c)Import一个ImportSelector:比如:@import(ImportSelector.class) ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,Spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中;
至于Spring在什么时候处理的呢,我大致叙述一下,有兴趣的可以自己去研究下spring源码: 对于普通类,Spring在扫描的时候,就将扫描出来的java类转换成我们的BeanDefinition,然后放入一个BeanDefinitionMap中去;
对于@import的三种情况,处理就在ConfigurationClassPostProcessor(该类是BeanDefinitionRegistryPostProcessor后置处理器的一个实现,同时这也是我们spring内部自己维护的唯一实现类)类中,具体处理Import的核心代码如下,if-else 很容易可以看出spring对于我们Import三种类型的处理:
/** * 处理我们的@Import注解,注意我们的@Import注解传入的参数,可能有三种类型 * 1,传入一个class类,直接解析 * 2,传入一个registrar,需要解析这个registrar * 3,传入一个ImporterSelector,这时候会去解析ImporterSelector的实现方法中返回的数组的class * */ private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, boolean checkForCircularImports) { if (importCandidates.isEmpty()) { return; } if (checkForCircularImports && isChainedImportOnStack(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { this.importStack.push(configClass); try { for (SourceClass candidate : importCandidates) { //处理我们的ImportSelector if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports Class<?> candidateClass = candidate.loadClass(); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); ParserStrategyUtils.invokeAwareMethods( selector, this.environment, this.resourceLoader, this.registry); if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) { this.deferredImportSelectors.add( new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector)); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames); //注意可能我们ImportSelector传入的类上还有可能会Import,所以这里,spring采用了 //一个递归调用,解析所有的import processImports(configClass, currentSourceClass, importSourceClasses, false); } } //处理我们的Registrar else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions Class<?> candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); ParserStrategyUtils.invokeAwareMethods( registrar, this.environment, this.resourceLoader, this.registry); //添加的一个和Importselector方式不同的map中,sprig对两种方式传入的类注册方式不同 configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class //最后如果是普通类,传入importStack后交由processConfigurationClass进行注册处理 this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass)); } } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } finally { this.importStack.pop(); } } }
2.2、AOP实现思路
同样根据如上原理,下面我们便可以来模拟我们的AOP,如果有点基础的可能应该会知道,Spring是基于我们的动态代理实现的(先不考虑是cglib还是jdk动态代理),结合我们AOP使用,那么我们就需要解决如下几个问题:
- 我们知道开启和关闭aop需要注解@EnableAspectJAutoProxy,如何实现,结合上文,我们可以使用@import(ImportSelector.class)来实现该功能;
- 如何确定代理关系,即哪些是我们需要代理的目标对象和其中的目标方法,以及哪些方法是要增强到目标对象的目标方法上去的?
- 如何实现目标对象的替换,就是我们在getBean的时候,如何根据目标对象来获取到我们增强后的代理对象?
2.3、AOP实现代码
我们首先需要构建一个项目,创建如下包:
annotation:存放我们所有自定义的注解;
holder:存放代理类信息;
processor:存放后置处理器的实现类;
selector:存放ImportSelector的实现类;
1、首先创建AOP中使用到的注解,这些注解类名称和Spring自带的一致:
After.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 后置通知注解类 * * @author zy * @since 2020/6/20 17:32 */ @Retention(RetentionPolicy.RUNTIME) public @interface After { String value() default ""; }
Around.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 环绕通知注解类 * * @author zy * @since 2020/6/20 17:33 */ @Retention(RetentionPolicy.RUNTIME) public @interface Around { String value() default ""; }
Before.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 前置通知注解类 * * @author zy * @since 2020/6/20 17:31 */ @Retention(RetentionPolicy.RUNTIME) public @interface Before { String value() default ""; }
Aspect.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Aspect { }
EnableAspectAutoProxy.java:
package com.zy.blog.common.annotation; import com.zy.blog.common.selector.CustomizedImportSelector; import org.springframework.context.annotation.Import; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * aop开关注解 * 如果在App启动类上加入该注解,将会将我们的后置处理器的实现交给spring管理,spring才能去扫描得到这个类,才能去执行我们的自定义的后置处理器里面的方法,才能实现我们的aop的代理 * * @author zy * @since 2020/6/20 17:22 */ @Retention(RetentionPolicy.RUNTIME) @Import(CustomizedImportSelector.class) public @interface EnableAspectAutoProxy { }
上面我们已经说过,在@import(ImportSelector.class)的时候,Spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中,因此我们定义ImportSelector的实现类CustomizedImportSelector:
package com.zy.blog.common.selector; import com.zy.blog.common.processor.CustomizedBeanFactoryPostProcessor; import com.zy.blog.common.processor.CustomizedBeanPostProcessor; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; /** * 自定义aop实现,提交给spring容器 * ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,spring会把我们返回方法里面的类全部注册到 * BeanDefinitionMap中,继而将对象注册到Spring容器中\ * * @author zy * @since 2020/6/20 17:16 */ public class CustomizedImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { return new String[]{CustomizedBeanFactoryPostProcessor.class.getName(), CustomizedBeanPostProcessor.class.getName()}; } }
这样就可以将CustomizedBeanFactoryPostProcessor和CustomizedBeanPostProcessor类注册到BeanDefinitionMap中,继而将对象注册到Spring容器中。
Pointcut.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 切点 * * @author zy * @since 2020/6/21 17:54 */ @Retention(RetentionPolicy.RUNTIME) public @interface Pointcut { String value() default ""; }
同时定义一个工具类AspectUtil:
package com.zy.blog.common.util; import com.zy.blog.common.annotation.*; import com.zy.blog.common.holder.ProxyBeanHolder; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * aop切面注解工具类 * AOP 领域中的特性术语: * 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;对应注解@After、@Before、@Around ... * 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对应中的方法就是一个连接点 * 切点(PointCut): 就是连接点的集合;对应注解@PonitCut * 切面(Aspect): 切面是通知和切点的结合;对应注解@Aspect修饰的一个类 * * @author zy * @since 2020/6/20 15:27 */ public class AspectUtil { /* * 指定切面注解类 */ public static final String ASPECT = Aspect.class.getName(); /* * 指定切点注解类 */ public static final String POINTCUT = Pointcut.class.getName();; /* * 指定前置通知注解类 */ public static final String BEFORE = Before.class.getName();; /* * 指定后置通知注解类 */ public static final String AFTER = After.class.getName();; /* * 指定环绕通知注解类 */ public static final String AROUND = Around.class.getName();; /* * 存放AOP代理的全部目标类 目标类 ->(切面类,代理方法,通知注解) 如:com.jnu.example.blog.service.IArticleService -> [(com.jnu.example.blog.AspectTest, testBefore, com.zy.blog.common.annotation.Before)] */ public static volatile Map<String, List<ProxyBeanHolder>> classzzProxyBeanHolder = new ConcurrentHashMap<>(); }
2、问题2
针对问题2,我们可以定义一个BeanFactoryPostProcessor的实现类,完成对所有BeanDefinition的扫描,找出我们定义的所有的切面类,然后循环里面的方法,找到切点、以及所有的通知方法,然后根据注解判断通知类型(也就是前置,后置还是环绕),最后解析切点的内容,扫描出所有的目标类,放入我们定义好的容器中。
创建ProxyBeanHolder类,用于存放代理信息:
package com.zy.blog.common.holder; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 自定义数据结构 用于存放代理信息 * * @author zy * @since 2020/6/20 15:03 */ @Data @AllArgsConstructor @NoArgsConstructor public class ProxyBeanHolder { /** * 切面类名称 */ private String className; /** * 代理方法 如:testBefore */ private String methodName; /** * 通知注解类名称 如:{@link com.zy.blog.common.annotation.Before} */ private String annotationName; }
定义我们的注册类CustomizedBeanFactoryPostProcessor,用于注册我们的目标对象和切面对象之间的关系:
package com.zy.blog.common.processor; import cn.hutool.core.util.StrUtil; import com.zy.blog.common.holder.ProxyBeanHolder; import com.zy.blog.common.util.AspectUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.core.type.AnnotationMetadata; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.util.*; /** * BeanFactory初始化后调用 * 定义注册类 用于注册目标对象和切面对象之间的关系 * 1. 切面对象指的就是被@Aspect注解修饰的配置类对象 * 2. 目标对象就是被@Pointcut("execution(public * com.zy.example.bigdataplat.admin.controller.*.*(..))")拦截的bean * 完成所有BeanDefinition的扫描,找出我们所有的切面类,然后循环里面的方法,找到切点、以及所有的通知方法,然后根据注解判断通知类型(也就是前置,后置还是环绕), * 最后解析切点的内容,扫描出所有的目标类,放入我们定义好的容器中。 * * @author zy * @since 2020/6/20 15:34 */ @Slf4j public class CustomizedBeanFactoryPostProcessor implements BeanFactoryPostProcessor{ /* * 保存所有的切点修饰的方法名、以及切点的value()函数值 */ private Map<String,String> pointCutMap = new HashMap<>(); @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) { //获取所有的bean name String[] beanDefinitionNames = configurableListableBeanFactory.getBeanDefinitionNames(); for(String beanDefinitionName:beanDefinitionNames){ BeanDefinition beanDefinition = configurableListableBeanFactory.getBeanDefinition(beanDefinitionName); //判断bean是否被注解修饰 if(beanDefinition instanceof AnnotatedBeanDefinition){ //获取bean上的注解元数据 AnnotationMetadata metadata = ((AnnotatedBeanDefinition)beanDefinition).getMetadata(); //获取注解类型 如:{org.springframework.boot.autoconfigure.SpringBootApplication,com.jnu.example.db.annotation.CoreMapperScan,com.zy.blog.common.annotation.EnableAspectAutoProxy} Set<String> annotations = metadata.getAnnotationTypes(); //遍历所有注解、找到aop切面注解类 for(String annotation:annotations){ //如果被@Aspect注解修饰 表明这是一个切面类 查找切面类中指定的切点(@Pointcut) if(annotation.equals(AspectUtil.ASPECT)){ doScan((GenericBeanDefinition)beanDefinition); } } } } } /** * 扫描切面类所有方法上的注解 * 1、如果有@Pointcut注解:保存对应的切点 * 2、找到所有的通知注解@After、@Before、@Around,获取对应的切点 * 3、保存 * @param beanDefinition */ private void doScan(GenericBeanDefinition beanDefinition){ try{ //获取切面类名 如:com.jnu.example.blog.TestAop String className = beanDefinition.getBeanClassName(); //加载类 Class<?> beanDefinitionClazz = Class.forName(className); //获取所有方法 Method[] methods = beanDefinitionClazz.getMethods(); //遍历切面类所有方法 第一遍历找到所有切点 for(Method method:methods){ //获取注解类名 如:@com.zy.blog.common.annotation.Pointcut Annotation[] annotations = method.getAnnotations(); for(Annotation annotation:annotations){ //获取注解类名 如:@com.zy.blog.common.annotation.Pointcut String annotationName = annotation.annotationType().getName(); if(AspectUtil.POINTCUT.equals(annotationName)){ String value = getAnnotationValue(annotation); if(StrUtil.isNotBlank(value)) { //获取切点指定包 pointCutMap.put(method.getName()+"()", value); } } } } //遍历切面类所有方法 第二次遍历找到所有通知 for(Method method:methods) { //获取注解类名 如:@com.zy.blog.common.annotation.After Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { //获取注解类名 如:@com.zy.blog.common.annotation.After String annotationName = annotation.annotationType().getName(); //如果是Before、或者After、Around if (AspectUtil.BEFORE.equals(annotationName) || AspectUtil.AFTER.equals(annotationName) || AspectUtil.AROUND.equals(annotationName)) { try { doScan(className, method, annotation); } catch (Exception e) { log.error(e.getMessage(), e); } } } } }catch(ClassNotFoundException e){ log.error(e.getMessage(),e); } } /** * 扫描切点指定的所有目标类 保存到map中:目标类 ->(切面类,代理方法,通知注解) * @param className:切面类 * @param method: 通知方法 * @param annotation:通知注解 @after或者@before、@Around */ private void doScan(String className,Method method,Annotation annotation) throws URISyntaxException { //保存代理信息 ProxyBeanHolder proxyBeanHolder = new ProxyBeanHolder(className,method.getName(),annotation.annotationType().getName()); //获取通知value,即切点方法 String pointCutMethod = getAnnotationValue(annotation); //获取包路径 String packagePath = pointCutMap.get(pointCutMethod); //遍历包、找到包下的所有目标类 if(StrUtil.isNotBlank(packagePath)){ traverseDir(packagePath,proxyBeanHolder); } } /* * 遍历file对象 */ private void traverseDir(String packagePath,ProxyBeanHolder proxyBeanHolder) throws URISyntaxException { //获取目标包路径:classpath + 包名 String targetPackagePath = this.getClass().getResource("/").toURI().getPath() + packagePath.replace(".","/"); File file = new File(targetPackagePath); File[] fileList = file.listFiles(); if(fileList == null){ return; } List<ProxyBeanHolder> proxyBeanHolderList = null; //遍历包路径 for(File fp:fileList){ //判断是不是文件 if(fp.isFile()){ String targetClass = packagePath + "." + fp.getName().replace(".class",""); try{ proxyBeanHolderList = AspectUtil.classzzProxyBeanHolder.get(targetClass); }catch (Exception e){ log.error(e.getMessage(),e); } if(proxyBeanHolderList == null){ proxyBeanHolderList = new Vector<>(); } proxyBeanHolderList.add(proxyBeanHolder); AspectUtil.classzzProxyBeanHolder.put(targetClass,proxyBeanHolderList); }else{ traverseDir(packagePath + "." + fp.getName(),proxyBeanHolder); } } } /** * 获取注解value()方法的值 * @param annotation:注解 */ private String getAnnotationValue(Annotation annotation){ //获取注解上的所有方法 不包括继承的方法 Method[] annotationMethods = annotation.annotationType().getDeclaredMethods(); //遍历注解每一个方法 for(Method annotationMethod:annotationMethods){ //如果方法名称是value if(annotationMethod.getName().equals("value")){ try{ //执行value()方法 第一个参数代表调用的对象,第二个参数传递的调用方法的参数 return (String)annotationMethod.invoke(annotation,null); }catch(IllegalAccessException | InvocationTargetException e){ log.error(e.getMessage(),e); } } } return ""; } }
3、问题3
针对问题3,我们可以定义一个BeanPostProcessor的实现类CustomizedBeanPostProcessor,在bean实例化之后,在放入容器之前,进行一个条件过滤,如果当前对象是我们的目标对象(即在我们定义好的Map中),则对对象进行代理,将目标对象替换成代理对象返回即可。
注:Spring实现AOP采用cglib和jdk动态代理两种方式,@EnableAspectJAutoProxy(proxyTargetClass=true)可以加开关控制,如果不加,目标对象如果有实现接口,则使用jdk动态代理,如果没有就采用cglib(因为我们知道cglib是基于继承的))
我们这里实现,都简单粗暴一点,统一采用cglib代理,这样就可以完成对任意对象的代理了。
package com.zy.blog.common.processor; import com.zy.blog.common.util.AspectUtil; import lombok.SneakyThrows; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.cglib.proxy.Enhancer; import java.lang.reflect.Field; import java.lang.reflect.Modifier; /** * bean实例化之后调用 * 我们可以利用BeanPostProcessor,在bean实例化之后,在放入容器之前,进行一个条件过滤,如果当前对象是我们的目标对象(即在我们定义好的Map中), * 则对对象进行代理,将目标对象替换成代理对象返回即可 * * @author zy * @since 2020/6/20 16:52 */ public class CustomizedBeanPostProcessor implements BeanPostProcessor { /** * bean实例化之后放到Spring IOC容器之前 */ @SneakyThrows @Override public Object postProcessAfterInitialization(Object bean, String beanName) { //获取bean class Class clazz = bean.getClass(); //获取bean类名 String className = clazz.getName(); //如果这个bean是已经被代理后的 获取被代理前的类名 className = className.substring(0,className.indexOf("$$") > 0 ? className.indexOf("$$"):className.length()); Object object = bean; //对目标对象进行代理 采用cglib代理 继承方式 if( AspectUtil.classzzProxyBeanHolder.containsKey(className)){ // 创建加强器,用来创建动态代理类 Enhancer enhancer = new Enhancer(); // 为加强器指定要代理的业务类(即:为下面生成的代理类指定父类) enhancer.setSuperclass(clazz); // 设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦 enhancer.setCallback(new CustomizedProxyInterceptor(AspectUtil.classzzProxyBeanHolder.get(className))); // 创建动态代理类对象并返回 object = enhancer.create(); //获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段 Field[] fields = clazz.getDeclaredFields(); //遍历字段 for(Field field:fields){ //排除静态方法 if (Modifier.isFinal(field.getModifiers())){ continue; } //设置私有字段可以访问 field.setAccessible(true); //实现相同字段赋值,解决代理对象中的自动注入bean为空的问题 field.set(object,field.get(bean)); } } return object; } /** * bean实例化之后放到Spring IOC容器之后执行 */ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; } }
2.4、测试
我们定义一个AspectTest切面类,在调用com.jnu.example.service包类中的方法前会执行我们的前置通知:
package com.zy.blog.common; import com.zy.blog.common.annotation.Aspect; import com.zy.blog.common.annotation.Before; import com.zy.blog.common.annotation.Pointcut; import org.springframework.stereotype.Component; /** * 这是一个切面类 不可以使用@Configuration注解(会被动态代理) * * @author zy * @since 2020/6/21 10:12 */ @Component @Aspect public class AspectTest { /** * 定义一个切点 目前只支持指定包路径 */ @Pointcut("com.zy.blog.server") public void servicePointCut(){ } /** * 前置通知 */ @Before("servicePointCut()") public void testBefore(){ System.out.println("before -----------------------,测试成功"); } }
最后就是开启我们自定义的AOP功能,在我们的BlogApplication类上加入@EnableAspectAutoProxy,就会将我们定义的后置处理器的实现类交给Spring IOC容器,Spring才能去扫描得到这些类,才能去执行我们自定义的后置处理器里面的方法。
package com.zy.blog.api; import com.zy.blog.common.annotation.EnableAspectAutoProxy; import com.zy.blog.server.annotation.CoreMapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Spring Boot程序入口 * * @author zy * @since 2020/4/14 22:40 */ @SpringBootApplication(scanBasePackages ={"com.zy.blog"}) @CoreMapperScan @EnableAspectAutoProxy public class BlogApplication { public static void main(String[] args) { SpringApplication.run(BlogApplication.class, args); } }
三、源码下载
源码放置在gitee:blog-server(common模块)
参考文章: