目录
一、什么是 AOP
AOP(Aspect-Oriented Programming)即 “面向切面编程”,它是对 OOP(面向对象编程)的补充,用来处理一些与业务无关但在多个模块中重复出现的横切逻辑,比如:
日志记录
权限控制
事务管理
参数校验
接口统计/埋点
二、Spring AOP 的基本术语
名称 | 含义 |
---|---|
Join Point | 连接点:程序执行的某个点,比如方法的调用 |
Pointcut | 切入点:定义哪些 Join Point 会被切入 |
Advice | 通知:在切入点上执行的代码(如前置、后置、异常通知等) |
Aspect | 切面:切入点 + 通知的组合,表示一个完整的切面逻辑 |
Weaving | 织入:将切面代码织入到目标对象的过程,Spring 使用动态代理实现织入 |
Target Object | 被代理的目标对象 |
三、Spring Boot 中使用 AOP 的步骤
1. 引入依赖(若使用 starter 则默认包含)
<!-- spring-boot-starter-aop 默认集成AspectJ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 编写切面类(@Aspect)
@Aspect
@Component
public class LogAspect {
// 切入点:拦截所有controller层的方法
@Pointcut("execution(* com.example.controller..*.*(..))")
public void controllerMethods() {}
// 前置通知
@Before("controllerMethods()")
public void before(JoinPoint joinPoint) {
System.out.println("请求方法: " + joinPoint.getSignature().getName());
}
// 后置通知
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("返回结果: " + result);
}
// 异常通知
@AfterThrowing(pointcut = "controllerMethods()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("方法异常: " + ex.getMessage());
}
// 环绕通知(可以控制方法是否执行)
@Around("controllerMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("方法执行后");
return result;
}
}
3. 常见切点表达式示例
表达式示例 | 含义 |
---|---|
execution(* com.example.service..(…)) | 拦截 service 包下所有方法 |
@annotation(com.example.annotation.CheckToken) | 拦截带有特定注解的方法 |
within(com.example.controller…*) | 拦截 controller 包及子包 |
this(org.springframework.stereotype.Service) | 拦截被代理的实现类对象 |
四、@Order 注解
1. @Order 是什么
它来自 org.springframework.core.annotation.Order
当有多个切面时,用于指定切面(@Aspect)的 执行优先级顺序
值越小,优先级越高,越早执行
2. 多个切面的执行顺序举例
假设你有两个切面:
@Aspect
@Component
@Order(1) // 优先级高,先执行
public class AopLogAspect {
@Before("execution(* com.example..*.*(..))")
public void before() {
System.out.println("【LogAspect】前置逻辑");
}
}
@Aspect
@Component
@Order(2) // 优先级低,后执行
public class AopAuthAspect {
@Before("execution(* com.example..*.*(..))")
public void before() {
System.out.println("【AuthAspect】前置逻辑");
}
}
执行顺序:
【LogAspect】前置逻辑
【AuthAspect】前置逻辑
目标方法执行
3. @Order 的默认行为
不加 @Order 的切面,默认优先级最低(即 Integer.MAX_VALUE)
因此,建议显式加 @Order(n) 避免不确定性
4. 环绕通知(@Around)的执行流程顺序说明
对于多个 @Around,顺序如下:
(1)@Order 值小的 @Around 先进入(进入方法前)
(2)然后是值大的(栈式结构)
(3)再反过来执行 proceed() 后面的逻辑
例如:
@Aspect
@Order(1)
class AspectA {
@Around("execution(...)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("A - before");
Object result = pjp.proceed();
System.out.println("A - after");
return result;
}
}
@Aspect
@Order(2)
class AspectB {
@Around("execution(...)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("B - before");
Object result = pjp.proceed();
System.out.println("B - after");
return result;
}
}
输出结果:
A - before
B - before
目标方法执行
B - after
A - after
五、常见应用场景
统一日志打印
权限控制(结合注解)
接口限流
埋点数据采集
参数校验或脱敏处理
接口性能监控
六、Spring AOP 使用建议
尽量使用 @annotation 结合自定义注解来增强可读性与可控性
避免将业务逻辑放在切面中,应保持职责单一
切点表达式建议明确粒度,避免匹配过宽导致误拦截
七、示例
我们实现一个功能:
给某些接口加上 @CheckToken
注解,自动校验请求中是否携带合法的 token,若不合法就拦截请求。
1. 定义自定义注解 @CheckToken
@Target({ElementType.METHOD, ElementType.TYPE}) // 可用于方法或类
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckToken {
String value() default ""; // 可选的参数,例如角色或权限
String description() default ""; // 描述信息
}
2. Controller 中使用注解
@RestController
@RequestMapping("/api")
public class TestController {
@CheckToken(value = "admin", description = "管理员权限验证")
@GetMapping("/secure")
public String secureEndpoint() {
return "访问成功!你通过了Token校验";
}
@GetMapping("/open")
public String openEndpoint() {
return "这个接口不需要Token";
}
}
3. AOP 切面类拦截注解
@Aspect
@Component
public class CheckTokenAspect {
// 拦截所有带 @CheckToken 注解的方法或类
@Pointcut("@annotation(com.example.annotation.CheckToken) || @within(com.example.annotation.CheckToken)")
public void checkTokenPointcut() {}
// 环绕通知,决定是否执行目标方法
@Around("checkTokenPointcut()")
public Object doCheckToken(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 从请求头获取 token
String token = request.getHeader("token");
// 获取注解信息(方法级 > 类级)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
CheckToken checkToken = method.getAnnotation(CheckToken.class);
if (checkToken == null) {
checkToken = joinPoint.getTarget().getClass().getAnnotation(CheckToken.class);
}
// 打印注解元信息
if (checkToken != null) {
System.out.println("注解 value: " + checkToken.value());
System.out.println("注解 description: " + checkToken.description());
}
// 校验逻辑:此处示例简单 token 值固定
if (!"valid_token".equals(token)) {
throw new RuntimeException("Token 校验失败,请登录后再试!");
}
// 通过校验,继续执行原方法
return joinPoint.proceed();
}
}
4. 测试验证
请求示例:
✅ GET /api/secure,header 中加 token: valid_token → 响应正常
❌ GET /api/secure,未带 token 或 token 错误 → 异常提示“Token 校验失败”
✅ GET /api/open → 无需 token,正常响应