您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
源码学习之Spring AOP
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
源码学习之Spring AOP
自猿其说Tech
2022-06-02
IP归属:未知
591浏览
计算机编程
### 1 前言 实际工作中,为了满足业务需要,或者为多个不具有继承关系的对象引入一个公共行为,例如日志、监控、事务等,如果我们直接修改源码,会破坏原有的代码逻辑,可能产生大量重复的代码,也可能会造成一定的风险,这时候我们就会想到借助Spring AOP来做到这一点。 UserService是一个普通的bean ```java @Component public class UserService { public void test() { System.out.println("test..."); } } ``` 定义切面 ```java @Aspect @Component public class MyAspect { @Before("execution(public void com.jd.service.UserService.test())") public void myBefore(JoinPoint joinPoint) { System.out.println("before..."); } @After("execution(public void com.jd.service.UserService.test())") public void myAfter(JoinPoint joinPoint) { System.out.println("after..."); } @Around("execution(public void com.jd.service.UserService.test())") public Object cwAround(ProceedingJoinPoint point) { Object obj = null; System.out.println("around before..."); try { obj = point.proceed(); }catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("around after..."); return obj; } } ``` 开启对Spring AOP支持的配置 ```java @ComponentScan("com.jd.service") @EnableAspectJAutoProxy public class AppConfig { } ``` 测试 ```java public class Test { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = (UserService) applicationContext.getBean("userService"); userService.test(); } } ``` 不出意外,我们会看到控制台中打印了如下的结果:  那么,Spring究竟是如何实现AOP的呢?下面通过本文来进行详细分析(文章有些长,可以先收藏待有时间后再慢慢看)。 ### 2 Spring AOP原理介绍 AOP: aspect object programming 面向切面编程,其功能就是让关注点代码与业务代码分离。关于AOP中的概念本身是比较难理解的,Spring官网上是这么说的: Let us begin by defining some central AOP concepts and terminology. These terms are not Spring-specific. Unfortunately, AOP terminology is not particularly intuitive.However, it would be even more confusing if Spring used its own terminology. 意思是,AOP中的这些概念不是Spring特有的,不幸的是,AOP中的概念不是特别直观的,但是,如果Spring重新定义自己的那可能会导致更加混乱。 我们先看下AOP中主要的概念定义: - Aspect:表示切面,比如被@Aspect注解的类就是切面,可以在切面中去定义Pointcut、Advice等等; - Join point:表示连接点,表示一个程序在执行过程中的一个点,比如一个方法的执行,比如一个异常的处理,在Spring AOP中,一个连接点通常表示一个方法的执行。 - Advice:表示通知,表示在一个特定连接点上所采取的动作。Advice分为不同的类型,后面详细讨论,在很多AOP框架中,包括Spring,会用Interceptor拦截器来实现Advice,并且在连接点周围维护一个Interceptor链; - Pointcut:表示切点,用来匹配一个或多个连接点,Advice与切点表达式是关联在一起的,Advice将会执行在和切点表达式所匹配的连接点上; - Introduction:可以使用@DeclareParents来给所匹配的类添加一个接口,并指定一个默认实现; - Target object:目标对象,被代理对象; - AOP proxy:表示代理工厂,用来创建代理对象的,在Spring Framework中,要么是JDK动态代理,要么是CGLIB代理; - Weaving:表示织入,表示创建代理对象的动作,这个动作可以发生在编译时期(比如Aspejctj),或者运行时,比如Spring AOP。 下面用一个图描述下这些概念之间关系:  下面是Spring创建代理类的时序图:  下面是代理对象调用的时序图,以JDK动态代理为例:  ### 3 Spring AOP底层源码解析 #### 3.1 AOP基于注解的入口 在前言中的示例中,开启Sping AOP最重要的一个注解就是@EnableAspectJAutoProxy,这个注解主要就是往Spring容器中添加了一个AspectJAutoProxyRegistrar类型的Bean。  具体看看注解里面的 AspectJAutoProxyRegistrar 这个类:  再往下看:  其实,注解@EnableAspectJAutoProxy的作用就是通过@Import注册 AOP 入口类AnnotationAwareAspectJAutoProxyCreator,从而开启了AOP,并设置了两个属性proxyTargetClass和exposeProxy。 ##### proxyTargetClass: true - 目标对象实现了接口– 使用 CGLIB 代理机制 - 目标对象没有接口(只有实现类) – 使用 CGLIB 代理机制 false - 目标对象实现了接口– 使用 JDK 动态代理机制(代理所有实现了的接口) - 目标对象没有接口(只有实现类) – 使用 CGLIB 代理机制 ##### exposeProxy: 用于设置代理对象是否需要暴露,说白了就是确认是否需要把代理对象设置到ThreadLocal中。 #### 3.2 是否生成代理 我们再来看AnnotationAwareAspectJAutoProxyCreator这个类:  在类的层级中,我们看到AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcessor接口,当Spring在创建某个Bean时,就会进入到它对应的生命周期方法中,在某个Bean初始化之后,会调用wrapIfNecessary方法判断该 bean 是否生成代理。 在父类AbstractAutoProxyCreator的postProcessAfterInitialization中代码如下:  重点看一下这个方法 wrapIfNecessary,如果有切面就会生成对应的代理:  getAdvicesAndAdvisorsForBean方法就是判断当前 bean 是否有切面 advisor,如果有切面就会走到 createProxy 方法,生成代理对象然后返回。 我们继续分析,看下Spring是如何寻找切面Advisor的。先看找合格切面的逻辑:  在findEligibleAdvisors方法中会寻找合格的切面,主要包含两个过程:找到候选的切面和判断候选的切面是否适用于当前Bean上面,对应的方法就是findCandidateAdvisors和findAdvisorsThatCanApply。  ##### 3.2.1 获取Advisor集合 下面重点看一下 findCandidateAdvisors()寻找合格切面的过程:  重点看一下这个 buildAspectJAdvisors()方法,此方法的处理逻辑如下: 首先,获取 spring 容器中的所有 bean 的名称 BeanName,组成 BeanNames 数组。  然后,循环遍历 BeanNames 数组,判断该类上面是否有@Aspect 注解,如果有则是我们要找的,添加到 aspectNames。  再把带@Aspect 注解的单个 beanName 包装成 MetadataAwareAspectInstanceFactory 工厂,用于获取 getAdvisors 创建切面 advisor集合使用(注:一个 bean 当中可能有多个 advisor,单个 advisor 是由 poincut 和 advice 组成,因此每个 bean对应的都是一个 advisors 集合)  然后,创建切面 advisor 对象,下面是 getAdvisors 过程,循环单个 Bean 里面的除了@PointCut 注解的方法,判断当前方法上面的注解是否在@Around、@Before、@After、 @AfterReturning、 @AfterThrowing 中间,如果包含在这些注解之中, 就把注解里面的信息,比如表达式,argNames,注解类型等信息封装成对象 AspectJAnnotation。  getAdvisors方法中又包含了以下几步: - 从工厂中获取有@Aspect 注解的类 Class; - 从工厂中获取有@Aspect 注解的类的名称; - 创建工厂的装饰类; - 从上面包装的工厂中获取对应的带@Aspect 注解的单个 Class 对象,遍历这个 aspectClass 对象中的所有没有被 @Pointcut 标注的方法,然后把收集到的方法进行过滤;  再看一下getAdvisor这个方法,循环遍历没有@Pointcut 注解的方法,并且把每个方法组装成 Advisor 对象。  ##### PointCut对象创建 我们先看一下如何获取 PointCut 对象的,看一下 getPointcut()这个方法,把注解信息封装成 AspectJAnnotation 对象,封装一个 PointCut 类,并且把前面从注解里面解析的表达式设置进去  重点看一下,怎么封装成AspectJAnnotaition 对象的,看一下这个方法 findAspectJAnnotationOnMethod,如下所示:  遍历所有注解(Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),找到对应的@Around@Before等注解封装成AspectJAnnotation对象。  下面再看一下 new AspectJAnnotation<>(result)这里面对应的内容。  以上,我们拥有了AspectJExpressionPointcut,有了 pointcut 对象。 ##### Advice对象创建 下面我们再看一下advice 对象的创建过程   看一下 instantiateAdvice 这个方法,如下图所示:  继续看 getAdvice 这个方法,这个方法是获取有@Aspect 注解的类,然后把方法上面的注解包装成 AspectJAnnotation 对象:  看下对应的 AspectJAnnotation 这个类, 这个类中包括 6 种注解类型:  我们再往下看 getAdvice 这个方法:   不同的Advice对象如下:AspectJAroundAdvice,AspectJAfterAdvice,AspectJAfterThrowingAdvice,AspectJMethodBeforeAdvice,AspectJAfterReturningAdvice,最终把注解对应的Advice对象和 pointCut对象封装成Advisor对象。 ##### 小结 上面的一大堆方法就是为了给一个类中的某一个方法包装成对应的 Advisor,一个类中可能有多个不同的方法,每个方法都包装成对应的 Advisor 对象,这样对应的一个类中就会有一个List<Advisor>集合,返回给前面。 ##### 3.2.2 寻找匹配的Advisor集合 上述我们分析了Spring如何把工程中所有有@Aspect这个注解的类封装成List<Advisor>返回,接下来,我们再继续分析Spring是如何判断当前bean是否能匹配到集合中的切面,即集合中是否包含当前bean的切面信息。 我们从findAdvisorsThatCanApply方法看下匹配合格切面的过程:  继续看AopUtils.findAdvisorsThatCanApply这个方法,主要是寻找所有Advisor中适用于当前class的Advisor,引介切面和普通的处理不太一样,所以进行了分开处理。  真正的匹配是在canApply方法中   #### 3.3 代理类的创建 当某个Bean有切面Advisor匹配时,便进入到了创建代理对象的过程中,我们回到AbstractAutoProxyCreator类的postProcessBeforeInstantiation方法中。  看下创建代理的过程: 对于代理类的创建及处理,Spring委托给了ProxyFactory去处理,在createProxy方法中,主要就是对ProxyFactory的初始化操作,进而对真正的创建代理做准备,这些初始化操作包括如下内容。 - 获取当前类的属性; - 添加代理接口; - 封装Advisor并加入到ProxyFactory中; - 设置要代理的类; - 提供了定制代理的方法customizeProxyFactory; - 获取代理对象;   其中,封装Advisor并加入到ProxyFactory中和创建代理是两个相对繁琐的过程。 buildAdvisors方法将切面对象重新包装,把自定义的MethodInterceptor类型的类包装成Advisor切面类并加入到代理工厂中。   proxyFactory.getProxy方法会根据proxyTargetClass参数和是否实现接口,来判断是采用JDK代理还是CGLIB代理。    JdkDynamicAopProxy 1. 在构造JdkDynamicAopProxy对象时,会先拿到被代理对象自己所实现的接口,并且额外的增加SpringProxy、Advised、DecoratingProxy三个接口,组合成一个Class[],并赋值给proxiedInterfaces属性; 2. 并且检查这些接口中是否定义了equals()、hashcode()方法; 3. 执行 Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this) ,得到代理对象,JdkDynamicAopProxy作为InvocationHandler,代理对象在执行某个方法时,会进入到JdkDynamicAopProxy的**invoke()**方法中。 ObjenesisCglibAopProxy 1. 创建Enhancer对象; 2. 设置Enhancer的superClass为通过ProxyFactory.setTarget()所设置的对象的类; 3. 设置Enhancer的interfaces为通过ProxyFactory.addInterface()所添加的接口,以及SpringProxy、Advised、DecoratingProxy接口; 4. 设置Enhancer的Callbacks为DynamicAdvisedInterceptor; 5. 最后创建一个代理对象,代理对象在执行某个方法时,会进入到DynamicAdvisedInterceptor的intercept()方法中。 #### 3.4 代理类的调用 上面Spring已经帮我们创建出来代理对象了,现在我们继续分析拿到代理对象后的调用,以JDK动态代理为例,CGLIB调用逻辑类似。 代理对象创建完成后,当发生代理对象调用时,肯定会调用到实现了invocationHandler接口的类,这个类就是JdkDynamicAopProxy,必定会调用到该类的invoke 方法。 Ok,我们看看invoke方法:  从代理工厂中拿到切面,并且跟当前被代理类和当前被调用方法匹配,如果匹配就返回切面中的advice 对象,这就是advice 执行链,其实就是对应的 interceptor List 列表:  我们看下getInterceptorsAndDynamicInterceptionAdvice这个方法的逻辑:  继续往下跟,从代理工厂中获得该被代理类的所有切面advisor,config就是代理工厂对象  然后再开始一轮遍历,如果切面的pointCut和被代理对象Class类是匹配的,说明被代理对象这个Class类是切面要拦截的对象。  判断匹配的被代理对象中的方法是否是切面pointcut需要拦截的方法  如上图所示,获取到切面advisor中的advice,并且包装成MethodInterceptor类型的对象,接下来我们看一下getInterceptors方法,此步骤最关键的是对不同类型的advice进行了统一包装,方便后续进行统一的代码调用。 如下图所示:  如果是MethodInterceptor类型的,如:AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice,则直接添加到拦截器;如果是MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter,则需要advice包装成MethodInterceptor类型的advice。 我们再返回到invoke方法中,继续往下看,从代理工厂中拿过滤器链,如果该方法没有执行链,则说明这个方法不需要被拦截,则直接反射调用  如果有执行连,即chain不为空,我们继续分析下proceed这个方法的调用逻辑。 我们以具体的示例来分析一下AOP调用的过程,此例中共有一个 before、一个after和一个aroud切面,通过调试发现拦截器数组中的顺序为AspectJAfterAdvice、AspectJAroundAdvice、MethodBeforeAdviceInterceptor,因此接下来先执行AspectJAfterAdvice内部的invoke方法。  此时AspectJAfterAdvice这里有一个mi.proceed()这个方法又会回到ReflectiveMethodInvocation这个类中,这时候索引又会+1,然后从链中后去下一个advice,这时候就会找到AspectJAroundAdvice中的invoke,如下图所示:  此方法会调用到MyAspect中cwAround方法  通过调用point.proceed()这个方法又会回到ReflectiveMethodInvocation这个类中,这时候索引又会+1,然后从链中后去下一个advice,然后又开始调用MethodBeforeAdviceInterceptor中的invoke ,如下图所示:  此时先调用Aspect中的before方法,然后调完以后,这里有一个mi.proceed()这个方法又会回到 ReflectiveMethodInvocation这个类中,这时候索引又会+1,此时的索引值已经等于数组大小-1了。直到数组链中全部调用完后会调用到具体的invokeJoinpoint方法:  此时就会调用被代理的方法了,调用完毕后,返回的值通过MethodBeforeAdviceInterceptor返回到AspectJAroundAdvice,最终返回到MyAspect中的cwAround方法,继续执行后面的逻辑。cwAround方法执行完毕后,把obj返回给上一个执行的advice->AspectJAfterAdvice中的invoke完成值的返回,最终会执行finally方法调用,完成@After方法调用,至此,代理对象的调用就全部结束了。 ### 4 总结 OOP表示面向对象编程,是一种编程思想,AOP表示面向切面编程,也是一种编程思想。Spring AOP就是Spring为了让程序员更加方便的做到面向切面编程所提供的技术支持,换句话说,就是Spring提供了一套机制,可以让我们更加容易的来进行AOP。 使用注解的方式来定义Pointcut和Advice,Spring并不是首创,首创是AspectJ,而且也不仅仅只有Spring提供了一套机制来支持AOP,还有比如 JBoss 4.0、aspectwerkz等技术都提供了对于AOP的支持。而刚刚说的注解的方式,Spring是依赖了AspectJ的,或者说,Spring是直接把AspectJ中所定义的那些注解直接拿过来用,自己没有再重复定义了,不过也仅仅只是把注解的定义赋值过来了,每个注解具体底层是怎么解析的,还是Spring自己做的,所以我们在用Spring时,如果你要用@Before、@Around等注解,是需要单独引入aspecj相关jar包的,比如: ``` <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.5</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency> ``` AspectJ是在编译时对字节码进行了修改,是直接在类对应的字节码中进行增强的,也就是可以理解为是在编译时就会去解析@Before这些注解,然后得到代理逻辑,加入到被代理的类中的字节码中去的,所以如果想用AspectJ技术来生成代理对象 ,是需要用单独的AspectJ编译器的。我们在项目中很少这么用,我们仅仅只是用了@Before这些注解,而我们在启动Spring的过程中,Spring会去解析这些注解,然后利用动态代理机制生成代理对象的。 ### 5 参考资料 - Spring 官方文档 https://docs.spring.io/spring/docs/5.0.6.RELEASE/spring-framework-reference/core.html#spring-core - Spring【AOP模块】就这么简单 - 《Spring源码深度解析 第2版》 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:于朔
原创文章,需联系作者,授权转载
上一篇:测试用例设计方法六脉神剑——第五剑:化气为型,场景用例破云
下一篇:测试用例设计方法六脉神剑——第四剑:石破天惊,功能图法攻阵
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2243020
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2243020
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号