0%

Spring MVC系列-(5) AOP

Spring.png

5 AOP

5.1 什么是AOP

AOP(Aspect-Oriented Programming,面向切面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。

OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即切面。所谓“切面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用拦截方法的方式,对该方法进行装饰,以取代原有对象行为的执行;二是采用静态织入的方

5.2 AOP术语

1. 连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点。这个点可以是类的某个方法调用前、调用后、方法抛出异常后等。

2. 通知(Advice)

在特定的连接点,AOP框架执行的动作。

Spring AOP 提供了5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能。
  • 后置通知(After):在目标方法完成之后调用通知,无论该方法是否发生异常。
  • 后置返回通知(After-returning):在目标方法成功执行之后调用通知。
  • 后置异常通知(After-throwing):在目标方法抛出异常后调用通知。
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

3. 切点(Poincut)

具体定位的连接点:上面也说了,每个方法都可以称之为连接点,我们具体定位到某一个方法就成为切点。

切点与连接点:切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。每个类都拥有多个连接点,例如 ArithmethicCalculator类的所有方法实际上都是连接点。

4. 切面(Aspect)

切面由切点和通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。

5. 织入(Weaving)

织入描述的是把切面应用到目标对象来创建新的代理对象的过程。 Spring AOP 的切面是在运行时被织入,原理是使用了动态代理技术。Spring支持两种方式生成代理对象:JDK动态代理和CGLib,默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

6. 引入(Introduction)

添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口。

5.3 AOP使用

首先新建业务逻辑类,该类实现了基本的除法操作:

1
2
3
4
5
6
7
public class Calculator {
//业务逻辑方法
public int div(int i, int j) {
System.out.println("--------");
return i/j;
}
}

现在需要实现:在div()方法运行之前, 记录一下日志, 运行后也记录一下,运行出异常,也打印一下。

因此可以使用AOP来完成日志的功能,新建日志切面类:

在定义切面类的时候,需要注意如下几点:

  • 在类上加上@Aspect声明为切面类。
  • 可以使用PointCut将相同的切点进行统一定义,其他地方直接引用即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Aspect
public class LogAspects {
@Pointcut("execution(public int com.enjoy.cap10.aop.Calculator.*(..))")
public void pointCut(){};

//@before代表在目标方法执行前切入, 并指定在哪个方法前切入
@Before("pointCut()")
public void logStart(JoinPoint joinPoint){
System.out.println(joinPoint.getSignature().getName()+"除法运行....参数列表是:{"+Arrays.asList(joinPoint.getArgs())+"}");
}
@After("pointCut()")
public void logEnd(JoinPoint joinPoint){
System.out.println(joinPoint.getSignature().getName()+"除法结束......");

}
@AfterReturning(value="pointCut()",returning="result")
public void logReturn(Object result){
System.out.println("除法正常返回......运行结果是:{"+result+"}");
}
@AfterThrowing(value="pointCut()",throwing="exception")
public void logException(Exception exception){
System.out.println("运行异常......异常信息是:{"+exception+"}");
}

/*@Around("pointCut()")
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("@Arount:执行目标方法之前...");
Object obj = proceedingJoinPoint.proceed();//相当于开始调div地
System.out.println("@Arount:执行目标方法之后...");
return obj;
}*/
}

有了以上操作, 我们还需要将切面类和被切面的类, 都加入到容器中,注意需要加上@EnableAspectJAutoProxy开启AOP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 日志切面类的方法需要动态感知到div()方法运行,
* 通知方法:
* 前置通知:logStart(); 在我们执行div()除法之前运行(@Before)
* 后置通知:logEnd();在我们目标方法div运行结束之后 ,不管有没有异常(@After)
* 返回通知:logReturn();在我们的目标方法div正常返回值后运行(@AfterReturning)
* 异常通知:logException();在我们的目标方法div出现异常后运行(@AfterThrowing)
* 环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法div,), 执行之前div()相当于前置通知, 执行之后就相当于我们后置通知(@Around)
*/
@Configuration
@EnableAspectJAutoProxy
public class Cap10MainConfig {
@Bean
public Calculator calculator(){
return new Calculator();
}

@Bean
public LogAspects logAspects(){
return new LogAspects();
}
}

