Spring AOP从零单排-织入时期源码分析

45次阅读

共计 5741 个字符,预计需要花费 15 分钟才能阅读完成。

问题:Spring AOP 代理中的运行时期,是在初始化时期织入还是获取对象时期织入?
织入就是代理的过程,指目标对象进行封装转换成代理,实现了代理,就可以运用各种代理的场景模式。
何为 AOP
简单点来定义就是切面,是一种编程范式。与 OOP 对比,它是面向切面,为何需要切面,在开发中,我们的系统从上到下定义的模块中的过程中会产生一些横切性的问题,这些横切性的问题和我们的主业务逻辑关系不大,假如不进行 AOP,会散落在代码的各个地方,造成难以维护。AOP 的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性、侵入性低、开发效率高。
AOP 使用场景

日志记录;记录调用方法的入参和结果返参。
用户的权限验证;验证用户的权限放到 AOP 中,与主业务进行解耦。
性能监控;监控程序运行方法的耗时,找出项目的瓶颈。
事务管理;控制 Spring 事务,Mysql 事务等。

AOP 概念点
AOP 和 Spring AOP 的关系
在这里问题中,也有一个类似的一对 IOC 和 DI(dependency injection)的关系,AOP 可以理解是一种编程目标,Spring AOP 就是这个实现这个目标的一种手段。同理 IOC 也是一种编程目标,DI 就是它的一个手段。
SpringAOP 和 AspectJ 是什么关系

在 Spring 官网可以看到,AOP 的实现提供了两种支持分别为 @AspectJ、Schema-based AOP。其实在 Spring2.5 版本时,Spring 自己实现了一套 AOP 开发的规范和语言,但是这一套规范比较复杂,可读性差。之后,Spring 借用了 AspectJ 编程风格,才有了 @AspectJ 的方式支持,那么何为编程风格。

Annotation 注解方式;对应 @AspectJ
JavaConfig;对应 Schema-based AOP

SpringAOP 和 AspectJ 的详细对比,在之后的章节会在进行更加详细的说明,将会在他们的背景、织入方法、性能做介绍。
Spring AOP 的应用
阅读官网,是我们学习一个新知识的最好途径,这个就是 Spring AOP 的核心概念点,跟进它们的重要性,我做了重新的排序,以便好理解,这些会为我们后续的源码分析起到作用。

Aspect:切面;使用 @Aspect 注解的 Java 类来实现,集合了所有的切点,做为切点的一个载体,做一个比喻就像是我们的一个数据库。Tips:这个要实现的话,一定要交给 Spirng IOC 去管理,也就是需要加入 @Component。
Pointcut:切点;表示为所有 Join point 的集合,就像是数据库中一个表。
Join point:连接点;俗称为目标对象,具体来说就是 servlet 中的 method,就像是数据库表中的记录。
Advice:通知;这个就是 before、after、After throwing、After (finally)。
Weaving:把代理逻辑加入到目标对象上的过程叫做织入。
target:目标对象、原始对象。
aop Proxy:代理对象 包含了原始对象的代码和增加后的代码的那个对象。
Tips 这个应用点,有很多的知识点可以让我们去挖掘,比如 Pointcut 中 execution、within 的区别,我相信你去针对性搜索或者官网都未必能有好的解释,稍后会再专门挑一个文章做重点的使用介绍;
SpringAOP 源码分析
为了回答我们的一开始的问题,前面的几个章节我们做了一些简单的概念介绍做为铺垫,那么接下来我们回归正题,正面去切入问题。以码说话,我们以最简洁的思路把 AOP 实现,我们先上代码。
项目结构介绍
项目目录结构,比较简单,5 个主要的文件;pom.xml 核心代码;spring-content 是核心 jar,已经包含了 spring 所有的基础 jar,aspectjweaver 是为了实现 AOP。AppConfig.java;定义一个 Annotation,做为我们 Spirng IOC 容器的启动类。
package com.will.config;

@Configuration
@ComponentScan(“com.will”)
@EnableAspectJAutoProxy(proxyTargetClass = false)
public class AppConfig {

}
WilAspect.java;按照官网首推的方式(@AspectJ support),实现 AOP 代理。
package com.will.config;

/**
* 定义一个切面的载体
*/
@Aspect
@Component
public class WilAspect {
/**
* 定义一个切点
*/
@Pointcut(“execution(* com.will.dao.*.*(..))”)
public void pointCutExecution(){
}

/**
* 定义一个 Advice 为 Before,并指定对应的切点
* @param joinPoint
*/
@Before(“pointCutExecution()”)
public void before(JoinPoint joinPoint){
System.out.println(“proxy-before”);
}
}
Dao.java
package com.will.dao;
public interface Dao {
public void query();
}
UserDao.java
package com.will.dao;
import org.springframework.stereotype.Component;
@Component
public class UserDao implements Dao {
public void query() {
System.out.println(“query user”);
}
}
Test.java
package com.will.test;
import com.will.config.AppConfig;
import com.will.dao.Dao;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
/**
* new 一个注册配置类,启动 IOC 容器,初始化时期;
*/
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

