JavaEE进阶 – Spring AOP – 细节狂魔

  • Post author:
  • Post category:java




1.什么是 Spring AOP?


在介绍 Spring AOP 之前,⾸先要了解⼀下什么是 AOP?


AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的集中处理。

⽐如⽤户登录权限的效验,没学 AOP 之前,我们所有需要判断⽤户登录的⻚⾯(中的⽅法),都要各⾃实现或调⽤⽤户验证的⽅法,然⽽有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了。

简单来说: AOP 可以让我们在写代码的时候,只关于业务本身!

比如:我们在添加某一项业务功能的时候,我们只需要完成 核心功能!至于权限的校验,不在需要我们去关注了!!

因为 我们的程序会做一个统一的处理。


举个例子:

我们在实现文章的添加/修改/删除,我们知道这些操作,是需要用户权限。

你不能说:我们在看别人的文章,感觉它写的不行。就把别人的文章给删了,对吧!

必须要是该文章的作者,才能删除。

如何得知你是不是作者?就是对我们的账号进行权限的校验(用户名和密码,还有写补充身份信息等等)。


但是!代码实现了 AOP 思想之后,权限校验,就不需要我们再去关注了。

因为我们的程序会做一个统一的处理。

而我们需要做的就是,直接编写 文章的添加/修改/删除 的 代码逻辑。

也就是说:我们的程序在调用方法之前,就会做相应的判断。


这就跟我们现在坐地铁是一样的,坐之前,会对我们进行安检。

以前可能是有乘务人员进行检查,看看有没有带了可疑物品。

这就绪要资金啊,你得雇人啊!

而现在呢,只需要花一台机器的钱,在加一个监守人员就够了。

成本大大降低!

别看机器比几个人的一个月工资加起来都多,但是!这是一次性消费!

不想雇那么人,每个月都是拿工资的。

而且,时间一长,过个一年,其消耗的资金超过了一台机器。

而且,机器是不会偷懒的,出错的概率是非常小的。


此时,安检机器,就是相当于是 AOP 思想,在乘客乘坐之前,我来做统一的校验。

确保安全后,再上车。

这个时候,就不需要担心有人会带危险物品上车了。

乘坐的安全性,大大提升!


AOP 就是做着这样的一件事:它可以对某一类事件做集中的处理 。

拿前面的用户登录来说,它就是属于一类事件。并且,多个地方都会使用。

OK,提取出来,集中放在一个地方来实现。

然后,其它在写业务的地方,需要使用 用户登录 的操作,就不需要再去管了!

因为我们已经写了一个

拦截规则

,符合这些拦截规则的所有的 URL,走到这一块之后,就不能再去直接访问URL,而是先经过校验后,并且,通过后,才能去访问后面的代码。

没通过,只返回一个 登录的错误信息,即可。

这就是 AOP 思想 在代码中的实现:“拦截规则”




2、为什么要使用 AOP?


想象⼀个场景,我们在做后台系统时,除了登录和注册等⼏个功能不需要做⽤户登录验证之外,其他⼏乎所有⻚⾯调⽤的前端控制器( Controller)都需要先验证⽤户登录的状态,那这个时候我们要怎么处理呢?

我们之前的处理⽅式是每个 Controller 都要写⼀遍⽤户登录验证,然⽽当你的功能越来越多,那么你要

写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会提高代码修改和维护的成本。那有没

有简单的处理⽅案呢?答案是有的,

对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑实现 AOP 思想来统⼀处理了。


这个其实在前面已经讲得很清楚了。

直白来说:使用 AOP 的主要原因:

1、有些代码通用性较强,并且使用频繁,冗余度高。使用 AOP 可以降低使用的成本。

2、处于对 业务的安全性来考虑,不得不做一个安全的校验。类似的业务有很多,于是 使用 AOP 这种思想,是非常好的。既能降低代码量,又能保证 安全性。


