Spring AOP

Spring的两大核心思想,是 IoC和AOP。接下来我们进入AOP的学习。

1. AOP概述

AOP,Aspect Oriented Programming (面向切面编程)

什么是面向切面编程呢?

切面就是指某一类特定问题 ,所以AOP也可以理解为面向特定方法编程。

什么是面向特定方法编程呢?

比如上个章节学习的"登录校验",就是一类特定问题,登录校验拦截器,就是对"登录校验"这类问题的统一处理。

所以,拦截器也是AOP的一种应用。AOP是一种思想,拦截器是AOP思想的一种实现。Spring框架实现了这种思想,提供了拦截器技术的相关接口。

同样的,统一数据返回格式和统一异常处理,也是AOP思想的一种实现。

简单来说:AOP是一种思想,是对某一类事情的集中处理

什么是Spring AOP?

AOP是一种思想,它的实现方法有很多,有SpringAOP,也有AspectJ、CGLIB等。

Spring AOP是AOP的一种实现方式


2. Spring AOP

示例:

现在有⼀个项目, 项目中开发了很多的业务功能,有一些业务的执行效率比较低,耗时较长,我们需要对接口进行优化。

第一步就需要定位出执行耗时比较长的业务方法,再针对该业务方法来进行优化

  • 定位:我们就需要统计当前项目中每一个业务方法的执行耗时。
  • 统计:可以在业务方法运行前和运行后,记录下方法的开始时间和结束时间,两者之差就是这个方法的耗时。

这种方法是可以解决问题的,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法,如果我们要在每个业务方法中都记录方法的耗时,对于程序员而言,会增加很多的工作量.

AOP就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强

  • AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性:解耦)

接下来我们来看Spring AOP如何来实现

① 引入AOP依赖

在pom.xml文件中添加配置:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

② 编写AOP程序

记录Controller中每个方法的执行时间

  1. @Aspect:标识这是一个切面类
  2. @Around:环绕通知,在目标方法的前后都会被执行.后面的表达式表示对哪些方法进行增强
  3. ProceedingJoinPoint.proceed()让原始方法(目标方法)执行

运行程序,观察日志:

3. Spring AOP详解

SprintAOP的概念:

  1. 切点
  2. 切面
  3. 通知
  4. 连接点

① 切点(Pointcut)

切点(Pointcut),也称之为"切入点"

Pointcut 的作用就是提供一组规则(使用AspectJ pointcut expression language来描述),告诉程序对

哪些方法来进行功能增强。

上面的表达式 execution(* com.book.demo.controller.*.*(..)) 就是切点表达式

② 连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法。

例如上述,所有 com.book.demo.controller 路径下的方法, 都是连接点。

java 复制代码
package com.book.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
    @RequestMapping("/addBook")
    public Result addBook(BookInfo bookInfo) {
        //...代码省略 
    }
    @RequestMapping("/queryBookById")
    public BookInfo queryBookById(Integer bookId){
        //...代码省略     
    }
    @RequestMapping("/updateBook")
    public Result updateBook(BookInfo bookInfo) {
        //...代码省略 
    }
}

上述BookController中的方法都是连接点。

切点和连接点的关系:

  • 连接点是满足切点表达式的元素,切点可以看做是保存了众多连接点的一个集合,

③ 通知(Advice)

通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间,就是通知

在AOP面向切面编程当中, 我们把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知的内容。

④ 切面(Aspect)

切面(Aspect) =切点(Pointcut)+通知(Advice)

通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作

切面既包含了通知逻辑的定义,也包括了连接点的定义.

切面所在的类, 我们⼀般称为切面类,而 @Aspect注解就是用来标识切面类的。


⑤ 通知类型

上面我们讲了什么是通知,接下来学习通知的类型,@Around就是其中一种通知类型,表示环绕通知。

Spring中AOP的通知类型有以下几种:

  • @Around:环绕通知 ,此注解标注的通知方法在目标方法前,后都被执行
  • @Before:前置通知 ,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知 ,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知 ,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知 ,此注解标注的通知方法发生异常后执行

示例:

1. 正常运行情况

测试方法:

运行程序,观察日志:

