在企业级Java开发中,Spring框架 是核心基础设施,而AOP(面向切面编程) 是Spring的两大核心特性之一(另一个是IOC/DI)。
OOP(面向对象编程)通过继承、封装、多态 处理纵向的业务逻辑,但对于横向通用逻辑(如日志记录、权限校验、事务管理、性能监控、全局异常处理),OOP会导致代码冗余、耦合度高。
基础概念
Spring AOP基于AspectJ切面规范实现,先掌握核心术语,是理解AOP的关键:
| 术语 | 英文 | 通俗解释 | 企业级类比 |
|---|---|---|---|
| 连接点 | JoinPoint | 程序中可以被拦截 的执行点(Spring中仅支持方法) | 所有员工的打卡机(所有可打卡的位置) |
| 切点 | Pointcut | 真正需要拦截的连接点(筛选后的方法) | 公司指定必须打卡的部门/岗位 |
| 通知 | Advice | 拦截后执行的增强逻辑(前置/后置/异常等) | 打卡时的语音提示、考勤记录逻辑 |
| 切面 | Aspect | 【切点 + 通知】的结合体(Java类) | 考勤管理模块(定义了谁打卡、打卡做什么) |
| 目标对象 | Target | 被AOP增强的原始业务对象 | 打卡的员工 |
| 代理对象 | Proxy | Spring运行时生成的、包含增强逻辑的对象 | 员工+打卡逻辑的结合体 |
| 织入 | Weaving | 将切面逻辑嵌入目标方法的过程 | 把考勤逻辑绑定到员工打卡动作 |
核心总结
- Spring AOP 仅支持方法级别的拦截(不支持字段、构造器拦截);
- 底层基于动态代理实现:JDK动态代理(接口)、CGLIB代理(类);
- 运行时织入(区别于AspectJ的编译期织入),轻量无侵入。
开发环境准备
Spring Boot 对AOP提供自动配置,无需手动编写配置类,仅需引入依赖即可。
1. Maven依赖
xml
<!-- Spring AOP 核心依赖(Spring Boot自动装配) -->
<!-- 无需写版本,Spring会自动调用与SpringBoot对应的版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 核心注解
Spring AOP 注解驱动(企业主流方式)核心注解:
@Aspect:标记当前类为切面类;@Pointcut:定义切点表达式(筛选拦截的方法);- 5种通知注解:
@Before/@After/@AfterReturning/@AfterThrowing/@Around; @Component:将切面类交给Spring容器管理。
切点表达式
切点表达式用于精准筛选需要拦截的方法 。Spring支持多种切点表达式,execution 是企业最常用。
1. execution 表达式(万能)
语法:
execution(修饰符 返回值类型 包名.类名.方法名(参数类型) 异常类型)
- 修饰符:可省略(public/private/protected,Spring仅支持public);
*:通配符,匹配任意字符;..:匹配任意参数、任意包路径;
常用样例
java
// 1. 拦截 com.example.service 包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
// 2. 拦截 UserServiceImpl 类的 addUser 方法(参数为String+Integer)
@Pointcut("execution(* com.example.service.UserServiceImpl.addUser(String, Integer))")
// 3. 拦截 所有返回值为String的方法
@Pointcut("execution(String *(..))")
// 4. 拦截 所有public方法
@Pointcut("execution(public * *(..))")
2. 其他常用切点表达式
| 表达式 | 作用 | 样例 |
|---|---|---|
within |
拦截指定类/包下的所有方法 | within(com.example.service.*) |
@annotation |
拦截添加了自定义注解的方法 | @annotation(com.example.anno.Log) |
args |
拦截参数类型匹配的方法 | args(Long, String) |
bean |
拦截指定名称的Spring Bean | bean(userServiceImpl) |
五种通知类型
通知是AOP的增强逻辑,Spring提供5种标准通知,覆盖所有业务增强场景。
前置准备:业务接口+实现类
先编写一个基础业务类,用于AOP增强测试:
java
// 1. 业务接口
public interface UserService {
String addUser(String username, Integer age);
void deleteUser(Long id);
}
// 2. 业务实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public String addUser(String username, Integer age) {
System.out.println("【业务逻辑】执行添加用户:" + username);
// 模拟异常:if(age < 0) throw new RuntimeException("年龄不能为负数");
return "用户添加成功";
}
@Override
public void deleteUser(Long id) {
System.out.println("【业务逻辑】执行删除用户:" + id);
}
}
类型1:前置通知 @Before
作用 :目标方法执行之前 执行增强逻辑。
场景:参数校验、权限预检查、日志打印。
java
@Aspect
@Component
public class LogAspect {
/**
* 切点:拦截 UserServiceImpl 下的所有方法
* execution(返回值 包名.类名.方法名(参数))
*/
@Pointcut("execution(* com.example.service.UserServiceImpl.*(..))")
public void pointcut() {}
// 前置通知
@Before("pointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("===== 前置通知 =====");
System.out.println("目标方法名:" + joinPoint.getSignature().getName());
System.out.println("目标方法参数:" + Arrays.toString(joinPoint.getArgs()));
}
}
类型2:后置通知 @After
作用 :目标方法执行之后 执行(无论方法正常返回/抛出异常)。
场景:资源释放、日志收尾。
java
// 后置通知
@After("pointcut()")
public void afterAdvice() {
System.out.println("===== 后置通知 =====");
}
类型3:返回通知 @AfterReturning
作用 :目标方法正常返回结果后 执行,可获取返回值。
场景:记录返回结果、数据二次封装。
java
// 返回通知:returning = "result" 绑定返回值参数名
@AfterReturning(pointcut = "pointcut()", returning = "result")
public void afterReturningAdvice(Object result) {
System.out.println("===== 返回通知 =====");
System.out.println("方法返回值:" + result);
}
类型4:异常通知 @AfterThrowing
作用 :目标方法抛出异常后 执行,可获取异常信息。
场景:全局异常捕获、异常告警、日志记录。
java
// 异常通知:throwing = "ex" 绑定异常参数名
@AfterThrowing(pointcut = "pointcut()", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {
System.out.println("===== 异常通知 =====");
System.out.println("异常信息:" + ex.getMessage());
}
类型5:环绕通知 @Around(最实用)
作用 :包裹目标方法的执行,可控制目标方法是否执行、修改参数/返回值、捕获异常 。
场景:性能监控、事务管理、限流、缓存。
注意:环绕通知必须接收
ProceedingJoinPoint参数(JoinPoint的子类),必须调用proceed()执行目标方法。
java
// 环绕通知
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("===== 环绕通知-方法执行前 =====");
// 执行目标方法(必须调用,否则业务方法不执行)
Object result = pjp.proceed();
System.out.println("===== 环绕通知-方法执行后 =====");
System.out.println("方法执行耗时:" + (System.currentTimeMillis() - start) + "ms");
// 可修改返回值
// return "修改后的返回值";
return result;
}
五种通知执行顺序
- 正常执行:
环绕通知前 -> 前置通知 -> 业务方法 -> 返回通知 -> 后置通知 -> 环绕通知后 - 异常执行:
环绕通知前 -> 前置通知 -> 业务方法抛异常 -> 异常通知 -> 后置通知
高级特性
1. 获取连接点信息
所有通知都可通过 JoinPoint 获取目标方法的详细信息:
java
// 获取方法签名
Signature signature = joinPoint.getSignature();
// 方法名
String methodName = signature.getName();
// 类名
String className = joinPoint.getTarget().getClass().getName();
// 方法参数
Object[] args = joinPoint.getArgs();
2. 自定义注解+AOP(主流)
通过自定义注解标记需要增强的方法,灵活性极高。
步骤1:自定义注解
java
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface OperationLog {
// 操作描述
String value() default "";
// 操作类型
String type() default "查询";
}
步骤2:编写切面
java
@Aspect
@Component
public class OperationLogAspect {
// 拦截所有添加了 @OperationLog 注解的方法
@Pointcut("@annotation(com.example.anno.OperationLog)")
public void logPointcut() {}
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
// 获取注解信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
OperationLog log = signature.getMethod().getAnnotation(OperationLog.class);
System.out.println("【操作日志】操作:" + log.value() + ",类型:" + log.type());
Object result = pjp.proceed();
System.out.println("【操作日志】操作执行完成");
return result;
}
}
步骤3:使用注解
java
@Override
@OperationLog(value = "添加用户", type = "新增")
public String addUser(String username, Integer age) {
System.out.println("【业务逻辑】执行添加用户:" + username);
return "用户添加成功";
}
3. 多切面排序
多个切面拦截同一个方法时,用 @Order(数字) 指定执行顺序:
- 数字越小,优先级越高;
- 环绕通知:优先级高的先执行前逻辑,后执行后逻辑。
java
@Aspect
@Component
@Order(1) // 优先级最高
public class LogAspect {}
@Aspect
@Component
@Order(2) // 优先级次之
public class PermissionAspect {}
4. 拦截请求参数/修改返回值
环绕通知可修改入参 和返回值,适配数据脱敏、参数格式化等场景:
java
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 修改参数
Object[] args = pjp.getArgs();
if(args.length > 0 && args[0] instanceof String){
args[0] = "修改后的用户名";
}
// 执行方法
Object result = pjp.proceed(args);
// 修改返回值
return "【增强】" + result;
}
企业级实战案例
案例1:统一接口日志切面
记录接口请求参数、返回值、耗时、请求路径,适配Web开发。
java
@Aspect
@Component
public class WebLogAspect {
@Pointcut("execution(* com.example.controller.*.*(..))")
public void webLog() {}
@Around("webLog()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求信息
System.out.println("===== 请求开始 =====");
System.out.println("请求URL:" + request.getRequestURL());
System.out.println("请求方法:" + request.getMethod());
System.out.println("请求参数:" + Arrays.toString(pjp.getArgs()));
long start = System.currentTimeMillis();
Object result = pjp.proceed();
// 打印响应信息
System.out.println("返回值:" + result);
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("===== 请求结束 =====");
return result;
}
}
案例2:接口权限校验切面
通过AOP实现接口权限校验,无需在Controller编写冗余代码。
java
@Aspect
@Component
public class PermissionAspect {
@Pointcut("@annotation(com.example.anno.RequirePermission)")
public void permissionPointcut() {}
@Before("permissionPointcut()")
public void checkPermission(JoinPoint joinPoint) {
// 模拟:从Token获取用户权限
String userPermission = "admin";
RequirePermission annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RequirePermission.class);
String requiredPermission = annotation.value();
if(!requiredPermission.equals(userPermission)){
throw new RuntimeException("权限不足,无法访问");
}
}
}
案例3:方法性能监控
统计慢查询、慢方法,用于系统优化。
java
@Around("pointcut()")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
// 耗时超过500ms打印告警
if(cost > 500){
System.out.println("【慢方法告警】" + pjp.getSignature().getName() + ",耗时:" + cost + "ms");
}
return result;
}
Spring AOP 底层原理
Spring AOP 基于动态代理实现,运行时生成代理对象,分为两种:
- JDK动态代理 :默认方式,要求目标类实现接口,通过实现接口生成代理;
- CGLIB代理 :目标类无接口 时使用,通过继承目标类生成代理。
Spring Boot 2.x 及以上:默认使用CGLIB代理(无需接口)。
核心区别
| 代理方式 | 要求 | 性能 | 适用场景 |
|---|---|---|---|
| JDK动态代理 | 目标类必须实现接口 | 高 | 有接口的业务类 |
| CGLIB代理 | 目标类不能被final修饰 | 较高 | 无接口的业务类 |
常见避坑指南
坑1:同类内部方法调用,AOP不生效
java
@Service
public class UserServiceImpl {
public void method1(){
// 内部调用method2,AOP不生效(绕过了代理对象)
this.method2();
}
public void method2(){}
}
解决方案:
- 从Spring上下文获取当前Bean,调用方法;
- 开启AopContext.currentProxy()获取代理对象。
坑2:私有方法/ final方法不被拦截
Spring AOP 仅支持public方法拦截,private/final方法无法被代理增强。
坑3:环绕通知忘记调用 proceed()
目标方法不会执行,业务逻辑直接丢失。
坑4:切点表达式写错
导致AOP不生效,优先检查execution表达式的包名、类名、方法名。
总结
- 核心定位:AOP是横向解耦工具,用于抽取通用逻辑,无侵入增强业务方法;
- 核心组成:切面(@Aspect)+ 切点(@Pointcut)+ 5种通知;
- 最强通知:环绕通知@Around,可控制方法执行全流程;
- 企业场景:日志、权限、事务、性能监控、异常处理;
- 底层原理:动态代理(JDK/CGLIB),运行时织入。