使用JoinPoint可以拿到相关的内容, 比如方法名, 参数

Pictu222re1.png

那么方法正常返回, 怎么拿方法的返回值呢?

Pictu4444re1.png

那么如果是异常呢?定义

Pictur55551.png

下面是测试程序,注意需要使用从IOC容器中取出Bean,否则直接new对象进行操作,AOP、无法生效。

Pict111111ure1.png

从下面的运行结果可以看到,AOP生效,日志功能正常:

22.png

小结: AOP看起来很麻烦, 只要3步就可以了:
1, 将业务逻辑组件和切面类都加入到容器中, 告诉spring哪个是切面类(@Aspect)
2, 在切面类上的每个通知方法上标注通知注解, 告诉Spring何时运行(写好切入点表达式,参照官方文档)
3, 开启基于注解的AOP模式 @EableXXXX

5.4 Java动态代理

Spring AOP的实现是基于动态代理,在介绍具体实现细节之前,本节先介绍动态代理的原理。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在两者之间起到中介的作用(可类比房屋中介,房东委托中介销售房屋、签订合同等)。 所谓动态代理,就是实现阶段不用关心代理谁,而是在运行阶段才指定代理哪个一个对象(不确定性)。如果是自己写代理类的方式就是静态代理(确定性)。

很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。

(动态)代理模式主要涉及三个要素:

  • 抽象类接口
  • 被代理类(具体实现抽象接口的类)
  • 动态代理类:实际调用被代理类的方法和属性的类

实现方式: 实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了反射机制。还有其他的实现方式,比如利用字节码操作机制,类似 ASM、CGLIB(基于 ASM)、Javassist 等。 举例,常可采用的JDK提供的动态代理接口InvocationHandler来实现动态代理类。其中invoke方法是该接口定义必须实现的,它完成对真实方法的调用。通过InvocationHandler接口,所有方法都由该Handler来进行处理,即所有被代理的方法都由InvocationHandler接管实际的处理任务。此外,我们常可以在invoke方法实现中增加自定义的逻辑实现,实现对被代理类的业务逻辑无侵入。

反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

JDK动态代理

代理模式的形式如下图所示:

Screen Shot 2020-02-12 at 8.28.31 PM.png

代理模式最大的特点就是代理类和实际业务类实现同一个接口(或继承同一父类),代理对象持有一个实际对象的引用,外部调用时操作的是代理对象,而在代理对象的内部实现中又会去调用实际对象的操作。Java动态代理其实内部也是通过Java反射机制来实现的,即已知的一个对象,然后在运行时动态调用其方法,这样在调用前后作一些相应的处理。

下面举例说明:

1. 静态代理

若代理类在程序运行前就已经存在,那么这种代理方式被成为静态代理 ,这种情况下的代理类通常都是我们在Java代码中定义的。 通常情况下, 静态代理中的代理类和委托类会实现同一接口或是派生自相同的父类。

1
2
3
4
public interface Sell {
void sell();
void ad();
}

Vendor的定义如下:

1
2
3
4
5
6
7
8
9
10
public class Vendor implements Sell{
@Override
public void sell() {
System.out.println("In sell method");
}
@Override
public void ad() {
System.out.println("ad method");
}
}

BusinessAgent的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 静态代理,通过聚合来实现,让代理类有一个委托类的引用即可。
*
*/
public class BusinessAgent implements Sell{
private Sell vendor;
public BusinessAgent(Sell vendor) {
this.vendor = vendor;
}
@Override
public void sell() {
// 一些业务逻辑
System.out.println("before sell");
vendor.sell();
System.out.println("after sell");
}
@Override
public void ad() {
// 一些业务逻辑
System.out.println("before ad");
vendor.ad();
System.out.println("after ad");
}
}

由上面的代码可以看到, 通过静态代理,一方面无需修改Vendor的代码就可以加入一些业务处理逻辑;另一方面,实现了客户端与委托类的解耦。但这种静态代理的局限在于,必须在运行前编写好代理类,如果委托类的方法较多,在添加业务逻辑时的工作量较大,需要对每个方法单独添加。