基于这两个主要的原因,所以我们要使用 AOP:在一个统一的位置,进行统一的处理。让后面写代码的时候,程序员没有后顾之优,就是不需要担心安全 性问题。而且,由于是在同一个位置实现的,所以,不会影响到其它代码的执行。


除了统⼀的⽤户登录判断之外,AOP 还可以实现:

1、统⼀⽇志记录


在我们去记录所有日志的时候,不需要我们每个地方都去写 日志信息 :这个类它的日志内容是什么,发生的时间,执行哪个方法。

这个时候,我们可以把所有的方法,全部拦截。

然后,在执行方法之前(之后 / 执行当中),我们都可以记录日志的。

并且,我们是在一个统一的地方去写的,不会干扰到原来的业务执行。

原来的业务逻辑,该怎么写,还是怎么去写。

我们做一个 拦截器,一个AOP,专门去解决这个问题(统一记录日志)。


2、统⼀⽅法执⾏时间统计

我们课可以统计所有方法的执行时间,我们只需要设置一个拦截器。

然后,在这个拦截器里面 实现 两个方法。

1、执行方法之前的前置方法

2、执行完方法之后的后置方法。

在前置方法中开启一个计数器,记录方法的启动时间,等这个方法执行完之后,在后置方法中记录一个结束时间。

拿 结束时间 减去 开始时间,不就是 方法的执行时间了嘛。

而且,所有方法的执行时间,我们都可以通过这个方法来获取。

这样做,可以方便我们进行 大数据的观测,看看那些方法执行的的比较慢,进行一个统计。

将那些运行最慢的方法,留作 优化内容之一。


3、统⼀的返回格式设置

通常我们都是顶一个通用的类,来完成对返回格式的统一。

使用到这个类的时候,我们需要去new,去设置返回的内容信息。

其实,还有一个更简单的做法:

比如:我们在 添加/删除/修改 用户信息的时候,不是返回一个受影响的行数嘛。

这个时候,我们就可以对所有的方法进行一个拦截。

然后,拦截完之后呢,操作返回的结果,无非就是一个整数嘛。

此时,我们既可以对其进行处理:拼接一个 状态码 和 message。

也就是说:你只需要返回操作的结果,后面,我会帮你进行包装。

其它方法返回的结果,也都会包装成这种格式,从而完成格式的统一。

最后,返回这个数据返回给前端。


4、统⼀的异常处理

这个功能是非常使用的!

如果在我们不做统一异常处理的前提下,那我们的前后端就会出现一个非常尴尬的问题。

在某一些请求下,你会发现程序报错了,但是前端没有任何处理,因为报错信息的状态是 500,也就是服务器代码出现了问题。而且,更主要的是后端没有将其打包成一个json 格式的 错误信息。

所以,前端就蒙了。因为 前端 ajax 里面的 success 识别不了这样的信息,只能识别 json 格式的数据。

那么,我们有了 AOP 之后,可以对所有当前项目中的所有异常,做一个拦截。

只要 你出现 500 了,立马能感应到。

感应到之后,进行拦截。拦截之后,把这些异常封装成 JSON 格式。

异常信息,作为 message 属性 的 value 值。

然后,再把 转换后的 json 数据 返回给前端。

此时,就不会出现 前端 无法做出对应处理的事情了。

因为后端返回的数据是 json 格式,它是能识别的。


5、事务的开启和提交

如果没有 AOP,我们想要在成序中实现 事务 是很复杂的。

这个后面会讲:事务的代码实现的方式 和 注解实现的方式。

注解实现的方式,就是 使用的 AOP 。

如果不使用注解的方式来写事务,你会发现代码要写6,7行。而且给你的感觉很别扭。

但是有 AOP 之后,一个注解,直接搞定。

所有的流程,都是自动化的,不用我们去手动编写。

等等。。


也就是说使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。




Spring AOP 应该怎么学习呢?


Spring AOP 学习主要分为以下 3 个部分:

1、 学习 AOP 是如何组成的?也就是学习 AOP 组成的相关概念。

2、 学习 Spring AOP 使⽤。

3、 学习 Spring AOP 实现原理。

下⾯我们分别来看。




AOP 组成



切⾯(Aspect)


切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。


AOP,是面向切面编程。那 切面(aspect)就是 AOP 里面的关键了。

这个切面就是某一方面的意思。

那么,切面(aspect)具体是什么?

比如:AOP 是要针对某一方面的功能,做一个统一的处理。

那,是哪一个方面呢?这一方面就是 切面(aspect)定义的部分。

是统一日志记录?统一方法执行时间的统计?统一数据数据返回格式?统一异常处理?

还是说:统一事务开启和提交 和 用户登录的校验?

切面具体的功是到是哪一个?

这么说吧:一个功能代表着一个切面。

也就说,上述 6 中功能(时间),每一个都算是一个切面(aspect)。


切⾯是包含了:通知、切点和切⾯的类,相当于 AOP 实现的某个功能的集合。


说简单点:切面,定义了 AOP 针对的是哪一个统一功能的集合。

即:每一个统一的功能,都可以叫做切面。

并且,切面 是由 切点 和 通知 组成的。

切面是可以调用其它切面中的方法的。



连接点(Join Point)


应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为。


连接点 相当于 需要被增强的某个 AOP 功能的某个⽅法。

AOP 中所有的方法,都可以被称为是一个连接点。

举个例子,比如:我们要实现一个验证用户登录的切面。

然后,验证用户登录的切面中,是有很多方法的!

假设我们程序中有一百个方法,只有2个方法(注册,登录),它们是不要验证登录状态的。剩余的 98 个方法,可以被称为是一个个 连接点。



连接点,表示 所有可能触发 AOP (拦截方法的点)。

即通过这些连接点,就可以进入到 与 它“连接”的方法(需要验证登录状态的方法)。

在这里插入图片描述

既然,都可以进入到方法里面了,自动就可以添加某种新的行为。



切点(Pointcut)


注意!切点和连接点,是不一样的。

连接点是使用方,当需要使用 AOP 的时候,会触发连接点。

切点(Pointcut)是提供者。

Pointcut 是匹配 Join Point 的谓词。

Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述),用来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。


这么说吧:Pointcut 提供的一组规则,根据这组规则,找到 那 98 个 需要验证登录状态的方法,将其组合成一个集合。将 另外两个方法排除。

并且,会给匹配到的方法,发送 Advice(通知)。

通知,就是告诉你,要做事了。



举个例子:

政府对我们村发送补助,内容是针对所有的村民,每人补助 200 元。

这件事,由村支书负责落实到位。

于是,村支书就需要挨家挨户的去通知他们,什么时候来领取补助。


也就是说 通知,就是我具体要实现的事是什么。

比如说,我们要实现用户登录状态的检查,它就是在登录里面去写的



切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,⽽连接点就是表中⼀条⼀条的数据)。

在这里插入图片描述


切点:就是定义 AOP 拦截的规则。

前面说到:切面 是有 切点 和 通知组成的。

也就是说:切面 不止有一个切点 和 通知。

另外,切面是一个类,类里面具体要实现什么方法,是切面说的算的!

切面,就像公司的总经理,负责发布任务,切点,就是中层领导,规划任务和人员,制定计划。

不同的人,负责工作内容是不一样的,每一个人就是一个 连接点。

通知,告诉每个人负责工作的具体内容是什么,然后他去实现。



通知(Advice)


切⾯也是有⽬标的 ——它必须完成的⼯作。

在 AOP 术语中,切⾯的⼯作被称之为通知。

通知在切点中讲的非常清楚,我们就不再赘述

就是说: 我们要实现业务代码,就会写在通知里。


通知:定义了切⾯是什么,何时使⽤,其描述了切⾯要完成的⼯作,还解决何时执⾏这个⼯作的问题。

通知(Advice):规定 AOP 执行的时机 和 执行的方法。

就是说:AOP 执行的时机,是在调用方法之后,还是在调用之前、还是方法的整个调用期间,都执行呢?对吧。

这个 AOP 执行的时机,就非常重要。

下面,就介绍了 关于 执行时机 的注解。


Spring 切⾯类中,可以在⽅法上使⽤以下注解,设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:

前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。

后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。

返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。

抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。


环绕通知使⽤ @Around:通知包裹了的⽅法(集合中的连接点),在被通知的⽅法收到通知之前和调⽤之后执⾏⾃定义的⾏为。



AOP 整个组成部分的概念如下图所示,以多个⻚⾯都要访问⽤户登录权限为例:

在这里插入图片描述




Spring AOP 实现


想要实现 AOP 的话,我们需要关注的是:


1、定义一个切面。

2、定义一个切点

4、定义相关的通知

至于 连接点,是本来就存在的方法。


Spring AOP 的实现步骤如下:

1、 添加 Spring AOP 框架⽀持。

2、 定义切⾯和切点。

3、 定义通知。

有的人可能会很好奇:为什么 切面 和 切点 要放在一起定义?

这是因为:切面 和 切点,都是方法的方法体,是没有具体的实现的。

切面,本质上就是一个类,加了一个标识就成为一个 切面(类),是具体使用场景。

切点,就是制定拦截规则,但是有方法实现吗?没有。

凡是满足拦截规则,都会拦截下来,执行相应的通知。

通知才是具体的实现方法。

所以,它们放在一起定义,是没有问题。

当然,你硬要分析一点,分成四步,也行。

定义切面必须在前面,切面 是包含 切点的。


1、 添加 Spring AOP 框架⽀持。

2、 定义切⾯

3、 定义切点

4、定义通知。


接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的⽅法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。




1、 添加 Spring AOP 框架⽀持。


添加 Spring AOP 框架支持有两个场景:


1、 创建新项目时,添加 Spring AOP 框架的支持。

2、项目已将创建好了,但是没有添加 Spring AOP 框架,现在要补上。

PS: Spring AOP 项目还是基于 Spring Boot 实现的。

现在几乎全部的项目 都是 Spring Boot,因为它是在太香了。


1、 创建新项目时,添加 Spring AOP 框架的支持。

社区版创建 Spring Boot 项目,可以参考

SpringBoot 的 概念、创建和运行


在这里插入图片描述

Spring AOP 框架, 在创建新项目的时候,搜索不到。

Spring Boot 项目中,有没有内置 AOP 框架。

这个时候,我们就需要借助 中央仓库了

https://mvnrepository.com/


在这里插入图片描述

此时,一个 Spring AOP 项目就差不多创建完成了。

那么,引入 Spring AOP 的 第二种情况,就不用我说了吧。

是一样的添加方式。

我们直接把 引入依赖的 maven 连接给你们。

注意!,我说的是 差不多创建完成了。

也就是还有创建完成。

还需要对引入的依赖,进行修改。

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.7.1</version>
</dependency>


细节拓展: Spring AOP 依赖的版本号标签是可以省略的。

虽然 Spring AOP 没有 作为一个常用框架,导致我们引入框架的时候,需要借助 Maven 中央仓库来引入。


但是!Spring Boot 里面,其实有记录 Spring AOP 的 版本关联信息、

它可根据当前项目的环境,自动引入合适版本的 Spring AOP.

在这里插入图片描述

此时,一个 Spring AOP 项目,才真正创建成功了。

哦,对了。

我们还需要把一些无用的文件删除掉。

在这里插入图片描述




2、定义切⾯

在这里插入图片描述


切面是一个类。

此时,我们就把一个切面类给定义好了。




3、 定义切点


切面类定义好了,下面就是 制定 拦截规则。


前面我们定义切面的时候,使用了一个 @Aspect 注解 来声明一个类是切面类。

那么,切点 是不是使用一个 @Pointcut 注解呢?

确实是!

在这里插入图片描述

在这里插入图片描述




4、定义通知。


Spring 切⾯类中,可以在⽅法上使⽤以下注解,设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:

前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。

后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。

返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。

抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。


环绕通知使⽤ @Around:通知包裹了的⽅法(集合中的连接点),在被通知的⽅法收到通知之前和调⽤之后执⾏⾃定义的⾏为。


实现通知方法:在什么时机执行什么方法。

下面,我们以前置方法为例,演示一下。




前置通知


前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。

目前暂且,我们的其值方法就做什么呢?就打印一条输出语句,就行了。

在这里插入图片描述




验收阶段


经过前面那4步,我们的 AOP 就完成了。

下面我们就来验收成果。


1、把拦截对象 UserController 类创建了。

2、在里面构造几个方法。

3、使用浏览器去访问方法

在这里插入图片描述

而且,我们每一次访问方法,都会被拦截下来。

不行。你就刷新几次网页。

你就会看到下面的效果。

在这里插入图片描述

sayHello 方法也可以来访问一下。

在这里插入图片描述

这就是 AOP 的实现。




小结


我们 AOP 的实现的流程,并不难。

难就难在 切点的拦截规则的编辑。

下面,我们就针对它来进行重点分析。



AspectJ语法 详解

在这里插入图片描述




继续演示定义相关通知


通知定义的是被拦截的⽅法具体要执⾏的业务,⽐如⽤户登录权限验证⽅法就是具体要执⾏的业务。

Spring AOP 中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:


前置通知使⽤@Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。

(已演示)

后置通知使⽤@After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。

返回之后通知使⽤@AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。

抛异常后通知使⽤@AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。

环绕通知使⽤@Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。




1、前置通知 – 已演示


在 切面类中 定义一个方法,使用@Before注解,使其成为一个 前置方法。

另外,在@Before注解中需要标明 它针对切点是那一个(需要标明切点方法的名称)。

这样切点拦截下来的方法(连接点),在执行之前,需要先执行前置方法。

在这里插入图片描述




2、后置通知


后置通知使⽤@After:通知⽅法会在⽬标⽅法执行完成之后,或者抛出异常后,被调⽤。

使用的方式 和 @Before 注解 是一样的。

在这里插入图片描述

下面我们来看看效果:

在这里插入图片描述




后置通知:等目标方法执行完成之后,被调用


返回之后通知使⽤@AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。

由于我的idea已经是设置了 热部署的,项目会自动的进行重启。

没有设置热部署的朋友,可以参考这篇文章

Spring MVC 程序开发


在这里插入图片描述


下面,我们来看一下效果。

在这里插入图片描述


我们可以的出一个结论:

@AfterReturning 修饰的方法执行的优先级 比 @After 修饰的方法执行的优先级更高。




后置通知:如果目标方法在执行期间,抛出异常,会被调用


抛异常后通知使⽤@AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。

在这里插入图片描述

这里又可以得出一个小结论:

Spring AOP 代码 与 代码 之间的执行,是互不干扰。

你代码抛出了异常,并不会影响我们 AOP 代码运行。

而且,@AfterThrowing 方法 执行的优先级 也比 @After 方法 高。

另外,@AfterThrowing 方法 执行的时候,@AfterReturning 方法 是不会执行的。

因为两者的执行条件,是不一样的。

@AfterThrowing :连接点(方法)发生异常时,会被调用。

@AfterReturning::连接点(方法)执行完成之后,会被调用。

反过来,@AfterReturning 方法 执行的时候,@AfterThrowing 方法 是不会执行的。

在这里插入图片描述

而且,前置通知 和 后置通知,它们的执行 稳得一批!!!、

不管代码执行是否出现错误,它们都能正常执行。




练习:计算一个方法的执行时间。 – 前篇


有的人学的不错,说:

我们可以在 其值方法中 加一行代码,记录 开始时间。

然后,再在 后置方法中 记录 结束时间。

最后,两者相减,不就得到了 拦截到的方法的执行时间了嘛!


这样做,真的对吗? 是不对。

这得看情况。


如果是在单线程的环境下(同一时刻,只有一个线程在访问该方法),使用上述方式,没有问题。

但是!

在多线程的情况下,有多个用户访问 会被拦截下来的方法,每一次访问,都会调用 前置方法。

这会导致, 前置方法记录的开始时间,会不停被刷新(覆盖),最终记录的是 最后一个线程访问的时间。

后置方法,也是同样的情况。

也就是说我们最终相减的情况:

哪一次的开始时间 减去 哪一次 结束时间,我们都是无从获知的!

而且,得出非常多,数量取决访问的线程有多少。


那么,问题来了!

前面我不是说: AOP 可以统⼀⽅法执⾏时间的统计嘛。

但是,遇到问题了、

那么,我们该怎么做呢?

.

有的人可能会说:这是线程安全问题,加锁呗!


对不起,不行!这就是全局的问题,你加锁也解决不了问题。

但是!我们不是剩一个 还童通知吗?

解决的办法,就在这里。


下面,我们就来先了解一下 环绕通知。




3、环绕通知


环绕通知使⽤@Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。

形象来说:环绕通知,就是把 整个连接点(方法)包裹起来了,那我们就可以“为所欲为”了。

比如说:

我们执行的方法 是在当前通知里面去执行的,所以,我们就可以针对每一个方法去记录开始时间和结束时间。


因为在每一次在执行目标方法(连接点)和 通知 的时候,它们是在一块的。给人的感觉就像是具有了

事务的原子性


在这里插入图片描述


下面我们先来实现一个环绕通知。

在这里插入图片描述

下面我们再来具体看一下环绕通知的执行流程

在这里插入图片描述




练习(环绕通知):计算一个方法的执行时间。 – 后篇


废话不对说!直接上图。

在这里插入图片描述


当然,你使用 System.currentTimeMillis() 也是可以的。

只是说在 Spring 环境,使用配套的东西,效果会更好。

而且,stopWatch.getTotalTimeMillis() 方法,底层也是基于System.currentTimeMillis() 来实现的。

这也是框架的一大优势,把我们要使用的东西,都包装起来了。

复杂的调用代码不咋需要我们去写了,直接拿着就用。而且可选功能更多。




Spring AOP 实现原理 – 升华


下面我们来给大家做一个小小的升华。

难道你们就不好奇为什么我们使用 Spring AOP 可以实现 上述的这些功能呢(拦截,方法的统计)?


我除了要学习它的理论 和 使用 之外,还需要了解它的实现原理。


Spring AOP

是构建在

动态代理

的基础上,因此 Spring 对 AOP 的⽀持

局限于方法级别的拦截




代理,这个词,在我讲

日志

的时候,讲过它的好处。

用户操作日志的时候,是通过门面模式 Slf4j 去操作底层的 logback 实现。

Slf4j 就是起到一个代理的作用, 所有的用户操作日志的时候,操作是 SLF4J,然后,SLF4J 再去 对接 底层的实现。

具体的实现,还是需要靠底层才能实现的。

但是!对接的时候,不需要对接所有的代码。


日志中使用 代理的好处:可以让我们的代码只写一份,用户只和 “代理” 进行交互。然后,由 “代理” 去完成底层的操作(实现)。

这就能保证 代码的通用性 了,这就是日志文件中 使用 代理的好处。


当 “代理” 放到 Spring AOP 这一块,它有什么好处呢?

它的好处:就不再是 代码的通用性,而是说,有了这个代理之后,我可以咋执行 目标方法之前,或者是之后,甚至是 整个方法执行的期间,做一些事情。

也就是说:之前咱们程序是这样执行的。

在这里插入图片描述

如果再深入一点: 动态代理又是怎么实现的?

来看下面。



Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。


JDK Proxy:JDK 代理

CGLIB:( Code Generation Library – 说明字库生成工具源代码 ) 是一个开源项目。

是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成。



还记得前面 debug 的时候,我们查询 环绕通知 执行的目标方法名称 的时候,涉及到了 CGLIB 的!

在这里插入图片描述

也就是说: 环绕通知 是 基于 CGLIB 实现的 “代理功能”。‘


那么,问题来了: 为什么 Spring AOP 动态代理的实现,会有两种方式呢?’

使用 官方的 JDK Proxy 不好吗?为什么还有再加一个 CGLIB。

其中的缘由 和 Spring AOP 是一样的。


JDK proxy ,并不好用。

效率不高,性能不高。

而 CGLIB 性能高。

所以说:Spring AOP 两种 都 采用了。

那么,问题又来了!

既然,Spring AOP 采用 两种方式 来实现 动态代理,那么,用到 动态代理的时候,会调用哪一种方式 来 实现 动态代理呢?


这就买奶茶一样,牌子很多,需要作出选择。

如果 奶茶店只有一个 品牌,就不会存在这样的问题了。


默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。


这里还存在这一个细节:

具体调用 那一个方式 创建 动态代理,还需要看 Spring的版本。

在 Spring 的 4.2 之前,它是遵守 上述规则的。

一个类实现了某个接口,AOP 就会基于 JDK 生成 代理类。

反之,AOP 就会基于 CGLIB 生成代理类。


但是!在 4.2 之后,它默认的情况,就能使用 CGLIB ,就使用它。反观,JDK Proxy 能不用就不用。

原因也很简单:CGLIB 效率高!



又有一个问题来了!

什么情况下,用不了 CGLIB 方式 来实现 动态代理?


首先,我们需要 CGLIB 的实现原理。

CGLIB 是基于:生成目标对象的一个子类,来实现动态代理的!

就是在实现 动态代理之前,会创建一个类,来继承目标对象。

这样做,子类就会拥有目标对象的所有方法了。

这个时候,再使用它生成 动态代理的时候,子类已经拥有父类的一切了。


浓缩一下:CGLIB 就是通过 继承代理对象来实现 动态代理 的(子类拥有父类的所有功能)。

这又会延伸出另外一个问题。

如果目标对象是一个最终类,会怎么样?

最终类:被 final 修饰的类,是不可被继承的类。

所以,CGLIB 不能 代理 目标对象为 最终类 的类。

因为,最终类 违背了 CGLIB 的运行原理。

这个时候,才会去 使用 JDK Proxy 生成 动态代理。

这就是 CGLIB 不可用的场景。


总结:Spring AOP 实现 动态代理的方式,“主力” 为 CGLIB Proxy。“替补” 为 JDK Proxy。

理由: CGLIB Proxy 的性能更高。

“替补” JDK Proxy 上场情况: 目标对象 为 最终类的时候,也就是不满足 CGLIB Proxy 的执行条件的时候,JDK Proxy 才会 “上场”。

在这里插入图片描述




织⼊(Weaving):代理的生成时机


织入 ,与 AOP 的4个定义(切面,切点,连接点,通知) 是 并列的关系。织入,就是 AOP 第5个定义。

织⼊是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对象中。

说白了:织入,就是描述 动态代理 是在什么时候生成的。和标题的意思是一样的。


无论是通过哪种方式生成的 动态代理,都会涉及到 代理的生成时机。

就是说:动态代理是在什么时候生成的?

是像 lombok 一样,在idea编译的时候,就把 这个 动态代理给生成了呢?

还是说:在JVM 启动的时候,就把 这个 动态代理给生成了呢?

还是说:JVM 已经启动成功了,当我们调用代理类的时候,就把 这个 动态代理给生成了呢?


在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊,一共三个点。

也就是说: 我们动态代理生成的时机分为3个部分:

编译期:

切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。


类加载器:

切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。


运⾏期:

切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。


那么,问题来了。

Spring AOP 的动态代理 生成的时机 是在哪一个时期呢?

Spring AOP 的动态代理 生成的时机 是在

运行期


(“懒汉模式”:用到的时候,才会去生成。)


我们学习 Spring 框架中的AOP,主要基于两种⽅式:JDK 及 CGLIB 的⽅式。

这两种⽅式的代理⽬标都是被代理类中的⽅法

在运⾏期,动态的织⼊字节码⽣成代理类。

CGLIB是Java中的动态代理框架,主要作⽤就是根据⽬标类和⽅法,动态⽣成代理类。

Java中的动态代理框架,⼏乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。

字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码⽂件信息,修改部

分信息,或动态⽣成⼀个 class。




JDK 动态代理实现(依靠反射实现) – 了解即可


JDK 实现时,先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代理类。

以下为代码实现:

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler
{
    //⽬标对象即就是被代理对象
    private Object target;
    public PayServiceJDKInvocationHandler( Object target) {
        this.target = target;
    }
    
    //proxy代理对象,method 执行的目标方法,args 执行方法所需的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        
        //2.记录⽇志
        System.out.println("记录⽇志");
        
        //3.时间统计开始
        System.out.println("记录开始时间");
        
        
        //通过反射调⽤被代理类的⽅法 - 重点
        // invoke 就是实例反射的意思,把 目标对象 target 和 响应的参数args,传进去
        Object retVal = method.invoke(target, args);
        
        
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
    // PayService 它是一个接口,但对接的类 需要根据实际情况来决定
    // 下面就是 对应着 阿里的支付服务的实体类
        PayService target= new AliPayService();
        
       //⽅法调⽤处理器
        InvocationHandler handler =
                new PayServiceJDKInvocationHandler(target);
                
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                handler
        );
        // 调用 代理类
        proxy.pay();
    }
}



CGLIB 动态代理实现 – 了解即可


实现的方式 和 JDK 的一摸一样。


只有3处不同。

1、实现接口换成了 MethodInterceptor

2、重写方法的传参发生了改变。

3、调用的时候,比较简单一些。

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
    //被代理对象
    private Object target;
    public PayServiceCGLIBInterceptor(Object target){
        this.target = target;
    }
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
       //1.安全检查
        System.out.println("安全检查");
       //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        
        //通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
        PayService target= new AliPayService();
        PayService proxy= (PayService) Enhancer.create(target.getClass(),
                new PayServiceCGLIBInterceptor(target));
        proxy.pay();
    }
}



JDK 和 CGLIB 实现的区别


1、JDK 是官方提供的;CGLIB 是第三方提供的。

2、CGLIB 比 JDK 更高效

3、CGLIB 是通过 实现 继承 代理对象 来实现 动态代理的。

如果代理的对象是 最终类(不可被继承的类),Spring AOP 才会去调用 JDK 的方式生成 动态代理。




总结


AOP 是对某⽅⾯能⼒的统⼀实现,它是⼀种实现思想。

Spring AOP 是对 AOP 的具体实现,Spring AOP 可通过 AspectJ(注解)的⽅式来实现 AOP 的功能,Spring AOP 的实现步骤是:


1、 添加 AOP 框架⽀持。(删除版本号,再去刷新触发依赖下载)

2、 定义切⾯和切点。(定义一个切面类,在里面定义一个 切点的方法,并制定切点的拦截规则)

3、 定义通知。(在切面类中,定义一个普通方法,加上 通知的注解,使其成为一个通知)


Spring AOP 是通过动态代理的⽅式,在

运⾏期

将 AOP 代码织⼊到程序中的,它的实现⽅式有两种:


JDK Proxy 和 CGLIB。

默认情况下,是调用 CGLIB 来创建 动态代理。

只有在 代理对象是一个最终类的情况下,才会去调用 JDK 来创建 “动态代理”



版权声明:本文为DarkAndGrey原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。