一:概述
(1)什么是AOP
AOP(Aspect Oriented Programming):中文名叫做面向切面编程,是一种编程范式,旨在提高模块化程度,是对某一类事情的集中处理,特别是在解决横切关注点时。横且关注点是哪些影响应用程序多个部分的问题或功能,例如日志、安全、数据验证等等。这些功能通常与业务逻辑的主流程横向加交叉,如果不恰当管理,会造成代码重复和混乱
例如:假设你有一个应用程序,其中有多个方法,你希望在这些方法执行前后记录日志消息。不使用 AOP,你可能需要在每个方法的开始和结束处添加日志记录代码,这会导致代码重复和杂乱。使用 AOP,你可以创建一个日志记录的切面,它会自动在指定方法的执行前后添加日志
AOP核心概念包括
- 切面(Aspect):一个关注点的模块化,这个关注点可能横切多个对象。
- 连接点(Join Point):程序执行中的某个特定点,比如方法的调用或异常的抛出。
- 通知(Advice):在特定的连接点上执行的动作。
- 切点(Pointcut):匹配连接点的表达式,在这些点上应用通知。
- 引入(Introduction):向现有的类添加新方法或属性。
- 目标对象(Target Object):被一个或多个切面通知的对象。
- 织入(Weaving):把切面与其他应用类型或对象连接起来,创建一个被通知的对象
使用AOP理由包括
- 改善模块化:AOP 允许将那些与核心业务逻辑不直接相关的功能(如日志、事务管理等)分离出来,从而提高代码的模块化
减少代码重复:通过集中处理横切关注点,AOP 减少了在多个地方复制相同代码的需要
提高可维护性:分离关注点使得维护特定的行为(例如日志记录策略)变得更容易,因为所有相关的代码都在一个地方
提升灵活性和可重用性:AOP 的一个关键优点是提高了程序的灵活性和可重用性,因为它允许不修改源代码的情况下动态地添加或删除功能
简化了复杂性:特别是在处理事务和安全性等企业级功能时,AOP 可以隐藏复杂性,让开发者专注于核心业务逻辑
AOP可以实现以下
- 统一的用户登录判断
- 统一的方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事物的开启和提交等
- … …
(2)Spring AOP
Spring AOP:是 Spring 框架的一个关键组成部分,它提供了一种通过切面和通知来增强对象功能的方法,是AOP的具体实现。核心概念如下
- 切面 (Aspect):切面是跨多个类或方法的模块化的关注点。在 Spring AOP 中,切面可以使用普通的类配合注解来实现,或者通过实现特定的接口
- 连接点 (Join Point):在你的程序中,这些是你能够插入一个切面的点。在 Spring AOP 中,一个连接点总是代表一个方法的执行
- 通知 (Advice):这是在切面的某个特定连接点上执行的动作。主要有以下类型:
- 前置通知 (Before advice):在连接点之前执行,比如用于日志记录或权限检查
- 后置通知 (After advice):在连接点之后执行,无论方法执行成功还是异常终止
- 返回后通知 (After-returning advice):在连接点正常完成之后执行
- 异常后通知 (After-throwing advice):在方法抛出异常退出时执行
- 环绕通知 (Around advice):在连接点前后都可以执行,它会围绕一个连接点的执行。
- 切点 (Pointcut):这是你指定的执行某个通知的位置。它是一个用于定义何处应用通知的表达式
- 目标对象 (Target Object):被一个或多个切面所通知的对象
- 代理 (Proxy):Spring AOP 使用代理作为切面的执行机制。当一个切面被应用时,Spring AOP 会动态地创建一个目标对象的代理。这个代理负责执行通知和原始方法调用
二:Spring AOP详解
(1)AOP组成
- 代码实例可以在学习完AOP后使用再观看
①:切面
切面(Aspect):是AOP的核心组成部分,它将横切关注点封装起来,一个切面可以包含一个或多个通知和切点。说简单点,切面在程序中就是一个处理某方面具体问题的类,类里面包含了很多方法。这些便是切点和通知
举例:下面是一个服务类,包含我们希望记录日志的业务方法
public class BankService {
public void deposit(double amount) {
// 存款逻辑
}
public void withdraw(double amount) {
// 取款逻辑
}
public void transfer(double amount) {
// 转账逻辑
}
}
定义日志记录切面(LogginASext
如下,这个切面定义了日志记录的行为,它将在目标方法执行前后自动执行
- 在这里,我们使用
@Before
和@After
注解来定义前置和后置通知。这些通知会在BankService
类中的所有方法执行前后被调用。
@Aspect
public class LoggingAspect {
@Before("execution(* BankService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before executing method: " + joinPoint.getSignature().getName());
// 其他前置日志逻辑
}
@After("execution(* BankService.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After executing method: " + joinPoint.getSignature().getName());
// 其他后置日志逻辑
}
}
因此,当 BankService
类的任何方法被调用时(如 deposit
, withdraw
, transfer
),LoggingAspect
中定义的通知(日志记录逻辑)将自动执行。这样,你就能够在不修改业务逻辑代码的情况下,为这些方法添加日志记录功能
②:连接点
连接点(Joint Point):代表程序执行过程中的某个特定点,或者说是指应用执行过程中能够插入切面的一个点。这些特定点通常是程序执行的某些关键时刻,例如方法调用、异常抛出等等。切面代码可以利用这些点插入到应用的正常流程之中,添加新的行为
举例:结合上面例子,对于下面的Java类,这些方法的执行可以被视为连接点。在这个类中,deposit
和withdraw
方法的执行就是潜在的连接点。如果你使用AOP,你可以在这些方法执行之前、之后或抛出异常时插入额外的逻辑(如日志记录、安全检查等)
public class AccountService {
public void deposit(double amount) {
// 存款逻辑
}
public double withdraw(double amount) {
// 取款逻辑
return amount;
}
}
③:切点
切点(Pointcut):用于指定切面应当应用于哪些连接点。换句话说,切点定义了“在哪里”执行“什么”(切面的通知),用于配置规则
- 切点本质是连接点的一个集合。它们定义了在程序的哪些点上应用切面的通知
- 切点通常使用表达式来定义。这些表达式指定了哪些类和方法与该切点匹配
举例:对于上面的AccountService
类,如果你想要在他的方法前后添加日志记录。那么你可以定义一个切点来指定这些方法,切点表达式可能会是下面这样
- 这个切点表达式匹配
AccountService
类中的所有方法。execution
是最常用的切点指示符之一,它用于匹配方法执行。*
表示匹配所有的返回类型,com.example.service.AccountService
指定了类,*
表示匹配类中的所有方法,(..)
表示匹配任意参数的方法。
@Pointcut("execution(* com.example.service.AccountService.*(..))")
private void accountServiceMethods() {}
注意
- 与通知结合使用:切点通常与通知结合使用。当一个通知(如前置通知、后置通知等)与一个切点相关联时,该通知仅在切点指定的连接点上执行。
- 切面的构成:切点是定义切面行为的关键部分。一个切面包含一个或多个切点,以及与这些切点相关联的通知
④:通知
通知(Advice):通知是切面的一个行为,定义了在切点上要执行的操作。主要通知类型及注解如下,可以在方法上使用,会设置方法为通知方法,在满足条件后会通知本方法调用
- 前置通知
@Before
- 在连接点之前执行,但并不影响连接点的执行(除非抛出异常)
- 常用于执行验证,日志记录等
- 后置通知
@After
- 在连接点执行完后执行,无论其结果如何
- 常用于资源释放、记录方法执行完成的日志等
- 返回后通知
@AfterReturning
- 尽在连接点正常完成后执行
- 常用于处理方法的返回值,执行清理等
- 异常后通知
@AfterThrowing
- 仅在方法执行过程中抛出异常时执行
- 常用于异常处理、记录错误信息等
- 环绕通知
@Around
- 在方法调用之前后之后执行,可以在方法调用前后执行自定义行为,甚至可以完全覆盖方法调用
- 是最灵活的通知类型,可以用于日志记录、事务管理、性能监控等
举例:下面是一个服务类,包含我们希望记录日志的业务方法
public class BankService {
public void deposit(double amount) {
// 存款逻辑
}
public void withdraw(double amount) {
// 取款逻辑
}
public void transfer(double amount) {
// 转账逻辑
}
}
定义日志记录切面(LogginASext
如下,这个切面定义了日志记录的行为,它将在目标方法执行前后自动执行
- 在这里,我们使用
@Before
和@After
注解来定义前置和后置通知。这些通知会在BankService
类中的所有方法执行前后被调用。
@Aspect
public class LoggingAspect {
@Before("execution(* BankService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before executing method: " + joinPoint.getSignature().getName());
// 其他前置日志逻辑
}
@After("execution(* BankService.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After executing method: " + joinPoint.getSignature().getName());
// 其他后置日志逻辑
}
}
因此,当 BankService
类的任何方法被调用时(如 deposit
, withdraw
, transfer
),LoggingAspect
中定义的通知(日志记录逻辑)将自动执行。这样,你就能够在不修改业务逻辑代码的情况下,为这些方法添加日志记录功能
(2)SpringAOP实现
①:添加AOP框架支持
创建Spring Boot项目时没有Spring AOP框架可以选择,只能创建好之后再添加依赖
找到对应版本
<!-- 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.6</version>
</dependency>
②:定义切点和通知
相关代码如下,为了能够展示清晰,只设置了前置通知和后置通知
切面类
package com.example.demo.demos.app;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect // 切面类
@Component
public class UserAspect {
// 切点:配置拦截规则
// 返回类型可以是任意类型,UserController下的所有方法,参数可以是任何类型和数量
@Pointcut("execution(* com.example.demo.demos.controller.UserController.*(..))")
public void pointcut() {}
// 通知:定义通知
@Before("pointcut()") // 针对pointcut这个拦截规则
public void beforeAdvice() {
System.out.println("执行了前置通知");
}
@After("pointcut()") // 针对pointcut这个拦截规则
public void afterAdvice() {
System.out.println("执行了后置通知");
}
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("进入环绕通知");
Object obj = null;
// 执行目标方法
obj = joinPoint.proceed();
System.out.println("退出环绕通知");
return obj;
}
}
Controller
package com.example.demo.demos.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/hi")
public String sayHi() {
System.out.println("执行了sayHi()方法");
return "Hi, ";
}
@RequestMapping("/hello")
public String sayHello() {
System.out.println("执行了sayHello()方法");
return "Hello, ";
}
}
效果
③:切点表达式
- 上面指定拦截规则的表达式称为切点表达式:
@Pointcut("execution(* com.example.demo.demos.controller.UserController.*(..))")
切点表达式:定义了一种模式,用于匹配特定的连接点,使用AspectJ表达式语言编写。AspectJ支持三种通配符
*
:匹配任意字符,只匹配一个元素- 包
- 类
- 方法
- 方法参数
..
:匹配任意字符。可以匹配多个元素,在表示类时,必须和*
联用+
:表示按照类型匹配指定类的所有类,必须跟在类名后面。例如com.cad.Car+
表示继承该类的所有子类包括本身
切点表达式由切点函数组成,最为常用的是execution()
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
下面是一些例子
// 匹配目标类的所有 public 方法,第一个 * 代表返回类型,第二个 * 代表方法名,..代表方法的参数
execution(public * *(..))
// 匹配目标类所有以 User 为后缀的方法。第一个 * 代表返回类型,*User 代表以 User 为后缀的方法
execution(* *User(..))
// 匹配 User 类里的所有方法
execution(* com.test.demo.User.*(..))
// 匹配 User 类及其子类的所有方法
execution(* com.test.demo.User+.*(..)) :
// 匹配 com.test 包下的所有类的所有方法
execution(* com.test.*.*(..))
// 匹配 com.test 包下及其子孙包下所有类的所有方法
execution(* com.test..*.*(..)) :
// 匹配 getOrderDetail 方法,且第一个参数类型是 Long,第二个参数类型是 String
execution(* getOrderDetail(Long, String))
三:Spring AOP原理
Spring AOP原理:Spring AOP构建在动态代理之上,因此Spring对AOP的支持局限于方法级别的拦截
(1)动态代理
动态代理:是一种设计模式,它允许在运行时动态地创建并管理代理对象。这种方法主要用于拦截对某个对象的方法调用,允许在调用实际对象之前或之后执行特定的操作。在Java中,动态代理是非常常见的,它主要用于实现各种中间层服务,如事务管理、日志记录、权限控制等。Java中动态代理的实现通常有两种方式
- 基于接口的动态代理(JDK动态代理):
- 这种方式是通过实现接口的方法来创建代理对象。Java的
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口是实现这种动态代理的核心 InvocationHandler
是一个接口,需要实现它的invoke
方法来定义拦截器- 当代理对象的方法被调用时,
invoke
方法会被自动调用,我们可以在这个方法中添加自己的处理逻辑 - DK动态代理只能代理实现了接口的类
- 这种方式是通过实现接口的方法来创建代理对象。Java的
- 基于类的动态代理(CGLIB等):
- 当需要代理没有实现接口的类时,就可以使用基于类的动态代理技术,如CGLIB(Code Generation Library)
- CGLIB通过继承的方式,生成目标类的子类,并在子类中覆盖父类的方法,实现方法拦截和增强
- CGLIB动态代理相比JDK动态代理,使用更加复杂,但在某些场景下更加强大
动态代理的主要优势在于它的灵活性和动态性。它允许开发者在不修改原始代码的情况下,增加或改变对象的行为。这在很大程度上简化了代码的维护和扩展。例如,在Spring框架中,AOP(面向切面编程)的实现就大量使用了动态代理技术
(2)织入
织入(Weaving):是面向切面编程(AOP)中的一个核心概念,特别是在实现代理模式时非常关键。在AOP中,织入是指将切面(Aspect)应用到目标对象以创建代理对象的过程。这个过程可以在不同的时间点发生,从而影响代理对象的生成时机。织入主要有以下几种类型
- 编译时织入:
- 这种方式在Java源代码编译成字节码的时候,将切面逻辑插入到目标类中
- 这需要特殊的编译器支持,例如AspectJ提供了这样的编译器
- 编译时织入会直接修改类文件,因此在运行时不需要任何额外的处理或框架支持
- 类加载时织入:
- 类加载时织入在类被加载到JVM时,通过特殊的类加载器将切面逻辑插入到目标类中
- 这允许在不修改源代码的情况下应用切面,同时也不需要特殊的编译过程
- 这种方法需要JVM支持特定的代理机制,如Java代理API
- 运行时织入:
- 运行时织入是最常见的织入方式,尤其是在Spring框架中
- 在这种方式中,切面逻辑是在程序运行的过程中动态添加的,通常通过动态代理实现
- 这种方法的优势在于不需要修改原有类文件,也不需要特殊的类加载器。它增加了灵活性,因为可以根据需要动态地应用或取消切面逻辑
- 静态织入:
- 静态织入是一种特殊情况,它通常指在编译时或构建时就确定好切面逻辑并将其织入到目标类中,但这通常并不是指标准的编译过程。
- 这种方式通常与工具或框架结合使用,如通过Maven或Gradle构建过程中应用切面
评论区