irpas技术客

JavaEE进阶 - Spring AOP - 细节狂魔_Dark And Grey

网络 7085

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

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


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #JavaEE进阶 #Spring #AOP #细节狂魔 #在介绍