2. 动态代理

代理类在程序运行时创建的代理方式被成为 动态代理。 也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。

同样还是上面的例子,需要在委托类的每个方法前后加入一些处理逻辑,在动态代理的实现中,首先需要定义一个位于代理类与委托类之间的中介类,这个中介类被要求实现InvocationHandler接口,这个接口的定义如下:

1
2
3
4
5
6
7
/**
* 调用处理程序
* 代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数
*/
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args);
}

中介类必须实现InvocationHandler接口,作为调用处理器”拦截“对代理类方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DynamicProxy implements InvocationHandler {
// obj为委托对象
private Object object;
public DynamicProxy(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object result = method.invoke(object, args);
System.out.println("after");
return result;
}
}

在使用时需要动态生成代理类,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Main {
public static void main(String[] args) {
/* Static proxy */
Vendor vendor = new Vendor();
BusinessAgent businessAgent = new BusinessAgent(vendor);
businessAgent.sell();
businessAgent.ad();
/* Dynamic proxy */
DynamicProxy inter = new DynamicProxy(new Vendor());
//加上这句将会产生一个$Proxy0.class文件,这个文件即为动态生成的代理类文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
// 获取代理实例sell
/**
* public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
* throws IllegalArgumentException
* loader:定义了代理类的ClassLoder;
* interfaces:代理类实现的接口列表;
* h:调用处理器,也就是我们上面定义的实现了InvocationHandler接口的类实例.
*/
Sell sell = (Sell)(Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[] {Sell.class}, inter));
// 通过代理类对象调用代理方法,实际上会转到invoke方法调用
sell.sell();
sell.ad();
}
}

总结: 动态代理的原理就是,首先通过newProxyInstance方法获取代理类实例,而后我们便可以通过这个代理类实例调用代理类的方法,对代理类的方法的调用实际上都会调用中介类(调用处理器)的invoke方法,在invoke方法中我们调用委托类的相应方法,并且可以添加自己的处理逻辑。

CGLIB动态代理

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理。

来看示例,假设我们有一个没有实现任何接口的类HelloConcrete:

1
2
3
4
5
public class HelloConcrete {
public String sayHello(String str) {
return "HelloConcrete: " + str;
}
}

因为没有实现接口该类无法使用JDK代理,通过CGLIB代理实现如下:

  • 实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
  • 在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CGLIB动态代理
// 1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
class MyMethodInterceptor implements MethodInterceptor{
...
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
logger.info("You said: " + Arrays.toString(args));
return proxy.invokeSuper(obj, args);
}
}
// 2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());
HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
// 输出结果如下
// 日志信息: You said: [I love you!]
// HelloConcrete: I love you!

通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete的具体方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很类似,都是方法调用的中转站。

CGLIB是通过继承(如上面例子中的enhancer.setSuperclass(HelloConcrete.class))实现代理,由于final类型不能有子类,所以CGLIB不能代理final类型,遇到这种情况会抛出异常。

5.5 AOP原理深入分析

AOP的原理简单来讲,利用动态代理,在IOC容器初始化时,创建Bean的代理类;在代理方法被调用时,代理类会拦截方法的调用,并在之前或者之后插入切面方法,以此实现AOP的目标。

接下来会从以下几方面深入分析AOP的原理:

  • AnnotationAwareAspectJAutoProxyCreator注册
  • AnnotationAwareAspectJAutoProxyCreator分析
  • AOP流程分析

AnnotationAwareAspectJAutoProxyCreator注册

在之前使用AOP时,为了启用AOP,需要在配置类中,声明@EnableAspectJAutoProxy的注解,这个注解的功能就是注册AnnotationAwareAspectJAutoProxyCreator。下面具体分析这个组件是如何注册的。

Screen Shot 2020-02-13 at 11.47.39 AM.png@w=250

进入@EnableAspectJAutoProxy的源码中,可以看到该类引入了AspectJAutoProxyRegistrar

1
2
3
4
5
6
7
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
//proxyTargetClass属性,默认false,采用JDK动态代理织入增强(实现接口的方式);如果设为true,则采用CGLIB动态代理织入增强
boolean proxyTargetClass() default false;
//通过aop框架暴露该代理对象,aopContext能够访问
boolean exposeProxy() default false;
}

