AOP介绍
概述
需要先了解AOP的概念(Aspect Oriented Programming),即面向切面编程。
核心思想是能够将函数进行“包装”,在不改变原有业务逻辑的前提下,批量化地对一组函数或方法增加统一的上下文操作。
例如日志操作,权限校验等,需要在函数开始时进行记录或校验,结束时记录或者回收,这类操作通常被称为“通知”,AOP的核心就是简化这些通知的装入方法。并且尽量地隔离这些必要的上下文以使得业务代码更加干练。
类似于让通知自己设定作用于谁,而不是在业务中反复调用这些通知。
实用概念
贴近于实际使用的概念
- Advice(通知) :需要批量在上下文中被执行的操作函数
- Pointcut(切点):通知作用于的多个函数的集合
- Aspect(切面):通知和切点的结合体。在Spring AOP,两者都会被整合成一个类,这个类就是切面
基本概念
更贴近于理论思想本身的基本概念
- Join Point(连接点):能够被通知应用在上下文的函数,在Spring AOP每个成员方法都可以作为连接点
- Weaving(织入):将通知添加到连接点的过程
- Introduction(引入/引介):是一种特殊的通知机制,主要作用是 给已有的类动态增加新的方法或接口实现。
Spring AOP的使用
定义切点
想要使用AOP,就得先定义出切点,切点(Pointcut)是通知作用的目标范围,用来匹配哪些方法会被通知增强。
常见切点表达式
execution(* com.example.service..*(..))
:匹配 service 包及子包下的所有方法execution(* com.example.service.UserService.add*(..))
:匹配方法名以 add 开头的方法@annotation(org.springframework.transaction.annotation.Transactional)
:匹配带有@Transactional
注解的方法
例如:
// 匹配 UserService 类里的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 匹配 com.example.service 包及其子包的所有方法
@Pointcut("execution(* com.example.service..*(..))")
public void servicePackageMethods() {}
// 匹配方法名以 add 开头的方法,例如 addUser()、addRole()
@Pointcut("execution(* com.example.service.UserService.add*(..))")
public void addMethods() {}
// 匹配方法名以 set 开头的方法
@Pointcut("execution(* com.example.service.UserService.set*(..))")
public void setMethods() {}
// 匹配方法名以 add 开头的方法和 set 开头的方法
@Pointcut("addMethods() && setMethods()")
public void myMethods() {}
// 切点不需要函数体,仅作为一个标识符
切点也可以使用注解作为筛选条件,通过注解信息来筛选匹配的方法
// 匹配带有 @LogExecution 注解的方法
@Pointcut("@annotation(com.example.annotation.LogExecution)")
public void logExecutionMethods() {}
定义通知
在 Spring AOP 中,通知(Advice)是具体要执行的增强逻辑。常见的通知类型有:
- 前置通知(@Before):在目标方法执行前执行
- 后置通知(@AfterReturning):在目标方法正常返回后执行
- 异常通知(@AfterThrowing):在目标方法抛出异常时执行
- 最终通知(@After):无论目标方法是否正常返回,都会执行
- 环绕通知(@Around):可以在方法执行前后都插入逻辑,甚至决定是否执行目标方法
例如:
// 前置通知
@Before("userServiceMethods()")
public void beforeAllMethods(JoinPoint joinPoint) {
System.out.println("准备执行方法: " + joinPoint.getSignature().getName());
}
// 后置通知
@AfterReturning("userServiceMethods()")
public void afterAllMethods(JoinPoint joinPoint) {
System.out.println("方法执行完成: " + joinPoint.getSignature().getName());
}
JoinPoint常用 API
他会作为固定参数可以被使用。
方法/属性 | 说明 |
---|---|
joinPoint.getSignature() |
获取方法签名(方法名、返回类型、参数类型等) |
joinPoint.getSignature().getName() |
获取方法名 |
joinPoint.getArgs() |
获取方法参数数组 |
joinPoint.getTarget() |
获取目标对象(被代理对象) |
joinPoint.getThis() |
获取代理对象 |
joinPoint.toString() |
获取简要的连接点描述 |
环绕通知
环绕通知类似于python的修饰器,可以自定义的对切入点进行包含式重新设计。
并且环绕通知的参数不同,是**ProceedingJoinPoint
** 。
ProceedingJoinPoint
是 JoinPoint
的子类,提供额外的方法:
Object proceed()
:执行目标方法Object proceed(Object[] args)
:执行目标方法并可修改参数
因此非常类似于python将函数作为参数的修饰器,同时自己重新定义返回,可以选择直接调用切入点进行返回。
@Around("execution(* com.example.service.UserService.add*(..))")
public Object aroundAddMethods(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[环绕通知] 方法开始: " + pjp.getSignature().getName());
// 获取参数
Object[] args = pjp.getArgs();
System.out.println("参数: " + Arrays.toString(args));
// 修改参数示例
if (args.length > 0 && args[0] instanceof String) {
args[0] = "修改后的值";
}
// 执行目标方法
Object result = pjp.proceed(args);
System.out.println("[环绕通知] 方法结束: " + pjp.getSignature().getName());
return result;
}
定义切面
将我们上面的两个方法结合起来放进类里面,也就是切面。其中的类用Aspect注解。
@Aspect
@Component
public class UserServiceAspect {
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
@Pointcut("execution(* com.example.service..*(..))")
public void servicePackageMethods() {}
@Pointcut("execution(* com.example.service.UserService.add*(..))")
public void addMethods() {}
@Before("userServiceMethods()")
public void beforeAllMethods(JoinPoint joinPoint) {
System.out.println("[前置通知] 准备执行方法: " + joinPoint.getSignature().getName());
}
@AfterReturning("userServiceMethods()")
public void afterAllMethods(JoinPoint joinPoint) {
System.out.println("[后置通知] 方法执行完成: " + joinPoint.getSignature().getName());
}
@Before("addMethods()")
public void beforeAddMethods(JoinPoint joinPoint) {
System.out.println("[前置通知] add 方法开始: " + joinPoint.getSignature().getName());
}
}
切面顺序
我们已经完成关于Spring AOP的基本使用了。
通常也会遇到一些常见的问题,例如有多个通知都作用于一个切入点该怎么办:
- 两个通知位于一个切面内,通常根据定义顺序作为执行顺序。
- 两个通知位于不同切面内:切面可以使用
@Order
注解:
@Aspect
@Component
@Order(1) // 优先级高,先执行
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void beforeLogging(JoinPoint joinPoint) {
System.out.println("[LoggingAspect] 方法开始: " + joinPoint.getSignature().getName());
}
}
@Aspect
@Component
@Order(2) // 优先级低,后执行
public class SecurityAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void beforeSecurity(JoinPoint joinPoint) {
System.out.println("[SecurityAspect] 权限检查: " + joinPoint.getSignature().getName());
}
}
引入:为已有类添加接口
假设我们有一个原始类 UserService
:
public class UserService {
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
}
我们希望动态给 UserService
添加一个接口 Auditable
:
public interface Auditable {
void audit();
}
1) 定义引入切面
@Aspect
@Component
public class AuditIntroductionAspect {
// 将 UserService 动态实现 Auditable 接口,默认使用 AuditImpl 作为实现
@DeclareParents(value = "com.example.service.UserService+", defaultImpl = AuditImpl.class)
public Auditable mixin;
}
2) 提供接口实现类
@Component
public class AuditImpl implements Auditable {
@Override
public void audit() {
System.out.println("执行审计操作...");
}
}
3) 使用示例
@Autowired
private UserService userService;
public void test() {
userService.addUser("Alice");
// 强制转换为 Auditable,执行新方法
((Auditable) userService).audit();
}