程序正常运行的情况下,@AfterThrowing标识的通知方法不会执行。

从上图也可以看出来,@Around标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑"。

其中"前置逻辑"会先于@Before标识的通知方法执行,"后置逻辑"会晚于@After标识的通知方法执

行。

2. 异常时的情况

运行程序,观察日志:

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
  • @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

也可以通过 try-catch 捕获处理异常:就可以正常运行

此时再次运行:

注意事项:

  • @Around环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行.
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.
  • 一个切面类可以有多个切点。

⑥ @PointCut

上面代码存在一个问题,就是存在大量重复的切点表达式

execution(* com.book.demo.controller.*.*(..)),Spring提供了@Pointcut注解,把公共的切点
表达式提取出来,需要用到时引用该切入点表达式即可

上述代码就可以修改为:

当切点定义使用 private 修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把 private 改为 public。引用方式为:全限定类名.方法名()

其他切面类AspectDemo2:

运行结果:

⑦ @Order 切面优先级

当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?

我们还是通过程序来求证:

定义多个切面类:

  • 为防止干扰,我们把AspectDemo这个切面先去掉(把@Component注解去掉就可以)

为简单化,只写了@Before和aAfter两个通知:

先运行程序,观察日志:

通过上述程序的运行结果,可以看出:

存在多个切面类时,默认按照切面类的类名字母排序:

  • @Before通知:字母排名靠前的先执行
  • @After通知:字母排名靠前的后执行

但这种方式不方便管理,我们的类名更多还是具备一定含义的。

Spring给我们提供了一个新的注解,来控制这些切面通知的执行顺序:@Order

使用方式如下:

重新运行程序:

通过上述程序的运行结果,得出结论:

@Order注解标识的切面类,执行顺序如下:

  • @Before通知:数字越小先执行
  • @After通知:数字越大先执行

@Order控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。

⑧ 切点表达式

上面的代码中,我们一直在使用切点表达式来描述切点,下面我们来介绍一下切点表达式的语法。

切点表达式常见有两种表达方式

  1. execution(.....):根据方法的签名来匹配
  2. @annotation(.....):根据注解匹配

1. execution 表达式

execution() 是最常用的切点表达式, 用来匹配方法,语法为:

XML 复制代码
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

其中: 访问修饰符和异常可以省略

切点表达式支持通配符表达:

    1. * :匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
    • a.包名使用 * 表示任意包(一层包使用一个*)
    • b.类名使用 * 表示任意类
    • c.返回值使用 * 表示任意返回值类型
    • d.方法名使用 * 表示任意方法
    • e.参数使用 * 表示一个任意类型的参数
  • 2... :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
    • a.使用 **..**配置包名,标识此包以及此包下的所有子包
    • b.可以使用 **..**配置参数,任意个任意类型的参数

切点表达式示例:

TestController 下的 public修饰, 返回类型为String ,方法名为t1, 无参方法

XML 复制代码
execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符

XML 复制代码
execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型

XML 复制代码
execution(* com.example.demo.controller.TestController.t1())

匹配TestController 下的所有无参方法

XML 复制代码
execution(* com.example.demo.controller.TestController.*())

匹配TestController 下的所有方法

XML 复制代码
execution(* com.example.demo.controller.TestController.*(..))

匹配controller包下所有的类的所有方法

XML 复制代码
execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的TestController

XML 复制代码
execution(* com..TestController.*(..))

匹配com.example.demo包下, 子孙包下的所有类的所有方法

XML 复制代码
execution(* com.example.demo..*(..))

2. @annotation

execution表达式更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController中的 t1() 和 UserController中的 u1() 这两个方法。

这个时候我们使用execution这种切点表达式来描述就不是很方便了.

我们可以借助自定义注解的方式以及另一种切点表达式@annotation来描述这一类的切点

实现步骤:

  1. 编写自定义注解
  2. 使用@annotation表达式来描述切点
  3. 在连接点的方法上添加自定义注解
1)自定义注解 @MyAspect