AspectJAutoProxyRegistrar中, 可以看到实现了ImportBeanDefinitionRegistrar接口,这个接口之前也有介绍,能给容器中自定义注册组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

/**
* Register, escalate, and configure the AspectJ auto proxy creator based on the value
* of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
* {@code @Configuration} class.
*/
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}

}

重点关注AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);,这一步将会注册AnnotationAwareAspectJAutoProxyCreator。下面进入源码,

Pict111ure1.png

Pictur222e1.png

程序的逻辑很清晰,

  • 如果(registry.containsBeanDefinition(ATUO_PROXY_CREATOR_BEAN_NAME))也就是容器中bean已经有了 internalAutoProxyCreator, 执行内部内容,返回null。
  • 如果没有,创建AnnotationAwareAspectJAutoProxyCreator信息; 把此bean注册在registry中.
    做完后, 相当于给容器中注册internalAutoProxyCreator组件, 该组件类型为AnnotationAwareAspectJAutoProxyCreator.class。( 注意这里ATUO_PROXY_CREATOR_BEAN_NAME值为internalAutoProxyCreator)

综上分析,@EnableAspectJAutoProxy的功能就是,利用其中的AspectJAutoProxyRegistrar给我们容器中注册一个AnnotationAwareAspectJAutoProxyCreator组件,这是后续创建增强Bean的基础。

AnnotationAwareAspectJAutoProxyCreator分析

AnnotationAwareAspectJAutoProxyCreator的类层次结构如下图所示,

CDA7BFAFE6E8CF7B8DD9E99993FD64D6.png

继承关系为:

  • AnnotationAwareAspectJAutoProxyCreator
  • ->AspectJAwareAdvisorAutoProxyCreator
  • —>AbstractAdvisorAutoProxyCreator
  • —–>AbstractAutoProxyCreator implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware

其中的SmartInstantiationAwareBeanPostProcessor是Bean的后置处理器,同时也实现了BeanFactoryAware可以在容器初始化时,将beanFactory传进来进行相关操作。

由上述分析可知,AnnotationAwareAspectJAutoProxyCreator既具有BeanPostProcessor特点, 也实现了BeanFactoryAware接口,方便操作BeanFactory。

AOP实现流程

上面已经介绍了AOP过程中,核心的AnnotationAwareAspectJAutoProxyCreator组件,接下来对整个AOP的流程进行梳理,主要分为如下4个步骤:

  1. 注册AnnotationAwareAspectJAutoProxyCreator的BeanDefinition

  2. 创建AnnotationAwareAspectJAutoProxyCreator,并加入到BeanFactory

  3. 利用AnnotationAwareAspectJAutoProxyCreator拦截Bean的初始化,创建增强的Bean

  4. 增强Bean的调用过程

IOC容器初始化的入口是如下的refresh()函数,上面1,2,3步骤,分别发生在如下标出的3个函数中,下面分别对这三个函数进行详细介绍。

Screen Shot 2020-02-15 at 10.56.17 AM.png

1. 注册AnnotationAwareAspectJAutoProxyCreator的BeanDefinition

这一步主要是通过invokeBeanFactoryPostProcessors(beanFactory)函数,添加AnnotationAwareAspectJAutoProxyCreator的定义,最终调用的函数如下:

Screen Shot 2020-02-15 at 12.46.45 PM.png

注册的组件类型为AnnotationAwareAspectJAutoProxyCreator.class,组件名称ATUO_PROXY_CREATOR_BEAN_NAME值为internalAutoProxyCreator。

下面是调用栈:

Screen Shot 2020-02-15 at 12.54.46 PM.png

2. 创建AnnotationAwareAspectJAutoProxyCreator,并加入到BeanFactory

这一步入口是registerBeanPostProcessors(beanFactory),进入该函数后,会跳转到如下的核心函数中进行beanPostProcess的实例化。注意到之前提到过,AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcess接口,所以可以将其当成一个正常的后置处理器来进行实例化。

Screen Shot 2020-02-15 at 1.10.12 PM.png

