理解Spring(二):AOP 的概念与实现原理

佐手、 提交于 2020-08-14 22:38:48

什么是 AOP

AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它是对 OOP(Object Oriented Programming,面向对象编程)的一个补充。

OOP 允许我们通过类来定义对象的属性和行为,由于对象的行为是通过类中的方法来体现的,所以要想修改一个对象的行为,就必须修改类中相应的方法。试想这么一个场景,我们需要对某些对象的某些行为进行耗时统计,OOP 的做法只能是挨个去修改它们所属的类,在相应的方法上加入耗时统计的逻辑,如果只是针对少量几个行为的修改倒也无妨,但如果要统计的是成百上千个行为呢,挨个去修改这成百上千个方法就显得很拙劣,而且还会导致大量的代码重复,如果要统计的是第三方类库中的行为,那么 OOP 就显得更加力不从心了。

在实际开发中,除耗时统计之外,类似的还有日志记录、事务控制、权限验证等等,它们往往穿插在各个控制流中,被各个功能模块所调用,但它们却是与核心业务逻辑无关的。像这种穿插在各个功能模块中的且与核心业务无关的代码被称为横切(cross cutting)。

在传统 OOP 中,横切除了会导致大量的代码重复之外,还会使核心业务代码看起来臃肿,由于那些与核心业务无关的横切代码同核心业务代码紧密耦合在一起,甚至会出现核心业务代码被淹没在大量横切代码之中的情况,而且这些横切代码分散在系统的各个地方,非常不利于维护和管理。

AOP 提供了对横切的处理思路,它的主要思想是,将横切逻辑分离出来,封装成切面,通过某种机制将其织入到指定的各个功能模块中去,而不再是同核心业务代码交织在一起。AOP 使得我们可以暂时忽略掉系统中的横切逻辑,专注于核心业务逻辑的开发,实现横切逻辑与核心业务逻辑的解耦,允许我们对横切代码进行集中管理,消除代码重复。

AOP 的基本术语

切面(Aspect):是对横切逻辑的抽象,一个切面由通知和切点两部分组成。在实际应用中,切面被定义成一个类。

通知(Advice):是横切逻辑的具体实现。在实际应用中,通知被定义成切面类中的一个方法,方法体内的代码就是横切代码。通知的分类:以目标方法为参照点,根据切入方位的不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。

切点(Pointcut):用于说明将通知织入到哪个方法上,它是由切点表达式来定义的。

目标对象(Target):是指那些即将织入切面的对象。这些对象中已经只剩下干干净净的核心业务逻辑的代码了,所有的横切逻辑的代码都等待 AOP 框架的织入。

代理对象(Proxy):是指将切面应用到目标对象之后由 AOP 框架所创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上横切逻辑功能,代理对象对使用者而言是透明的。

织入(Weaving):是指将切面应用到目标对象从而创建一个新的代理对象的过程。

Spring AOP 的简单应用

Spring 的 AOP 模块简称 Spring AOP,该模块对 AOP 提供了支持。

使用 Spring 进行面向切面编程的基本步骤如下:

一、定义一个切面。使用 @Aspect 注解声明切面,并使用 @Component 注解将该 Bean 注册到 Spring 容器。

@Aspect
@Component
public class WebLogAspect {}

二、在切面中定义一个切点。通过 @Pointcut 注解指定切点表达式。

@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void controllerLog(){}

三、在切面中定义一个通知。例如使用 @Before 注解定义一个前置通知,并为其指定一个切点。然后在通知的方法体内编写横切代码。

@Before("controllerLog()")
public void beforeAdvice(JoinPoint joinPoint){
    logger.info("前置通知...");
}

以上是基于注解的切面定义方式,我们会发现这些注解是由 AspectJ 提供的。AspectJ 是一个专门的 AOP 框架,它提供了比 Spring AOP 更为强大的功能。那 Spring AOP 与 AspectJ 有什么关系呢?其实没有什么关系,只不过是 Spring AOP 把 AspectJ 的注解直接拿来用了罢了。所以上面这种基于注解的方式也被称为 AspectJ 风格。

