引言
本文是Spring
原理分析的第三篇博文,主要阐述Spring AOP
相关概念,同时从源码层面分析AOP
实现原理。对于AOP
原理的理解有利于加深对Spring
框架的深入理解。同时我也希望可以探究Spring
框架在处理AOP
的解决思路,学习框架的时候,有时候需要站在设计者的角度上去考虑,如果自己是设计者遇到同样需要解决的问题自己会怎么去处理,然后再对照实际框架中的处理方式,这样可以发现自己考虑不足之处。
本文侧重于找到Spring
框架处理AOP
的起点,至于涉及到的动态代理相关的问题将在下一篇文中着重介绍。
- 提问题
- AOP概念
- AOP代码示例
- 源码分析
- 总结
一、提问题
到底什么是AOP
?Spring
框架到底在什么阶段进行数据织入的?AOP
在实际项目开发中到底有什么作用以及怎样将AOP
的编程思想运用到我们的实际项目开发中?
二、AOP概念
AOP(Aspect Orient Programming)
,我们通常称为面向切面编程。所谓切面是相对于面向对象来说的。面向对象是将实物抽象为对象,这是个纵向的概念。而面向切面是一个横向的概念,它更加关注那些散落在代码中公用的不涉及具体业务逻辑的通用处理方式,例如日志、权限验证以及统一异常处理等等。它是对于面向对象编程思想的一种结构化补充。核心思想就是将与业务逻辑无关的进行统一的框架织入,不对原有代码以及业务逻辑造成侵入。
Spring AOP在运行时,能够动态地将代码切入到指定的类的指定方法、指定位置上的编程思想就是面向切面编程,这种切入的特点是不影响原来的业务逻辑。但是像AspectJ可以在编译阶段以及类加载阶段进行织入。
一些概念的说明:
- 切面
所谓切面,按照自己的理解可以把它看作为一把刀,将他横切于其他物品,通过@Aspect
来将类定义一个切面,它就是切点与通知的结合,如下图所示。
- 切点
本质上来说,就是需要定义一个切入点表达式,使得可以在增强处理中使用到。通过切点定位和筛选特定的连接点。它关注通知需要织入的一个或者多个连接点,切入点包括两部分:
(1)切入点表达式:指定切入点与哪些方法进行匹配;
(2)切入点名称:方法签名 - 连接点
连接点是一个相对虚拟的概念,可以将它理解为切点的集合,也就是Spring
允许我们进行通知操作的地方,比如方法、异常抛出的地方,如果使用aspectj
则也支持在构造器中或者属性中允许通知。 - 通知
通俗地说,通知就是我们需要实现的功能,可以分为前置、后置、异常、最终以及环绕通知这五类。例如日志,权限等业务逻辑。
- 前置通知:在目标方法或者连接点被调用前执行通知操作;
- 后置通知:在某些连接点执行完成之后进行通知操作;
- 异常通知:在方法抛出异常退出当前时进行通知操作;
- 最终通知:当切入点退出时无论是方法正常执行结束还是异常抛出后退出执行的通知操作;
- 环绕通知:包围一个切点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
三、AOP代码示例
1.定义一个普通业务逻辑方法
@Component("userDao") public class UserDao { /** * @Description: 查询 * @Return * @Author taomeng * @Version V1.0.0 * @CreateDate: 2018/9/23 11:31 */ public void query() { System.out.println("query user data!!!"); } }
2.定义一个切面类,同时在这个切面类中将切点等进行定义。
/** * @Auther: taomeng * @Date: 2018/9/17 23:32 * @Description: 定义切面类 */ @Configuration @Aspect @ComponentScan(basePackages = {"com.tm.springrun.module"}) @ImportResource(locations = {"classpath:spring-context.xml"}) public class AopConfig { /** * @Description:定义切点 * @Return * @Author taomeng * @Version V1.0.0 * @CreateDate: 2018/9/23 11:32 */ @Pointcut("execution(* com.tm.springrun.module.dao.UserDao.query())") public void declareJointPointExpression(){} @Before("declareJointPointExpression()") public void beforeMethod(JoinPoint joinPoint) { String method = joinPoint.getSignature().getName(); System.out.println("The method of before:" + method); } @After("declareJointPointExpression()") public void afterMethod(JoinPoint joinPoint) { String method = joinPoint.getSignature().getName(); System.out.println("The method of after:" + method ); } }
3.在Spring Context
中获取Bean
的实例,同时执行Bean
实例的方法。
/** * @Auther: taomeng * @Date: 2018/9/17 23:37 * @Description: 测试AOP */ public class Test { public static void main(String[] args) { //1.加载spring环境 AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AopConfig.class); //2.获取spring context中的bean的实例 UserDao userDao = (UserDao)annotationConfigApplicationContext.getBean("userDao"); //3.执行bean中的方法 userDao.query(); } }
同时需要在配置文件中开启AOP,如下所示:
<aop:aspectj-autoproxy/>
四、源码分析
对Spring AOP
进行源码分析,就是要找到Spring
框架中处理AOP
的源头以及在什么阶段进行 数据织入的,带着这样的问题去做源码分析才可以做到有的放矢,要不然那么多源码看下去就像无头苍蝇一样不知道如何下手。
如同第二节示例代码所示, 在方法执行时,通知已经被执行了。那么Spring AOP
应该是在前两个步骤中起作用的,要么是在Bean
定义时候,要么就是在获取Bean
的时候进行的。如下图所示,我们希望通过debug的方式找到AOP
在什么阶段开始起作用。
以下为寻找AOP
起作用起点的过程,首先我们猜测Spring
在处理AOP
的时候是在获取bean
的时候进行数据的织入的,所以在获取bean的时候进行断点跟踪。
进入断点,进入AbstractBeanFactory
类中的getBean
方法。
进入getBean
方法,在这个方法中
此时发现对象已经被代理了,所以需要到getSingleton(beanName)
这个方法中去查看。
在这个我们可以发现,Spring
框架存放bean名称以及对应的对象的数据结构实际上是一个ConcurrentHashMap
。如下所示:
/** Cache of singleton objects: bean name --> bean instance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
我们可以看一下在这个map中的存放了Spring中加载的对象,这也是Spring IOC的核心。既然在这个map中获取到了bean,那肯定是在某个地方将对象进行put操作。全文搜索后发现,在DefaultSingletonBeanRegistry这个类中的addSingleton方法中进行对象put操作。
从上图可知,在对象put操作时,userDao对象已经被代理了。这就说明在获取到bean的时候,对应的对象已经是被代理过的。所以实际数据织入并不是在获取bean的时候进行的,而是在bean加载到Spring环境中的时候就已经完成了。
那接下来我们需要找出调用addSingleton方法的地方,
从下图可以看出来,此时的mdb
还是原生的,那说明此时还没有进行数据织入,是在下面的方法中进行的。
在doCreateBean
方法中,我们跟踪到initializeBean
的方法,此时发现bean
已经被数据织入了,继续进入方法。
继续查看initializeBean
方法内部,如下图所示:
再继续查看
再进入方法进行查看
至此,我们终于跟踪到实现代理的最初部分,其中根据条件判断是jdk代理还是cglib代理。
五、总结
Spring
源码很复杂,如果不是带着目标去看,很难抓住重点。在寻找数据织入的部分,需要一步一步进行,每找到一个部分就需要在对应的部分打上断点,同时去掉之前的断点,不断迭代深入,直到获取到最终的起点,关于代理的这部分内容会放到下一篇文章中进行详细阐述。
根据第三章中的源码分析,我们明确了Spring
框架进行数据织入的起点,如下图所示,是在Spring
框架加载Bean
的时候进行的。
Spring AOP
相关知识树如下图所示: