JavaEE进阶 - Spring AOP - 细节狂魔

2年前 (2022) 程序员胖胖胖虎阿
276 0 0

文章目录

  • 1.什么是 Spring AOP?
  • 2、为什么要使用 AOP?
  • Spring AOP 应该怎么学习呢?
  • AOP 组成
    • 切⾯(Aspect)
    • 连接点(Join Point)
    • 切点(Pointcut)
    • 通知(Advice)
    • AOP 整个组成部分的概念如下图所示,以多个⻚⾯都要访问⽤户登录权限为例:
  • Spring AOP 实现
    • 1、 添加 Spring AOP 框架⽀持。
    • 2、定义切⾯
    • 3、 定义切点
    • 4、定义通知。
      • 前置通知
    • 验收阶段
    • 小结
      • AspectJ语法 详解
    • 继续演示定义相关通知
      • 1、前置通知 - 已演示
      • 2、后置通知
        • 后置通知:等目标方法执行完成之后,被调用
        • 后置通知:如果目标方法在执行期间,抛出异常,会被调用
      • 练习:计算一个方法的执行时间。 - 前篇
      • 3、环绕通知
      • 练习(环绕通知):计算一个方法的执行时间。 - 后篇
  • Spring AOP 实现原理 - 升华
    • 织⼊(Weaving):代理的生成时机
    • JDK 动态代理实现(依靠反射实现) - 了解即可
    • CGLIB 动态代理实现 - 了解即可
    • JDK 和 CGLIB 实现的区别
  • 总结

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 (拦截方法的点)。
即通过这些连接点,就可以进入到 与 它“连接”的方法(需要验证登录状态的方法)。
JavaEE进阶 - Spring AOP - 细节狂魔
既然,都可以进入到方法里面了,自动就可以添加某种新的行为。

切点(Pointcut)

注意!切点和连接点,是不一样的。
连接点是使用方,当需要使用 AOP 的时候,会触发连接点。
切点(Pointcut)是提供者。
Pointcut 是匹配 Join Point 的谓词。
Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述),用来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。

这么说吧:Pointcut 提供的一组规则,根据这组规则,找到 那 98 个 需要验证登录状态的方法,将其组合成一个集合。将 另外两个方法排除。
并且,会给匹配到的方法,发送 Advice(通知)。
通知,就是告诉你,要做事了。

举个例子:
政府对我们村发送补助,内容是针对所有的村民,每人补助 200 元。
这件事,由村支书负责落实到位。
于是,村支书就需要挨家挨户的去通知他们,什么时候来领取补助。
也就是说 通知,就是我具体要实现的事是什么。
比如说,我们要实现用户登录状态的检查,它就是在登录里面去写的

切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,⽽连接点就是表中⼀条⼀条的数据)。
JavaEE进阶 - Spring AOP - 细节狂魔

切点:就是定义 AOP 拦截的规则。
前面说到:切面 是有 切点 和 通知组成的。
也就是说:切面 不止有一个切点 和 通知。

另外,切面是一个类,类里面具体要实现什么方法,是切面说的算的!
切面,就像公司的总经理,负责发布任务,切点,就是中层领导,规划任务和人员,制定计划。
不同的人,负责工作内容是不一样的,每一个人就是一个 连接点。
通知,告诉每个人负责工作的具体内容是什么,然后他去实现。

通知(Advice)

切⾯也是有⽬标的 ——它必须完成的⼯作。在 AOP 术语中,切⾯的⼯作被称之为通知。

通知在切点中讲的非常清楚,我们就不再赘述
就是说: 我们要实现业务代码,就会写在通知里。

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

通知(Advice):规定 AOP 执行的时机 和 执行的方法。
就是说:AOP 执行的时机,是在调用方法之后,还是在调用之前、还是方法的整个调用期间,都执行呢?对吧。
这个 AOP 执行的时机,就非常重要。
下面,就介绍了 关于 执行时机 的注解。

Spring 切⾯类中,可以在⽅法上使⽤以下注解,设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:
前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。

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

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

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

JavaEE进阶 - Spring 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 的 概念、创建和运行
JavaEE进阶 - Spring AOP - 细节狂魔
Spring AOP 框架, 在创建新项目的时候,搜索不到。
Spring Boot 项目中,有没有内置 AOP 框架。
这个时候,我们就需要借助 中央仓库了https://mvnrepository.com/
JavaEE进阶 - Spring AOP - 细节狂魔
此时,一个 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.
JavaEE进阶 - Spring AOP - 细节狂魔
此时,一个 Spring AOP 项目,才真正创建成功了。

哦,对了。
我们还需要把一些无用的文件删除掉。
JavaEE进阶 - Spring AOP - 细节狂魔


2、定义切⾯

JavaEE进阶 - Spring AOP - 细节狂魔

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


3、 定义切点

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

前面我们定义切面的时候,使用了一个 @Aspect 注解 来声明一个类是切面类。
那么,切点 是不是使用一个 @Pointcut 注解呢?
确实是!
JavaEE进阶 - Spring AOP - 细节狂魔
JavaEE进阶 - Spring AOP - 细节狂魔


4、定义通知。

Spring 切⾯类中,可以在⽅法上使⽤以下注解,设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:
前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。

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

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

实现通知方法:在什么时机执行什么方法。
下面,我们以前置方法为例,演示一下。


前置通知

前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
目前暂且,我们的其值方法就做什么呢?就打印一条输出语句,就行了。
JavaEE进阶 - Spring AOP - 细节狂魔


验收阶段

经过前面那4步,我们的 AOP 就完成了。
下面我们就来验收成果。
1、把拦截对象 UserController 类创建了。
2、在里面构造几个方法。
3、使用浏览器去访问方法
JavaEE进阶 - Spring AOP - 细节狂魔
而且,我们每一次访问方法,都会被拦截下来。
不行。你就刷新几次网页。
你就会看到下面的效果。
JavaEE进阶 - Spring AOP - 细节狂魔
sayHello 方法也可以来访问一下。
JavaEE进阶 - Spring AOP - 细节狂魔
这就是 AOP 的实现。


小结

我们 AOP 的实现的流程,并不难。
难就难在 切点的拦截规则的编辑。
下面,我们就针对它来进行重点分析。

AspectJ语法 详解

JavaEE进阶 - Spring AOP - 细节狂魔


继续演示定义相关通知

通知定义的是被拦截的⽅法具体要执⾏的业务,⽐如⽤户登录权限验证⽅法就是具体要执⾏的业务。
Spring AOP 中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:

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

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

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

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

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


1、前置通知 - 已演示

在 切面类中 定义一个方法,使用@Before注解,使其成为一个 前置方法。
另外,在@Before注解中需要标明 它针对切点是那一个(需要标明切点方法的名称)。
这样切点拦截下来的方法(连接点),在执行之前,需要先执行前置方法。
JavaEE进阶 - Spring AOP - 细节狂魔


2、后置通知

后置通知使⽤@After:通知⽅法会在⽬标⽅法执行完成之后,或者抛出异常后,被调⽤。
使用的方式 和 @Before 注解 是一样的。
JavaEE进阶 - Spring AOP - 细节狂魔
下面我们来看看效果:
JavaEE进阶 - Spring AOP - 细节狂魔


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

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

由于我的idea已经是设置了 热部署的,项目会自动的进行重启。
没有设置热部署的朋友,可以参考这篇文章Spring MVC 程序开发
JavaEE进阶 - Spring AOP - 细节狂魔

下面,我们来看一下效果。
JavaEE进阶 - Spring AOP - 细节狂魔
我们可以的出一个结论:
@AfterReturning 修饰的方法执行的优先级 比 @After 修饰的方法执行的优先级更高。


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

抛异常后通知使⽤@AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
JavaEE进阶 - Spring AOP - 细节狂魔
这里又可以得出一个小结论:
Spring AOP 代码 与 代码 之间的执行,是互不干扰。
你代码抛出了异常,并不会影响我们 AOP 代码运行。
而且,@AfterThrowing 方法 执行的优先级 也比 @After 方法 高。

另外,@AfterThrowing 方法 执行的时候,@AfterReturning 方法 是不会执行的。
因为两者的执行条件,是不一样的。
@AfterThrowing :连接点(方法)发生异常时,会被调用。
@AfterReturning::连接点(方法)执行完成之后,会被调用。

反过来,@AfterReturning 方法 执行的时候,@AfterThrowing 方法 是不会执行的。
JavaEE进阶 - Spring AOP - 细节狂魔
而且,前置通知 和 后置通知,它们的执行 稳得一批!!!、
不管代码执行是否出现错误,它们都能正常执行。


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

有的人学的不错,说:
我们可以在 其值方法中 加一行代码,记录 开始时间。
然后,再在 后置方法中 记录 结束时间。
最后,两者相减,不就得到了 拦截到的方法的执行时间了嘛!
这样做,真的对吗? 是不对。
这得看情况。

如果是在单线程的环境下(同一时刻,只有一个线程在访问该方法),使用上述方式,没有问题。
但是!
在多线程的情况下,有多个用户访问 会被拦截下来的方法,每一次访问,都会调用 前置方法。
这会导致, 前置方法记录的开始时间,会不停被刷新(覆盖),最终记录的是 最后一个线程访问的时间。
后置方法,也是同样的情况。
也就是说我们最终相减的情况:
哪一次的开始时间 减去 哪一次 结束时间,我们都是无从获知的!
而且,得出非常多,数量取决访问的线程有多少。

那么,问题来了!
前面我不是说: AOP 可以统⼀⽅法执⾏时间的统计嘛。
但是,遇到问题了、
那么,我们该怎么做呢?

.有的人可能会说:这是线程安全问题,加锁呗!
对不起,不行!这就是全局的问题,你加锁也解决不了问题。
但是!我们不是剩一个 还童通知吗?
解决的办法,就在这里。

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


3、环绕通知

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

形象来说:环绕通知,就是把 整个连接点(方法)包裹起来了,那我们就可以“为所欲为”了。
比如说:
我们执行的方法 是在当前通知里面去执行的,所以,我们就可以针对每一个方法去记录开始时间和结束时间。
因为在每一次在执行目标方法(连接点)和 通知 的时候,它们是在一块的。给人的感觉就像是具有了 事务的原子性
JavaEE进阶 - Spring AOP - 细节狂魔

下面我们先来实现一个环绕通知。
JavaEE进阶 - Spring AOP - 细节狂魔
下面我们再来具体看一下环绕通知的执行流程
JavaEE进阶 - Spring AOP - 细节狂魔


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

废话不对说!直接上图。
JavaEE进阶 - Spring AOP - 细节狂魔
当然,你使用 System.currentTimeMillis() 也是可以的。
只是说在 Spring 环境,使用配套的东西,效果会更好。
而且,stopWatch.getTotalTimeMillis() 方法,底层也是基于System.currentTimeMillis() 来实现的。
这也是框架的一大优势,把我们要使用的东西,都包装起来了。
复杂的调用代码不咋需要我们去写了,直接拿着就用。而且可选功能更多。


Spring AOP 实现原理 - 升华

下面我们来给大家做一个小小的升华。
难道你们就不好奇为什么我们使用 Spring AOP 可以实现 上述的这些功能呢(拦截,方法的统计)?
我除了要学习它的理论 和 使用 之外,还需要了解它的实现原理。

Spring AOP 是构建在动态代理的基础上,因此 Spring 对 AOP 的⽀持局限于方法级别的拦截

代理,这个词,在我讲日志的时候,讲过它的好处。
用户操作日志的时候,是通过门面模式 Slf4j 去操作底层的 logback 实现。
Slf4j 就是起到一个代理的作用, 所有的用户操作日志的时候,操作是 SLF4J,然后,SLF4J 再去 对接 底层的实现。
具体的实现,还是需要靠底层才能实现的。
但是!对接的时候,不需要对接所有的代码。
日志中使用 代理的好处:可以让我们的代码只写一份,用户只和 “代理” 进行交互。然后,由 “代理” 去完成底层的操作(实现)。
这就能保证 代码的通用性 了,这就是日志文件中 使用 代理的好处。

当 “代理” 放到 Spring AOP 这一块,它有什么好处呢?
它的好处:就不再是 代码的通用性,而是说,有了这个代理之后,我可以咋执行 目标方法之前,或者是之后,甚至是 整个方法执行的期间,做一些事情。
也就是说:之前咱们程序是这样执行的。
JavaEE进阶 - Spring AOP - 细节狂魔
如果再深入一点: 动态代理又是怎么实现的?
来看下面。

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

JDK Proxy:JDK 代理

CGLIB:( Code Generation Library - 说明字库生成工具源代码 ) 是一个开源项目。
是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成。

还记得前面 debug 的时候,我们查询 环绕通知 执行的目标方法名称 的时候,涉及到了 CGLIB 的!
JavaEE进阶 - Spring AOP - 细节狂魔
也就是说: 环绕通知 是 基于 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 才会 “上场”。
JavaEE进阶 - Spring AOP - 细节狂魔


织⼊(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 来创建 “动态代理”

版权声明:程序员胖胖胖虎阿 发表于 2022年11月3日 上午9:56。
转载请注明:JavaEE进阶 - Spring AOP - 细节狂魔 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...