/**
* 获取 Dao 对象,获取对象时期,并进行 query 打印
*/
Dao dao = annotationConfigApplicationContext.getBean(Dao.class);
dao.query();
annotationConfigApplicationContext.start();
}
}
好了,这样我们整体的 AOP 代理就已经完成。
问题分析测试
究竟是哪个时期进行对象织入的,比如 Test 类中,究竟是第一行还是第二行进行织入的,我们只能通过源码进行分析,假如是你,你会进行如何的分析源码解读。
Spring 的代码非常优秀,同时也非常复杂,那是一个大项目,里面进行了很多的代码封装,那么的代码你三天三夜也读不完,甚至于你都不清楚哪一行的该留意的,哪一行是起到关键性作用的,这里教几个小技巧。

看方法返回类型;假如是 void 返回类型的,看都不看跳过。返回结果是对象,比如 T 果断进行去进行跟踪。
假设法;就当前场景,我们大胆假设是第二行进行的织入。
借助好的 IDE;IDEA 可以帮我们做很多的事情,它的 debug 模式中的条件断点、调用链(堆栈)会帮助到我们。

假设法源码分析

debug 模式 StepInfo(F5)后,进入 AbstractApplicationContext.getBean 方法,这个是 Spring 应用上下文中最重要的一个类,这个抽象类中提供了几乎 ApplicationContext 的所有操作。这里第一个语句返回 void,我们可以直接忽略,看下面的关键性代码。

继续 debug 后,会进入到 DefaultListableBeanFactory 类中,看如下代码
return new NamedBeanHolder<>(beanName, getBean
(beanName, requiredType, args));
在该语句中,这个可以理解为 DefaultListableBeanFactory 容器,帮我们获取相应的 Bean。

进入到 AbstractBeanFactory 类的 doGetBean 方法之后,我们运行完。
Object sharedInstance = getSingleton(beanName);
语句之后,看到 sharedInstance 对象打印出 &Proxyxxx,说明在 getSingleton 方法的时候就已经获取到了对象,所以需要跟踪进入到 getSingleton 方法中,继续探究。

不方便不方便我们进行问题追踪到这个步骤之后,我需要引入 IDEA 的条件断点,不方便我们进行问题追踪因为 Spring 会初始化很多的 Bean,我们再 ObjectsharedInstance=getSingleton(beanName); 加入条件断点语句。

继续 debug 进入到 DefaultSingletonBeanRegistry 的 getSingleton 方法。我们观察下执行完 ObjectsingletonObject=this.singletonObjects.get(beanName); 之后的 singletonObject 已经变成为 &ProxyUserDao,这个时候 Spring 最关键的一行代码出现了,请注意这个 this.singletonObjects。
this.singletonObjects 就是相当 IOC 容器,反之 IOC 容器就是一个线程安全的线程安全的 HashMap,里面存放着我们需要 Bean。

我们来看下 singletonObjects 存放着的数据,里面就有我们的 UserDao 类。
这就说明,我们的初始化的时期进行织入的,上图也有整个 Debug 模式的调用链。
源码深层次探索
通过上一个环节已经得知是在第一行进行初始化的,但是它在初始化的时候是什么时候完成织入的,抱着求知的心态我们继续求证。
还是那个问题,那么多的代码,我的切入点在哪里?
既然 singletonObjects 是容器,存放我们的 Bean,那么找到关键性代码在哪里进行存放(put 方法)就可以了。于是我们通过搜索定位到了。

我们通过 debug 模式的条件断点和 debug 调用链模式,就可以进行探索。

这个时候借助上图中的调用链,我们把思路放到放到 IDEA 帮我定位到的两个方法代码上。
DefaultSingletonBeanRegistry.getSingleton
我们一步步断点,得知,当运行完 singletonObject=singletonFactory.getObject(); 之后,singletonObject 已经获得了代理。

至此我们知道,代理对象的获取关键在于 singletonFactory 对象,于是又定位到了 AbstractBeanFactorydoGetBean 方法,发现 singletonFactory 参数是由 createBean 方法创造的。这个就是 Spring 中 IOC 容器最核心的地方了,这个代码的模式也值得我们去学习。
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
这个第二个参数是用到了 jdk8 中的 lambda,这一段的含义是就是为了传参,重点看下 createBean(beanName,mbd,args); 代码。随着断点,我们进入到这个类方法里面。
AbstractAutowireCapableBeanFactory.createBean 中的;
ObjectbeanInstance=doCreateBean(beanName,mbdToUse,args) 方法;
doCreateBean 方法中,做了简化。
Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}


return exposedObject;

当运行完 exposedObject=initializeBean(beanName,exposedObject,mbd); 之后,我们看到 exposedObject 已经是一个代理对象,并执行返回。这一行代码就是取判断对象要不要执行代理,要的话就去初始化代理对象,不需要直接返回。后面的 initializeBean 方法是涉及代理对象生成的逻辑(JDK、Cglib),后续会有一个专门的章节进行详细介绍。
总结
通过源码分析,我们得知,Spring AOP 的代理对象的织入时期是在运行 Spring 初始化的时候就已经完成的织入,并且也分析了 Spring 是如何完成的织入。

正文完
 0