从下面的debug信息可以看到,在这一步中,容器需要实例化4个后置处理器,其中最后一个就是我们关注的AnnotationAwareAspectJAutoProxyCreator

Screen Shot 2020-02-15 at 1.12.50 PM.png

整个初始化后置处理器的流程,可以分为如下几步:

1)先获取ioc容器已经定义了的需要创建对象的所有BeanPostProcessor
3)优先注册实现了PriorityOrdered接口的BeanPostProcessor;
4)再给容器中注册实现了Ordered接口的BeanPostProcessor;
5)注册没实现优先级接口的BeanPostProcessor;

Screen Shot 2020-02-15 at 1.18.01 PM.png

后置处理器AnnotationAwareAspectJAutoProxyCreator实例化完成之后,在接下来的Bean的实例化过程中,它会去尝试拦截Bean的初始化,如果有需要,则会创建代理增强后的Bean。

3. 利用AnnotationAwareAspectJAutoProxyCreator拦截Bean的初始化,创建增强的Bean

在之前的例子中,定义了如下的切面类,实现了相关的advice方法。

Screen Shot 2020-02-15 at 1.22.17 PM.png

这是Calculate类,就是需要增强的类。

Screen Shot 2020-02-15 at 1.31.49 PM.png@w=300

这一步中主要关注这两个Bean的实例化。

这一步的入口是refresh函数中的beanFactory.preInstantiateSingletons(),下一步进入到getBean-->doGetBean函数,

Screen Shot 2020-02-15 at 3.10.53 PM.png

Screen Shot 2020-02-15 at 3.12.38 PM.png

接着进入doGetBean-->createBean函数,
Screen Shot 2020-02-15 at 3.17.15 PM.png

Screen Shot 2020-02-15 at 3.17.41 PM.png

接着进入到createBean函数,会调用函数Object bean = resolveBeforeInstantiation(beanName, mbdToUse);试图直接返回proxy对象。

接下来首先分析这个函数,再分析之后正常的初始化流程。createBean函数是理解整个AOP流程的核心。

Screen Shot 2020-02-15 at 3.26.04 PM.png

进入到函数的实现,可以看到最后会去尝试调用类型为InstantiationAwareBeanPostProcessor的后置处理器,由于AnnotationAwareAspectJAutoProxyCreator实现了该接口,所以这个时候会被调用来试图返回proxy对象,但是通常情况下,增强bean不会在这里生成。

Screen Shot 2020-02-15 at 3.35.20 PM.png

但并不是说这个AnnotationAwareAspectJAutoProxyCreator就没有作用,进入到该函数的实现,可以发现在shouldSkip函数中会去找到所有的Advisor,也就是之前例子中的LogAspects类,并把这些Advisor放到BeanFactory中,方便后续创建增强的Bean。

Screen Shot 2020-02-15 at 4.25.23 PM.png

在获取到所有的Advisor之后,判断当前bean是否在advisedBeans中(保存了所有需要增强bean)
以及判断当前bean是否是基础类型的Advice、Pointcut、Advisor、AopInfrastructureBean,如果是的话就跳过。

回到createBean函数,下面进入到正常的Bean初始化流程,一步步跟进到initializeBean函数中,可以看到在初始化Bean的前后都会调用对应的后置处理器来完成相应的功能,但是AbstractAutoProxyCreator的实现中,在初始化Bean之前,只是直接返回Bean;但是在初始化完Bean之后,会调用对应的后置处理器,也就是在applyBeanPostProcessorsAfterInitialization函数中来创建增强的Bean。

Screen Shot 2020-02-15 at 5.00.58 PM.png

下面对该函数进行仔细分析,

Screen Shot 2020-02-15 at 5.35.30 PM.png

接着分析createProxy函数的实现,下面省略了部分中间调用,在最后的实现中,createAopProxy会根据情况使用jdk代理或者CGLib,从代码中可以看到,当被代理类是接口或者是proxy类时,就会采用jdk动态代理,反之则采用CGLib。

以后容器中获取到的就是这个组件的代理对象,执行目标方法的时候,代理对象就会执行通知方法的流程;

Screen Shot 2020-02-15 at 6.09.40 PM.png

