Spring 中的 AOP(Aspect-Oriented Programming,面向切面编程)是一种通过分离关注点来增强代码模块化的编程范式。在 Spring 中,AOP 允许开发者定义通用的行为(如日志记录、安全性验证、事务管理等),然后以非侵入的方式将这些行为应用到应用程序的特定部分(例如方法或类)上,从而避免代码重复并提高代码的可维护性和可读性。
以下是 Spring AOP 的核心概念和工作原理:
核心概念
-
切面(Aspect)
- 切面是功能模块化的横切关注点,例如日志记录或事务管理。
- 由一个类实现,里面包含横切逻辑代码。
- 通常通过
@Aspect
注解标识。
-
连接点(Join Point)
- 连接点是程序执行的一个特定点,比如方法调用、对象实例化等。
- 在 Spring AOP 中,连接点主要指方法的执行。
-
通知(Advice)
- 通知是切面中定义的具体动作,即在连接点上执行的代码。
- Spring 支持以下几种通知类型:
- 前置通知(Before Advice):在目标方法执行之前运行。
- 后置通知(After Advice):在目标方法执行之后运行。
- 返回通知(After Returning Advice):在目标方法正常返回后运行。
- 异常通知(After Throwing Advice):在目标方法抛出异常后运行。
- 环绕通知(Around Advice):包裹目标方法的执行,在方法执行前后均可运行。
-
切入点(Pointcut)
- 切入点是定义某些连接点的表达式,用于指定通知应用的位置。
- 通常使用 AspectJ 表达式(如
execution(* com.example.service.*.*(..))
)定义。
-
目标对象(Target Object)
- 被 AOP 增强的对象。Spring AOP 使用动态代理来创建目标对象的代理对象。
-
织入(Weaving)
- 织入是将切面应用到目标对象并创建代理对象的过程。
- Spring AOP 在运行时通过动态代理实现织入。
Spring AOP 的实现方式
Spring AOP 的底层实现主要基于以下两种技术:
-
JDK 动态代理
- 如果目标类实现了接口,Spring AOP 会使用 JDK 动态代理来创建代理对象。
-
CGLIB 动态代理
- 如果目标类没有实现接口,Spring AOP 会使用 CGLIB(Code Generation Library)来生成子类代理。
AOP 的使用步骤
-
引入依赖
在 Spring 项目中启用 AOP 通常需要引入spring-aop
模块,或在 Spring Boot 项目中通过添加spring-boot-starter-aop
依赖。 -
配置 AOP
- 开启 AOP 支持:通过
@EnableAspectJAutoProxy
注解启用 AOP。 - 定义切面类:使用
@Aspect
注解声明一个类为切面。
- 开启 AOP 支持:通过
-
定义通知
- 在切面类中定义通知方法,并通过注解(如
@Before
、@After
等)绑定到具体的切入点。
- 在切面类中定义通知方法,并通过注解(如
在pom.xml⽂件中添加配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
示例代码
以下是一个简单的 Spring AOP 示例,记录方法执行的日志:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 定义一个切入点,匹配指定包中所有方法
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod() {
System.out.println("方法执行前记录日志...");
}
}
配置类:
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
目标类:
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void performTask() {
System.out.println("执行业务逻辑...");
}
}
输出结果:
方法执行前记录日志...
执行业务逻辑...
通知(Advice)
定义
通知是切面中的具体动作,指在某个连接点上执行的代码逻辑。
Spring 提供了多种类型的通知,分别对应在连接点的不同时间点执行的操作。
通知类型
前置通知(@Before)
在目标方法执行前运行。适用于验证输入参数、初始化资源等操作。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("方法执行前的通知");
}
后置通知(@After)
在目标方法执行后运行,无论方法是否抛出异常。
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice() {
System.out.println("方法执行后的通知");
}
返回通知(@AfterReturning)
在目标方法成功返回后运行,适用于获取返回值并进行后续处理。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(Object result) {
System.out.println("方法返回值: " + result);
}
异常通知(@AfterThrowing)
在目标方法抛出异常时运行,用于记录错误日志或进行回滚操作。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
System.out.println("方法抛出异常: " + exception.getMessage());
}
环绕通知(@Around)
包裹目标方法的执行,能在方法执行前后都插入逻辑代码。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法执行前的环绕通知");
Object result = joinPoint.proceed(); // 调用目标方法
System.out.println("方法执行后的环绕通知");
return result;
}
通知的核心点
- 通知必须绑定到一个切入点。
- 通知中可以获取目标方法的上下文信息,比如方法名、参数、返回值等。
切入点(Pointcut)
定义
切入点是一个表达式,定义在哪些连接点上应用通知。
它的作用是筛选出一组匹配的连接点,从而指定通知的作用范围。
表达式
切入点通常使用 AspectJ 表达式定义,主要包含以下部分:
方法签名匹配:
使用 execution
表达式,匹配目标方法的访问修饰符、返回类型、类路径、方法名、参数等。
示例:
execution(<修饰符>? <返回类型> <类路径>.<方法名>(<参数列表>))
例如:
// 匹配 com.example.service 包下所有类的所有方法
execution(* com.example.service.*.*(..))
// 匹配返回类型为 String 的所有方法
execution(String *.*(..))
类注解匹配:
使用 @within
或 @target
表达式,匹配带有特定注解的类。
示例:
@within(org.springframework.stereotype.Service)
方法注解匹配:
使用 @annotation
表达式,匹配带有特定注解的方法。
示例:
@annotation(org.springframework.transaction.annotation.Transactional)
参数匹配:
使用 args
表达式,匹配方法的参数类型或具体值。
示例:
args(java.lang.String)
切入点定义示例
@Aspect
@Component
public class LoggingAspect {
// 定义切入点表达式
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// 使用切入点
@Before("serviceMethods()")
public void logBefore() {
System.out.println("在服务方法之前执行日志记录");
}
}
总结
- 连接点是程序执行中的某个具体点,Spring AOP 只支持方法级别的连接点。
- 通知是在连接点上执行的具体代码逻辑,可以选择在方法前、后或异常时执行。
- 切入点通过表达式定义了哪些连接点会被增强,是通知的作用范围的核心筛选条件。
切面优先级
在 Spring AOP 中,多个切面可以作用于同一个目标方法。如果这些切面包含不同的通知(如 @Before
、@After
等),Spring 提供了优先级机制来控制切面执行的顺序。这个优先级可以通过 @Order
注解来定义。
@Order
注解
@Order
是 Spring 提供的注解,用于指定切面的优先级。
- 数值越小,优先级越高,切面会更早执行。
- 数值越大,优先级越低,切面会更晚执行。
适用范围
@Order
注解应用在切面类(标注了 @Aspect
的类)上。
优先级的执行规则
-
前置通知(
@Before
)- 按照优先级从 低到高(
@Order
值从小到大)依次执行。
- 按照优先级从 低到高(
-
后置通知(
@After
和@AfterReturning
)- 按照优先级从 高到低(
@Order
值从大到小)依次执行。
- 按照优先级从 高到低(
-
环绕通知(
@Around
)- 环绕通知包裹目标方法,其执行顺序遵循前置通知的优先级规则。
示例代码
定义两个切面
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1) // 优先级最高
public class FirstAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("FirstAspect: 前置通知");
}
}
@Aspect
@Component
@Order(2) // 优先级较低
public class SecondAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("SecondAspect: 前置通知");
}
}
目标方法
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void performTask() {
System.out.println("执行业务逻辑...");
}
}
执行结果
当调用 UserService.performTask()
时,输出顺序为:
FirstAspect: 前置通知
SecondAspect: 前置通知
执行业务逻辑...
如果是后置通知,顺序会反过来:
SecondAspect: 后置通知
FirstAspect: 后置通知
注意事项
-
默认优先级
- 如果未标注
@Order
,默认优先级最低(即Integer.MAX_VALUE
)。 - 如果多个切面都未标注
@Order
,它们的执行顺序是不确定的。
- 如果未标注
-
@Order
与切入点无关@Order
仅影响同一个连接点上的切面的优先级,与切入点匹配逻辑无直接关系。
-
环绕通知的特殊性
- 环绕通知会包裹目标方法及其他通知,因此优先级需要特别注意,否则可能导致执行顺序不符合预期。
总结
@Order
控制切面之间的优先级,数值越小优先级越高。- 不同类型的通知(如前置和后置)执行顺序遵循不同规则:
- 前置通知按优先级升序执行。
- 后置通知按优先级降序执行。
- 合理使用
@Order
可以确保切面按照预期顺序执行。