什么是 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 ,那么就无法再继承目标类了,但是代理类与目标类之间必须要建立一种关系,以保证代理对象能够被引用到,且对使用者而言是透明的,这样就只能通过接口来实现了,也就是让代理类实现与目标类相同的接口,用接口类型的变量去接收代理类的实例。

理解Spring(二):AOP 的概念与实现原理的更多相关文章

  1. spring(二) AOP之AspectJ框架的使用

    前面讲解了spring的特性之一,IOC(控制反转),因为有了IOC,所以我们都不需要自己new对象了,想要什么,spring就给什么.而今天要学习spring的第二个重点,AOP.一篇讲解不完,所以 ...

  2. 学习 Spring (十二) AOP 基本概念及特点

    Spring入门篇 学习笔记 AOP: Aspect Oriented Programming, 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术 主要功能是:日志记录.性能统计.安全控 ...

  3. 理解Spring:AOP的原理及手动实现

    引入 到目前为止,我们已经完成了简易的IOC和DI的功能,虽然相比如Spring来说肯定是非常简陋的,但是毕竟我们是为了理解原理的,也没必要一定要做一个和Spring一样的东西.到了现在并不能让我们松 ...

  4. 深入理解 Spring 事务:入门、使用、原理

    大家好,我是树哥. Spring 事务是复杂一致性业务必备的知识点,掌握好 Spring 事务可以让我们写出更好地代码.这篇文章我们将介绍 Spring 事务的诞生背景,从而让我们可以更清晰地了解 S ...

  5. spring学习(二) ———— AOP之AspectJ框架的使用

    前面讲解了spring的特性之一,IOC(控制反转),因为有了IOC,所以我们都不需要自己new对象了,想要什么,spring就给什么.而今天要学习spring的第二个重点,AOP.一篇讲解不完,所以 ...

  6. Spring的AOP原理

    转自 https://www.tianmaying.com/tutorial/spring-aop AOP是什么? 软件工程有一个基本原则叫做“关注点分离”(Concern Separation),通 ...

  7. 深入理解Spring AOP之二代理对象生成

    深入理解Spring AOP之二代理对象生成 spring代理对象 上一篇博客中讲到了Spring的一些基本概念和初步讲了实现方法,当中提到了动态代理技术,包含JDK动态代理技术和Cglib动态代理 ...

  8. 对于Spring中AOP,DI,IoC概念的理解

    IOC IoC(inversion of Control),控制反转.就好像敏捷开发和SCRUM一样,不是什么技术,而是一种方法论,一种工程化的思想.使用IoC的思想意味着你将设计好的对象交给容器控制 ...

  9. Spring理解IOC,DI,AOP作用,概念,理解。

    IOC控制反转:创建实例对象的控制权从代码转换到Spring容器.实际就是在xml中配置.配置对象 实例化对象时,进行强转为自定义类型.默认返回类型是Object强类型. ApplicationCon ...

随机推荐

  1. java中“”==“” equals hashcode的关系

    ava中的数据类型,可分为两类: 1.基本数据类型,也称原始数据类型.byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比 ...

  2. js语法基础入门(7)

    7.数组 7.1.什么是数组以及相关概念? 什么是数组?是一组数据有序排列的集合.将一组数据按一定顺序组织为一个组合,并对这个组合命名,这样便构成了数组. 什么是数组元素?组成数组的每一个数据称为数组 ...

  3. Python并发编程理论篇

    Python并发编程理论篇 前言 其实关于Python的并发编程是比较难写的一章,因为涉及到的知识很复杂并且理论偏多,所以在这里我尽量的用一些非常简明的语言来尽可能的将它描述清楚,在学习之前首先要记住 ...

  4. Java 添加、提取PDF中的图片

    Spire.Cloud.SDK for Java提供了PdfImagesApi接口可用于添加图片到PDF文档addImage().提取PDF中的图片extractImages(),具体操作步骤和Jav ...

  5. 一文读懂 Redis 分布式部署方案

    为什么要分布式 Redis是一款开源的基于内存的K-V型数据库,因为内存访问速度快,一般被用来做系统的缓存. Redis作为单机部署能够支持业务简单,数据量不大的系统需求,但在实际应用中,一旦系统规模 ...

  6. 服务消费者(Ribbon)

    上一篇文章,简单概述了服务注册与发现,在微服务架构中,业务都会被拆分成一个独立的服务,服务之间的通讯是基于http restful的,Ribbon可以很好地控制HTTP和TCP客户端的行为,Sprin ...

  7. Linux CentOS 7 下dotnet core webpai + nginx 部署

    参考:https://www.jianshu.com/p/b1f573ca50c7 跟着做到,配置nginx访问dotnet core网站时,报错了. 错误如下所示—— 查看nginx的错误日志: c ...

  8. rhel7 rpmbuild 制作二进制程序安装包(.rpm) 简单示例

    下载rpm-build: # yum install rpm-build 如果上述方式无法安装(没配置网络源,虚拟机下是安装媒介源) 可以用下列方式下载后再安装(实践结果可能版本问题引起的缺少太多的* ...

  9. C program Language 'EOF' and 'getchar()'

    #include <stdio.h> void main() { int c; c=getchar(); while(c!=EOF) { putchar(c); c=getchar(); ...

  10. 洛谷 P4910 帕秋莉的手环

    题意 多组数据,给出一个环,要求不能有连续的\(1\),求出满足条件的方案数 \(1\le T \le 10, 1\le n \le 10^{18}\) 思路 20pts 暴力枚举(不会写 60pts ...