注意一点:在createAopProxy时,会判断config.isProxyTargetClass(),这个值默认为false。但是在两个地方进行设置,一个是EnableAspectJAutoProxy注解中,另一个地方是在createProxy函数中,evaluateProxyInterfaces会去查找目标类的所有interface,如果可用的话,则将其加到proxyFactory中,否则,调用setProxyTargetClass,设置为true。在本例子中,calculate类没有相关接口,所以设置为true。这也是为什么在createAopProxy函数中,会进行判断,而不是直接返回jdk动态代理的类。

Screen Shot 2020-02-15 at 6.26.00 PM.png

4. 增强Bean的调用过程

上面对AOP流程进行了梳理,通过代码分析了如何代理生成增强的Bean。这部分介绍在调用增强Bean的方法时,proxy对象是如何拦截方法调用的。

当被增强的Bean在执行时,会进入到下面的拦截执行流程,

Screen Shot 2020-02-16 at 5.05.32 PM.png

首先,根据ProxyFactory对象获取将要执行的目标方法拦截器链:List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

Picture1.png

进一步跟进到实现,getInterceptorsAndDynamicInterceptionAdvice的流程大致如下,主要是为了获取拦截链:

  1. List interceptorList保存所有拦截器,本例子中有5个拦截器,一个默认的ExposeInvocationInterceptor 和 4个增强器;
  2. 遍历所有的增强器,将其转为List;如果是MethodInterceptor,直接加入到集合中。如果不是,使用AdvisorAdapter将增强器转为MethodInterceptor;转换完成返回MethodInterceptor数组。
  3. 在得到拦截链之后,如果没有拦截器链,直接执行目标方法;如果有拦截器链,把需要执行的目标对象,目标方法,拦截器链等信息传入创建一个 CglibMethodInvocation 对象,并调用 mi.proceed();来获取执行结果。

    注意:拦截器链的触发过程是一个迭代的过程,

    1. 如果没有拦截器执行执行目标方法,或者拦截器的索引和拦截器数组-1大小一样(指定到了最后一个拦截器)执行目标方法;
    2. 链式获取每一个拦截器,拦截器执行invoke方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行;

    从下面的调用栈可以看到,所有的拦截器都会等待下一个拦截器调用完成后,再接着执行。

    Screen Shot 2020-02-16 at 8.36.58 PM.png

    当在执行Before方法时,会先执行完before定义好的方法,然后再去执行正常的方法体:

    Screen Shot 2020-02-16 at 8.39.32 PM.png

    整个拦截的流程可以总结如下图所示:

    Picture111111.png

    下面对整个AOP实现流程进行总结:

    1. @EnableAspectJAutoProxy 开启AOP功能,会给容器中注册一个组件 AnnotationAwareAspectJAutoProxyCreator
    2. AnnotationAwareAspectJAutoProxyCreator是一个后置处理器;
    3. 容器的创建流程:
    • registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator对象
    • finishBeanFactoryInitialization()初始化剩下的单实例bean

      a. 创建业务逻辑组件和切面组件
      b. AnnotationAwareAspectJAutoProxyCreator拦截组件的创建过程
      c. 组件创建完之后,判断组件是否需要增强,如果是,则将切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib);

    1. 执行目标方法:
    • 代理对象执行目标方法
    • CglibAopProxy.intercept();

      a. 得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor)
      b. 利用拦截器的链式机制,依次进入每一个拦截器进行执行;
      c. 效果:
      正常执行:前置通知-》目标方法-》后置通知-》返回通知
      出现异常:前置通知-》目标方法-》后置通知-》异常通知

    5.6 Spring AOP VS AspectJ

    之前介绍的都是标准的Spring AOP实现,通过在运行时对目标类增强,生成代理类。但是利用AspectJ同样可以实现增强,只是后者是编译时增强,而且与Spring框架没有关系,可以独立运行。

    下面先简单介绍AspectJ的使用,然后将其与Spring AOP进行对比。

    AspectJ的使用

    1. 下载AspectJ并安装:http://www.eclipse.org/aspectj/downloads.php
    2. 实现HelloWord
    1
    2
    3
    4
    5
    6
    7
    业务组件  SayHelloService
    package com.ywsc.fenfenzhong.aspectj.learn;
    public class SayHelloService {
    public void say(){
    System.out.print("Hello AspectJ");
    }
    }

    需要来了,在需要在调用say()方法之后,需要记录日志。那就是通过AspectJ的后置增强吧。

    1
    2
    3
    4
    5
    6
    7
    LogAspect 日志记录组件,实现对SayHelloService 后置增强
    public aspect LogAspect{
    pointcut logPointcut():execution(void SayHelloService.say());
    after():logPointcut(){
    System.out.println("记录日志 ...");
    }
    }
    1. 编译SayHelloService
    1
    2
    3
    4
    执行命令   ajc -d . SayHelloService.java LogAspect.java
    生成 SayHelloService.class
    执行命令 java SayHelloService
    输出 Hello AspectJ 记录日志

    ajc.exe 可以理解为 javac.exe 命令,都用于编译 Java 程序,区别是 ajc.exe 命令可识别 AspectJ 的语法;我们可以将 ajc.exe 当成一个增强版的 javac.exe 命令.执行ajc命令后的 SayHelloService.class 文件不是由原来的 SayHelloService.java 文件编译得到的,该 SayHelloService.class 里新增了打印日志的内容——这表明 AspectJ 在编译时“自动”编译得到了一个新类,这个新类增强了原有的 SayHelloService.java 类的功能,因此 AspectJ 通常被称为编译时增强的 AOP 框架。

    Spring AOP和AspectJ对比

    1. 从目标角度讲:

    • Spring AOP侧重于在IOC容器中,提供了一个简单的AOP实现,它并不是一个完整的AOP解决方案,只适用于被IOC容器管理的Bean。
    • AspectJ是原始的AOP方案,目标是提供一套完整的AOP解决方案。相比Spring AOP,鲁棒性更强,可以适用于所有的对象,但是也更加复杂。

    2. 织入(Weaving)

    AspectJ利用了下面3种不同的织入方法:

    1. Compile-time weaving: The AspectJ compiler takes as input both the source code of our aspect and our application and produces a woven class files as output
    2. Post-compile weaving: This is also known as binary weaving. It is used to weave existing class files and JAR files with our aspects
    3. Load-time weaving: This is exactly like the former binary weaving, with a difference that weaving is postponed until a class loader loads the class files to the JVM

    相比于AspectJ,Spring AOP利用了运行时织入(runtime weaving)。

    通过动态织入,切面方法被动态的织入到程序的运行过程中,通常有JDK动态代理或者CGLIB代理。

    • Spring AOP倾向于使用JDK动态代理,只要目标对象实现了至少一个接口,Spring将会采用JDK动态代理来创建增强的Bean。

    • 如果目标方法没有实现接口,就会采用CGLIB来实现。

    Screen Shot 2020-02-18 at 3.28.16 PM.png

    3. 连接点(Join point)

    从设计的角度讲,Spring AOP通过代理模式来实现, 例如CGLIB创建目标类的子类(如下图的实例所示),再调父类的目标方法实现AOP。但是,一旦目标父类使用了关键字final,子类无法继承,切入就不能实现。因此不能代理final类型,同样的,也不能代理static方法,因为他们不能被重写。所以通常情况下,Spring AOP只支持方法作为连接点。

    Screen Shot 2020-02-18 at 4.12.05 PM.png

    AspectJ没有这种限制,在编译期直接将增强方法织入到代码中,也不需要像Spring AOP那样继承目标方法,因此可以支持更多的连接点。

    具体比较如下:

    Screen Shot 2020-02-18 at 4.19.53 PM.png

    总结:

    Spring AOP是基于代理的实现方式,在程序运行时创建代理,并通过拦截链来执行切面方法。AspectJ在编译期将切面方法织入到目标类,在运行期没有其他性能损耗,因此性能上相比Spring AOP会快很多。

    下表是一个整体的对比:

    Screen Shot 2020-02-18 at 4.24.51 PM.png


    参考:


    本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处

    搜索『后端精进之路』关注公众号,立刻获取最新文章和价值2000元的BATJ精品面试课程

    后端精进之路.png