创建⼀个注解类(和创建Class文件⼀样的流程, 选择Annotation就可以了)

  • @Target标识了Annotation所修饰的对象范围,即该注解可以用在什么地方.
    • 常用取值:
    • ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明
    • ElementType.METHOD:描述方法
    • ElementType.PARAMETER:描述参数
    • ElementType.TYPE_USE:可以标注任意类型
  • @Retention指Annotation被保留的时间长短,标明注解的生命周期
    • @Retention的取值有三种:
    • 1.RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃,这意味着在运行时无法获取到该注解的信息,只能在编译时使用.比如@suppressWarnings,以及lombok提供的注解@Data,@Slf4j
    • 2.RetentionPolicy.CLASS:编译时注解,表示注解存在于源代码和字节码中,但在运行时会被丢弃,这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取.通常用于一些框架和工具的注解
    • 3.RetentionPolicy.RUNTIME:运行时注解.表示注解存在于源代码,字节码和运行时中.这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息,通常用于一些需要在运行时处理的注解,如Spring的@Controller@ResponseBody
2)切面类

使用 @annotation 切点表达式定义切点, 只对 @MyAspect 生效

切面类代码如下:

3)添加自定义注解

在TestController中的t2()和UserController中的u2()这两个方法上添加自定义注解 @MyAspect ,其 他方法不添加。

运行程序,测试接口:

可以看见,测试了 test.t2 和 user.u2,切面通知被执行了:

没有加上自定义注解@MyAspect 的其他方法,都不会执行切面通知:

⑨ Spring AOP的实现方式

  1. 基于注解 @Aspect (参考上述课件内容)
  2. 基于自定义注解 (自定义注解 @annotation)

4. Spring AOP原理

上面我们主要学习了SpringAOP的应用,接下来我们来学习SpringAOP的原理,也就是Spring是如何实现AOP的。

SpringAOP是基于动态代理来实现AOP的。

① 代理模式

代理模式,也叫委托模式。

定义:为其他对象提供一种代理以控制对这个对象的访问,它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用,

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,

使用代理前:

使用代理之后:

代理模式的主要角色

  1. Subject:业务接口类.可以是抽象类或者接口(不一定有)
  2. RealSubject:业务实现类,具体的业务执行,也就是被代理对象.
  3. Proxy:代理类.RealSubject的代理

【比如房屋租赁

Subject就是提前定义了房东做的事情,交给中介代理,也是中介要做的事情

RealSubject:房东

Proxy: 中介】

代理模式可以在不修改被代理对象 的基础上,通过扩展代理类,进行一些功能的附加与增强

根据代理的创建时期,代理模式分为静态代理和动态代理

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了.
  • 动态代理:在程序运行时,运用反射机制动态创建而成。

② 静态代理

静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的工作,就等租户来租房子了)

我们通过代码来加深理解,以房租租赁为例

1.定义接口(定义房东要做的事情,也是中介需要做的事情)

  1. 实现接口(房东出租房子)
  1. 代理(中介, 帮房东出租房子)
  1. 使用

运行结果:

上面这个代理实现方式就是静态代理

从上述程序可以看出,虽然静态代理也完成了对目标对象的代理,但是由于代码都写死了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景。

接下来新增需求:中介又新增了其他业务:代理房屋出售

我们需要对上述代码进行修改

1.接口定义修改

  1. 接口实现修改
  1. 代理类修改

从上述代码可以看出,我们修改接口(Subject)和业务实现类(RealSubject)时,还需要修改代理类

(Proxy)。同样的,如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy).

既然代理的流程是一样的,有没有一种办法,让他们通过一个代理类来实现呢?

这就需要用到动态代理技术了。

③ 动态代理

相比于静态代理来说,动态代理更加灵活。

我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建生成。

比如房屋中介,我不需要提前预测都有哪些业务,而是业务来了我再根据情况创建

我们还是先看代码再来理解。

Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:

  1. JDK动态代理
  2. CGLIB动态代理

1)JDK动态代理

JDK动态代理类实现步骤

  1. 定义一个接口及其实现类(静态代理中的HouseSubject和RealHouseSubject)
  2. 自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑
  3. 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>\[\] interfaces,InvocationHandlerh)方法创建代理对象

定义JDK动态代理类,实现 InvocationHandler 接口

创建一个代理对象并使用

代码讲解:

**1. InvocationHandler:**InvocationHandler 接口是Java动态代理的关键接口之⼀, 它定义了⼀个单一方法 invoke() , 用于处理被代理对象的方法调用;通过实现 InvocationHandler 接⼝, 可以对被代理对象的方法进行功能增强。

  • invoke 方法参数说明
    • proxy:代理对象
    • method:代理对象需要实现的方法,即其中需要重写的方法
    • args:method所对应方法的参数

**2. Proxy:**Proxy 类中使用频率最高的方法是: newProxyInstance() , 这个方法主要用来生成⼀个代理对象

  • newProxyInstance() 方法这个方法一共有3个参数:
  • Loader:类加载器,用于加载代理对象。
  • interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)
  • h:实现了InvocationHandler接口的对象

2)CGLIB动态代理

JDK动态代理 有一个最致命的问题是其只能代理实现了接口的类(只能代理接口)

有些场景下,我们的业务代码是直接实现的,并没有接口定义,为了解决这个问题,我们可以用CGLIB动态代理机制来解决。

CGLIB(CodeGeneration Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理,很多知名的开源框架都使用到了CGLIB.例如Spring中的AOP模块中:如果目标对象实现了接口,则默认采用JDK动态代理,否则采用CGLIB动态代理

CGLIB动态代理类实现步骤

  1. 定义一个类(被代理类)
  2. 自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似
  3. 通过Enhancer类的create()创建代理类

接下来看下实现:

添加依赖:

和JDK动态代理不同,CGLIB(CodeGenerationLibrary)实际是属于一个开源项目,如果你要使用它

的话,需要手动添加相关依赖

XML 复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

自定义 MethodInterceptor(方法拦截器),实现MethodInterceptor接口

创建代理类并使用

但是运行起来会报错:原因在于 JDK版本与CGLIB库之间的兼容性问题。

解决方式:添加 JVM 启动参数

在运行该 DynamicMain 类时,添加 --add-opens 参数,手动将 java.lang 包开放给 CGLIB。

  1. 点击右上角的运行配置(Run/Debug Configurations)。

  2. 在 VM options 输入框中填入:

    XML 复制代码
    --add-opens java.base/java.lang=ALL-UNNAMED

这样就能运行成功:

代码讲解:

1. MethodInterceptor: MethodInterceptor 和 JDK动态代理中的 InvocationHandler 类似, 它只定义了⼀个方法 intercept() , 用于增强目标方法。

参数说明:

  • obj:被代理的对象
  • method:目标方法(被拦截的方法,也就是需要增强的方法)
  • objects:方法入参
  • methodProxy:用于调用原始方法

**2. Enhancer.create():**用来生成⼀个代理对象

参数说明:

  1. type: 被代理类的类型(类或接⼝)
  2. callback: 自定义方法拦截器 MethodInterceptor
相关推荐
不能只会打代码2 个月前
Day 011 — Spring 全家桶深度拆解
spring boot·mybatis·spring aop·spring mvc·spring ioc
一轮弯弯的明月4 个月前
Spring AOP编程
java·开发语言·spring boot·笔记·spring aop·学习心得
Java成神之路-5 个月前
面试题:Spring AOP底层实现原理
java·spring aop
__土块__5 个月前
Java 大厂一面模拟:从类加载器到热点Key治理的连续压问
jvm·spring aop·java面试·类加载·大厂一面·mysql间隙锁·redis缓存雪崩
弹简特6 个月前
【JavaEE27-后端部分】Spring AOP 核心概念详解——把抽象变具象,让理论不再“飘”
java·spring·spring aop
弹简特6 个月前
【JavaEE29-后端部分】Spring AOP 切点表达式详解——精准定位,想切哪里切哪里
java·spring·spring aop
弹简特6 个月前
【JavaEE28-后端部分】Spring AOP 通知详解——五种“增强时机”,一网打尽
java·spring·spring aop
独断万古他化8 个月前
【Spring 核心:AOP】基础到深入:思想、实现方式、切点表达式与自定义注解全梳理
java·spring·spring aop·aop·切面编程
她说..8 个月前
Spring 核心工具类 AopUtils 超详细全解
java·后端·spring·springboot·spring aop