采用 AspectJ 风格来定义切面,需要开启 AspectJ 自动代理选项,如使用注解 @EnableAspectJAutoProxy 或配置 XML 标签 <aop:aspectj-autoproxy>

根据我们上面配置的切点表达式,Spring 会给 com.example.demo.controller 包下的所有的类都生成相应的代理类,并将横切代码 logger.info("前置通知..."); 织入到代理类的每一个 public 方法中,由于我们定义的是前置通知,所以它会被织入到方法内其他代码的前面。然后 Spring 会生成代理类的实例作为代理对象,并将其加入到 Spring 容器的单例池中。当我们拿到代理对象之后,调用它们的 public 方法首先执行的是 logger.info("前置通知..."); 这行横切代码,然后才是我们在目标类中写的代码。当然,如果我们定义是后置通知(AfterReturning),那么与前置通知刚好相反,这行横切代码会被织入到方法内其他代码的后面。

通过 AOP,我们将横切代码与核心业务代码进行了分离,然后又通过某种机制将其联系了起来,在 Spring AOP 中,这个机制就是动态代理。

Spring AOP 与动态代理

Spring AOP 是基于动态代理技术来实现的,因此需要了解什么是动态代理。

动态代理是代理模式的一种实现方式,我们先来看一下什么是代理模式。

代理模式是 GoF 的 23 种设计模式之一,代理模式允许我们在不修改目标类的前提下对目标对象的行为做一些补充。它是通过在客户端对象与目标对象之间引入一个代理对象来实现的,代理对象相当于一个中介,负责代理目标对象的业务,并且它在代理业务的同时还可以添油加醋,有了代理对象之后,客户端对象访问代理对象,既能实现目标业务,而且还能让代理对象在目标业务的基础上增加一些额外的服务,如“端茶送水”等,当然代理对象可能需要“收点小费”了。如果没有代理对象,客户端对象就享受不到“端茶送水”的额外服务,除非修改目标对象的行为。

代理模式分为静态代理与动态代理。

静态代理需要我们手动编写代理类,代理类需要实现与目标类相同的接口,并通过构造方法传入目标对象,然后调用目标对象的相应方法,将具体业务委托给目标对象来执行,并在委托时可以做一些处理。由于静态代理需要我们手动编写代理类,大大增加了我们的工作量,并且还可能导致大量的代码重复,因此,自 JDK1.3 引入了动态代理技术之后,我们更加偏向使用动态代理。

动态代理基于反射技术,允许程序在运行期间动态生成代理类与代理对象,这样就不需要我们编写代理类了。

动态代理有两种实现方式,一种是基于 JDK 的动态代理,另一种是基于 CGLib 的动态代理,也就是说,一个是使用 JDK 提供的动态代理技术来实现,一个是使用第三方库 CGLib 提供的动态代理技术来实现。

基于 JDK 的动态代理是面向接口的代理,它要求目标类必须实现至少一个接口,其动态生成的代理类也会实现同样的接口。基于 CGLib 的动态代理是面向类的代理,它所生成的代理类是目标类的一个子类,因此要求目标类和目标方法不能声明为 final。

Spring AOP 通过 JDK 或 CGLib 的动态代理技术,将横切代码动态织入到目标类的方法前后,并生成一个代理对象,用这个织入了横切逻辑后的代理对象充当目标对象供我们使用。

Spring AOP 的实现原理(源码分析)

我们知道,当一个 Bean 被实例化出来之后,Spring 会对其执行一些初始化操作,如:回调 Aware 接口方法、调用 init 方法、应用后置处理器等。其中应用后置处理器的代码如图所示。

该方法会遍历所有已注册的 Bean 后置处理器,依次调用它们的 postProcessAfterInitialization() 方法对 Bean 实例执行相应的处理。我们在这个地方打个断点,看看它都注册了哪些后置处理器。

在这些 Bean 后置处理器当中,有一个 AnnotationAwareAspectJAutoProxyCreator 对象,顾名思义,它是一个基于注解的“代理创建器”。我们猜测,代理类的创建就是在这个后置处理器中进行的。它的postProcessAfterInitialization() 方法如图所示。

这个名为“代理创建器”的后置处理器主要做的事情就是调用 wrapIfNecessary() 方法。该方法的具体实现如图所示。

我们在 wrapIfNecessary() 方法中发现了创建代理的逻辑,看来一切要真相大白了。该方法会根据需要,为给定的 Bean 实例(即目标对象)创建代理并返回代理对象,或者将该 Bean 实例原封不动直接返回。

至此可以得知,代理对象的创建是在 Bean 的初始化阶段完成的,是通过名为“代理创建器”的这么一个后置处理器来实现的。

我们进入到 createProxy() 方法中,看一下创建代理的具体实现。

该方法主要是创建并配置 ProxyFactory 对象(如配置 Advisor 、设置目标对象等),然后调用它的 getProxy() 方法得到一个代理对象。

这里顺便介绍一下 Advisor。在 Spring 内部,每个切面都会被封装成一个 Advisor 对象,一个 Advisor 对象内部包含一个通知对象( Advice )和一个切点对象(Pointcut),因此可以说,Advisor 对象就是真正的切面对象。

上面的 getProxy() 方法先是会调用 createAopProxy() 方法创建一个 AopProxy 对象,然后将创建代理的任务委托给 AopProxy 对象来执行。AopProxy 本身是一个接口,它主要有两个实现类:一个是 JdkDynamicAopProxy,一个是 ObjenesisCglibAopProxy。顾名思义,前者使用 JDK 动态代理技术,后者使用 CGLib 动态代理技术。 createAopProxy() 方法会根据条件选择使用哪种动态代理技术,具体实现如图所示。

大体来说,在默认情况下,如果目标类没有实现任何接口,那么就使用 CGLib 动态代理,否则使用 JDK 动态代理。由于 CGLib 的性能相对较好,我们可以通过开启 proxyTargetClass 选项强制 Spring 始终使用 CGLib 动态代理。(注:Spring Boot 默认开启了 proxyTargetClass

AopProxy 的功能很简单,就是使用动态代理技术生成代理类及其实例,JdkDynamicAopProxy 通过 JDK 提供的 ProxyInvocationHandler 来实现,ObjenesisCglibAopProxy 通过 CGLib 提供的 Enhancer 来实现。(注:Spring AOP 中集成并定制了 CGLib,因此无需引入外部的 CGLib 依赖)。

总结:Spring AOP 的核心是“代理创建器”,也就是 AbstractAutoProxyCreator 的子类,本质上它是一个 Bean 的后置处理器,Spring 会根据我们的配置,将相应的“代理创建器”注册到 Spring 容器,例如当我们项目中配置了 @EnableAspectJAutoProxy 注解时,Spring 就会将 AnnotationAwareAspectJAutoProxyCreator 注册到 Spring 容器。由于它是一个 Bean 的后置处理器,所以它会在 Bean 的初始化阶段得到调用,它会首先判断当前这个 Bean 是否需要被代理,如果不需要,直接将原 Bean 实例返回,如果需要,就使用动态代理技术为当前 Bean 创建一个代理类,并将横切代码织入到代理类中,然后生成一个代理类的实例并将其返回,也就是用代理对象充当 Bean 实例。如果该 Bean 是单例的,那么这个代理对象就会被加入到 Spring 容器的单例池中,之后当我们 getBean 时,就可以直接从单例池中拿到这个代理对象。

扩展:为什么 JDK 动态代理要求目标类必须实现接口

通过查看 java.lang.reflect.Proxysun.misc.ProxyGenerator 的源码,不难发现, 它所生成的代理类都继承自 java.lang.reflect.Proxy。关键代码如图所示。



由于 Java 不支持多继承,所以既然代理类继承了 Proxy ,那么就无法再继承目标类了,但是代理类与目标类之间必须要建立一种关系,以保证代理对象能够被引用到,且对使用者而言是透明的,这样就只能通过接口来实现了,也就是让代理类实现与目标类相同的接口,用接口类型的变量去接收代理类的实例。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!