Spring AOP原理(二):@Aspect注解深入,5种通知类型完全掌握!

系列文章第2篇 | 共3篇
难度:⭐⭐⭐ | 适合人群:想深入理解Spring AOP的开发者


📝 上期回顾

上一篇我们学习了:

  • ✅ 代理模式的概念
  • ✅ JDK动态代理(基于接口)
  • ✅ CGLIB动态代理(基于继承)
  • ✅ 两种代理方式的对比
  • ✅ Spring的选择策略

上期思考题解答:

Q1: 为什么final方法无法被代理?
A: CGLIB通过继承实现代理,子类无法重写父类的final方法,所以无法代理。JDK代理也无法代理接口中声明为default final的方法。

Q2: 既有接口又有非接口方法会怎样?
A: 如果用JDK代理,只能代理接口中的方法,其他方法无法代理。如果用CGLIB,所有public非final方法都能代理。

Q3: this调用为什么AOP不生效?
A: 今天详细解答!


💥 开场:一次权限校验的"惨案"

时间: 周二下午
地点: 办公室
事件: 线上安全漏洞

安全部门: "你们的删除订单接口没做权限校验,普通用户可以删除任何订单!"

我: "不可能啊,我明明加了权限注解..." 😰


代码是这样的:

@Service
public class OrderService {
    
    @Autowired
    private OrderDao orderDao;
    
    /**
     * 删除订单(需要管理员权限)
     */
    @RequireRole("ADMIN")  // 自定义权限注解
    public void deleteOrder(Long orderId) {
        orderDao.delete(orderId);
        System.out.println("订单删除成功");
    }
    
    /**
     * 批量删除
     */
    public void batchDelete(List<Long> orderIds) {
        for (Long id : orderIds) {
            this.deleteOrder(id);  // 调用deleteOrder
        }
    }
}

哈吉米: "你通过batchDelete删除,权限校验不会生效。"

我: "为什么?我调用了deleteOrder啊,它有@RequireRole注解。" 🤔

南北绿豆: "因为你用this.deleteOrder()调用的,this是原始对象,不是代理对象!"

阿西噶阿西画了个图:

客户端调用
    ↓
orderService(这是代理对象)
    ↓
代理逻辑执行(权限校验) ✅
    ↓
batchDelete()方法
    ↓
this.deleteOrder()  ← this是原始对象!
    ↓
直接调用原始方法,没经过代理 ❌
    ↓
权限校验不生效!

我: "原来如此!那怎么解决?" 💡

哈吉米: "要么注入自己,要么用AopContext.currentProxy(),但这些都是治标不治本。要真正理解AOP,得从@Aspect注解开始学..."


🎯 第一问:Spring AOP核心概念

AOP是什么?

AOP = Aspect Oriented Programming(面向切面编程)

南北绿豆: "我用一个场景给你解释。"

场景: 你要给所有Service方法加日志

传统方式(OOP):

public class UserService {
    public void createUser() {
        System.out.println(">>> 日志开始");  // 重复代码
        // 业务逻辑
        System.out.println("<<< 日志结束");  // 重复代码
    }
    
    public void deleteUser() {
        System.out.println(">>> 日志开始");  // 重复代码
        // 业务逻辑
        System.out.println("<<< 日志结束");  // 重复代码
    }
}

public class OrderService {
    public void createOrder() {
        System.out.println(">>> 日志开始");  // 重复代码
        // 业务逻辑
        System.out.println("<<< 日志结束");  // 重复代码
    }
}

// 100个Service,每个方法都要写日志代码...

问题: 代码重复、难维护!


AOP方式:

// 业务代码:只关注业务逻辑
public class UserService {
    public void createUser() {
        // 只写业务逻辑
    }
}

// 日志代码:统一在切面中处理
@Aspect
@Component
public class LogAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println(">>> 日志开始");
        Object result = pjp.proceed();
        System.out.println("<<< 日志结束");
        return result;
    }
}

优势:

  • ✅ 业务代码干净
  • ✅ 日志逻辑集中
  • ✅ 易于维护

AOP核心概念

阿西噶阿西: "AOP有6个核心概念。"

1. 切面(Aspect)

@Aspect  // ← 这就是一个切面
@Component
public class LogAspect {
    // 包含切点和通知
}

定义: 切面 = 切点 + 通知

类比: 切面就是一个"功能模块"(如日志模块、权限模块)


2. 连接点(Join Point)

定义: 程序执行过程中可以插入切面的点

可以是:

  • 方法调用
  • 方法执行
  • 字段访问
  • 异常抛出

Spring AOP只支持方法执行级别的连接点!

public void createUser() {  // ← 这个方法的执行就是一个连接点
    // ...
}

3. 切点(Pointcut)

定义: 匹配连接点的表达式

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

作用: 定义"哪些方法"需要被增强

类比: 切点就是"过滤器",选出需要增强的方法


4. 通知(Advice)

定义: 在切点处执行的代码

5种类型:

  • @Before - 前置通知
  • @After - 后置通知
  • @AfterReturning - 返回通知
  • @AfterThrowing - 异常通知
  • @Around - 环绕通知

5. 目标对象(Target)

@Service
public class UserService {  // ← 这就是目标对象
    public void createUser() {
        // 被代理的对象
    }
}

6. 织入(Weaving)

定义: 将切面应用到目标对象创建代理的过程

时机:

  • 编译期(AspectJ)
  • 类加载期(AspectJ)
  • 运行期(Spring AOP)← Spring用这个

概念关系图

        切面(Aspect)
        ├─ 切点(Pointcut)
        │   └─ 匹配哪些方法
        └─ 通知(Advice)
            └─ 做什么增强
                ↓
        应用到目标对象
                ↓
        通过织入创建代理
                ↓
        在连接点执行增强

🎨 第二问:5种通知类型详解

准备工作

创建测试Service:

@Service
public class UserService {
    
    public String createUser(String name) {
        System.out.println("   [业务方法] 创建用户:" + name);
        return "User-" + name;
    }
    
    public void deleteUser(Long id) {
        System.out.println("   [业务方法] 删除用户:" + id);
        if (id == 0) {
            throw new IllegalArgumentException("ID不能为0");
        }
    }
}

类型1:@Before(前置通知)

定义: 在目标方法执行之前执行

@Aspect
@Component
public class BeforeAspect {
    
    @Before("execution(* com.example.service.UserService.*(..))")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        System.out.println(">>> 【Before】方法:" + methodName);
        System.out.println("    参数:" + Arrays.toString(args));
    }
}

测试:

userService.createUser("张三");

输出:

>>> 【Before】方法:createUser
    参数:[张三]
   [业务方法] 创建用户:张三

特点:

  • 在方法执行前执行
  • 无法阻止方法执行
  • 无法修改返回值

类型2:@After(后置通知)

定义: 在目标方法执行之后执行(无论成功还是异常)

@Aspect
@Component
public class AfterAspect {
    
    @After("execution(* com.example.service.UserService.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("<<< 【After】方法:" + joinPoint.getSignature().getName());
        System.out.println("    无论成功还是异常都会执行");
    }
}

测试:

// 正常执行
userService.createUser("张三");

// 抛异常
try {
    userService.deleteUser(0L);
} catch (Exception e) {
    System.out.println("捕获异常:" + e.getMessage());
}

输出:

   [业务方法] 创建用户:张三
<<< 【After】方法:createUser
    无论成功还是异常都会执行

   [业务方法] 删除用户:0
<<< 【After】方法:deleteUser
    无论成功还是异常都会执行
捕获异常:ID不能为0

特点:

  • 类似于finally块
  • 无论成功失败都执行
  • 无法获取返回值

类型3:@AfterReturning(返回通知)

定义: 在目标方法正常返回后执行

@Aspect
@Component
public class AfterReturningAspect {
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        returning = "result"  // 绑定返回值
    )
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("<<< 【AfterReturning】方法:" + 
            joinPoint.getSignature().getName());
        System.out.println("    返回值:" + result);
    }
}

测试:

// 正常返回
String user = userService.createUser("张三");

// 抛异常
try {
    userService.deleteUser(0L);
} catch (Exception e) {
    System.out.println("捕获异常");
}

输出:

   [业务方法] 创建用户:张三
<<< 【AfterReturning】方法:createUser
    返回值:User-张三

   [业务方法] 删除用户:0
捕获异常
(AfterReturning不执行)

特点:

  • 只在正常返回时执行
  • 可以获取返回值
  • 无法修改返回值(只读)

类型4:@AfterThrowing(异常通知)

定义: 在目标方法抛异常后执行

@Aspect
@Component
public class AfterThrowingAspect {
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        throwing = "ex"  // 绑定异常
    )
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("<<< 【AfterThrowing】方法:" + 
            joinPoint.getSignature().getName());
        System.out.println("    异常:" + ex.getClass().getSimpleName());
        System.out.println("    消息:" + ex.getMessage());
    }
}

测试:

// 正常执行
userService.createUser("张三");

// 抛异常
try {
    userService.deleteUser(0L);
} catch (Exception e) {
    System.out.println("已捕获异常");
}

输出:

   [业务方法] 创建用户:张三
(AfterThrowing不执行)

   [业务方法] 删除用户:0
<<< 【AfterThrowing】方法:deleteUser
    异常:IllegalArgumentException
    消息:ID不能为0
已捕获异常

特点:

  • 只在抛异常时执行
  • 可以获取异常对象
  • 可以记录异常日志

类型5:@Around(环绕通知)

定义: 包围目标方法,最强大的通知类型

@Aspect
@Component
public class AroundAspect {
    
    @Around("execution(* com.example.service.UserService.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        
        System.out.println(">>> 【Around-Before】方法:" + methodName);
        System.out.println("    参数:" + Arrays.toString(args));
        
        Object result = null;
        try {
            // 执行目标方法
            result = pjp.proceed();
            
            System.out.println("<<< 【Around-AfterReturning】返回值:" + result);
        } catch (Exception e) {
            System.out.println("<<< 【Around-AfterThrowing】异常:" + e.getMessage());
            throw e;
        } finally {
            System.out.println("<<< 【Around-After】方法结束\n");
        }
        
        return result;
    }
}

测试:

userService.createUser("张三");

输出:

>>> 【Around-Before】方法:createUser
    参数:[张三]
   [业务方法] 创建用户:张三
<<< 【Around-AfterReturning】返回值:User-张三
<<< 【Around-After】方法结束

特点:

  • 最强大,可以完全控制方法执行
  • 可以决定是否执行目标方法
  • 可以修改参数
  • 可以修改返回值
  • 可以捕获异常

5种通知对比

通知类型执行时机获取返回值修改返回值捕获异常阻止执行
@Before方法前
@After方法后(finally)
@AfterReturning正常返回后
@AfterThrowing异常后
@Around环绕方法

通知执行顺序

哈吉米: "如果多个通知同时存在,执行顺序是怎样的?"

正常情况(无异常):

@Around - Before
    ↓
@Before
    ↓
【目标方法】
    ↓
@Around - After
    ↓
@After@AfterReturning

异常情况:

@Around - Before
    ↓
@Before
    ↓
【目标方法抛异常】
    ↓
@Around - AfterThrowing
    ↓
@After@AfterThrowing

完整验证:

@Aspect
@Component
public class OrderAspect {
    
    @Pointcut("execution(* com.example.service.UserService.createUser(..))")
    public void pointcut() {}
    
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("1. Around - Before");
        try {
            Object result = pjp.proceed();
            System.out.println("5. Around - AfterReturning");
            return result;
        } catch (Exception e) {
            System.out.println("5. Around - AfterThrowing");
            throw e;
        } finally {
            System.out.println("4. Around - After");
        }
    }
    
    @Before("pointcut()")
    public void before() {
        System.out.println("2. Before");
    }
    
    @After("pointcut()")
    public void after() {
        System.out.println("6. After");
    }
    
    @AfterReturning("pointcut()")
    public void afterReturning() {
        System.out.println("7. AfterReturning");
    }
}

测试:

userService.createUser("张三");

输出:

1. Around - Before
2. Before
   [业务方法] 创建用户:张三
5. Around - AfterReturning
4. Around - After
6. After
7. AfterReturning

顺序完全验证!


📐 第三问:切点表达式详解

execution表达式(最常用)

完整语法:

execution(modifiers? return-type declaring-type?method-name(param-types) throws?)

modifiers:修饰符(publicprivate等)
return-type:返回类型
declaring-type:类名
method-name:方法名
param-types:参数类型
throws:异常类型

南北绿豆: "我们看几个实际例子。"


示例1:匹配所有public方法

@Pointcut("execution(public * *(..))")

示例2:匹配指定包下的所有方法

@Pointcut("execution(* com.example.service.*.*(..))")
//           ↓      ↓                      ↓ ↓  ↓
//        返回类型  包名                  类 方法 参数

// 解释:
// * - 任意返回类型
// com.example.service.* - service包下的所有类
// .* - 所有方法
// (..) - 任意参数

示例3:匹配指定类的所有方法

@Pointcut("execution(* com.example.service.UserService.*(..))")
//                                       ↑
//                                   具体类名

示例4:匹配指定方法名

@Pointcut("execution(* com.example.service.*.create*(..))")
//                                            ↑
//                                   方法名以create开头

示例5:匹配指定参数

// 只有一个String参数
@Pointcut("execution(* com.example.service.*.*(String))")

// 第一个参数是String
@Pointcut("execution(* com.example.service.*.*(String, ..))")

// 无参方法
@Pointcut("execution(* com.example.service.*.*())")

示例6:匹配指定返回类型

// 返回String
@Pointcut("execution(String com.example.service.*.*(..))")

// 返回void
@Pointcut("execution(void com.example.service.*.*(..))")

@annotation表达式(推荐)

场景: 匹配带有特定注解的方法

自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}

切点:

@Pointcut("@annotation(com.example.annotation.Log)")
public void logMethods() {}

使用:

@Service
public class UserService {
    
    @Log("创建用户")  // ← 加了注解,会被拦截
    public void createUser(String name) {
        System.out.println("创建用户:" + name);
    }
    
    public void deleteUser(Long id) {  // 没加注解,不会被拦截
        System.out.println("删除用户:" + id);
    }
}

优点:

  • ✅ 精确控制哪些方法需要增强
  • ✅ 不依赖包名、方法名
  • ✅ 更灵活

within表达式

匹配指定类型内的方法:

// 匹配UserService类中的所有方法
@Pointcut("within(com.example.service.UserService)")

// 匹配service包及子包下的所有方法
@Pointcut("within(com.example.service..*)")

args表达式

匹配参数类型:

// 第一个参数是String
@Pointcut("args(String, ..)")

// 只有一个Long参数
@Pointcut("args(Long)")

组合表达式

// && 与
@Pointcut("execution(* com.example.service.*.*(..)) && args(String)")

// || 或
@Pointcut("execution(* create*(..)) || execution(* save*(..))")

// ! 非
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* delete*(..))")

💻 第四问:实战案例

案例1:自定义日志切面

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    /**
     * 切点:所有Controller方法
     */
    @Pointcut("execution(* com.example.controller.*.*(..))")
    public void controllerMethods() {}
    
    /**
     * 环绕通知:记录请求日志
     */
    @Around("controllerMethods()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        
        // 获取方法信息
        String className = pjp.getTarget().getClass().getSimpleName();
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        
        // 记录请求
        log.info(">>> 请求开始:{}.{}", className, methodName);
        log.info("    参数:{}", Arrays.toString(args));
        
        long start = System.currentTimeMillis();
        Object result = null;
        
        try {
            // 执行方法
            result = pjp.proceed();
            
            long end = System.currentTimeMillis();
            log.info("<<< 请求成功:{}.{}", className, methodName);
            log.info("    返回值:{}", result);
            log.info("    耗时:{}ms", (end - start));
            
        } catch (Exception e) {
            long end = System.currentTimeMillis();
            log.error("<<< 请求失败:{}.{}", className, methodName);
            log.error("    异常:{}", e.getMessage());
            log.error("    耗时:{}ms", (end - start));
            throw e;
        }
        
        return result;
    }
}

案例2:自定义权限校验切面

自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String value();  // 需要的角色
}

切面实现:

@Aspect
@Component
public class RoleAspect {
    
    @Around("@annotation(requireRole)")
    public Object checkRole(ProceedingJoinPoint pjp, RequireRole requireRole) 
            throws Throwable {
        
        String requiredRole = requireRole.value();
        
        // 获取当前用户角色(简化处理)
        String currentRole = getCurrentUserRole();
        
        System.out.println(">>> 【权限校验】需要角色:" + requiredRole);
        System.out.println("    当前角色:" + currentRole);
        
        // 校验权限
        if (!requiredRole.equals(currentRole)) {
            System.out.println("<<< 【权限不足】拒绝访问!\n");
            throw new SecurityException("权限不足,需要" + requiredRole + "角色");
        }
        
        System.out.println("<<< 【权限通过】允许访问");
        
        // 执行方法
        return pjp.proceed();
    }
    
    private String getCurrentUserRole() {
        // 实际项目从SecurityContext或Session获取
        return "USER";  // 模拟普通用户
    }
}

使用:

@Service
public class OrderService {
    
    @RequireRole("ADMIN")  // 需要管理员角色
    public void deleteOrder(Long orderId) {
        System.out.println("   [业务方法] 删除订单:" + orderId);
    }
    
    @RequireRole("USER")  // 需要普通用户角色
    public void viewOrder(Long orderId) {
        System.out.println("   [业务方法] 查看订单:" + orderId);
    }
}

测试:

// 测试1:普通用户查看订单(有权限)
orderService.viewOrder(123L);

// 测试2:普通用户删除订单(无权限)
try {
    orderService.deleteOrder(123L);
} catch (SecurityException e) {
    System.out.println("捕获异常:" + e.getMessage());
}

输出:

>>> 【权限校验】需要角色:USER
    当前角色:USER
<<< 【权限通过】允许访问
   [业务方法] 查看订单:123

>>> 【权限校验】需要角色:ADMIN
    当前角色:USER
<<< 【权限不足】拒绝访问!

捕获异常:权限不足,需要ADMIN角色

完美!


案例3:性能监控切面

@Aspect
@Component
public class PerformanceAspect {
    
    /**
     * 监控service层方法性能
     */
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        
        String methodName = pjp.getSignature().toShortString();
        
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        
        long cost = end - start;
        
        // 如果耗时超过100ms,打印警告
        if (cost > 100) {
            System.out.println("⚠️  【性能警告】方法:" + methodName);
            System.out.println("    耗时:" + cost + "ms(超过阈值100ms)");
        }
        
        return result;
    }
}

🎓 第五问:JoinPoint详解

JoinPoint vs ProceedingJoinPoint

哈吉米: "注意区别!"

// JoinPoint:用于Before、After、AfterReturning、AfterThrowing
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
    // 可以获取方法信息,但不能控制方法执行
}

// ProceedingJoinPoint:只用于Around
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 可以控制方法执行
    return pjp.proceed();  // 执行目标方法
}

JoinPoint常用方法

@Before("pointcut()")
public void before(JoinPoint joinPoint) {
    
    // 1. 获取方法签名
    Signature signature = joinPoint.getSignature();
    String methodName = signature.getName();
    String className = signature.getDeclaringTypeName();
    
    // 2. 获取方法参数
    Object[] args = joinPoint.getArgs();
    
    // 3. 获取目标对象
    Object target = joinPoint.getTarget();
    
    // 4. 获取代理对象
    Object proxy = joinPoint.getThis();
    
    // 5. 获取连接点类型
    String kind = joinPoint.getKind();  // method-execution
    
    System.out.println("类名:" + className);
    System.out.println("方法名:" + methodName);
    System.out.println("参数:" + Arrays.toString(args));
    System.out.println("目标对象:" + target.getClass().getSimpleName());
    System.out.println("代理对象:" + proxy.getClass().getSimpleName());
}

ProceedingJoinPoint特有方法

@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    
    // 1. 执行目标方法(原始参数)
    Object result = pjp.proceed();
    
    // 2. 执行目标方法(修改参数)
    Object[] newArgs = new Object[]{"修改后的参数"};
    result = pjp.proceed(newArgs);
    
    return result;
}

💡 知识点总结

本篇你学到了什么?

AOP核心概念

  • 切面(Aspect)= 切点 + 通知
  • 连接点(Join Point)= 可以插入切面的点
  • 切点(Pointcut)= 匹配连接点的表达式
  • 通知(Advice)= 增强逻辑
  • 目标对象(Target)= 被代理的对象
  • 织入(Weaving)= 创建代理的过程

5种通知类型

  • @Before - 前置通知
  • @After - 后置通知(finally)
  • @AfterReturning - 返回通知
  • @AfterThrowing - 异常通知
  • @Around - 环绕通知(最强大)

通知执行顺序

  • 正常:Around-Before → Before → 方法 → Around-After → After → AfterReturning
  • 异常:Around-Before → Before → 方法 → Around-AfterThrowing → After → AfterThrowing

切点表达式

  • execution - 方法匹配(最常用)
  • @annotation - 注解匹配(推荐)
  • within - 类型匹配
  • args - 参数匹配

实战案例

  • 日志切面
  • 权限校验切面
  • 性能监控切面

记忆口诀

Before方法前,After像finally。
AfterReturning成功返,AfterThrowing异常抛。
Around最强大,控制全流程。
execution匹配方法,annotation更灵活。
JoinPoint获信息,Proceeding能执行。

🤔 思考题

问题1: 如果一个方法上有多个切面,执行顺序如何控制?

提示: @Order注解

问题2: @Around能完全替代其他4种通知吗?为什么还要有其他类型?

问题3: Spring AOP的代理对象是什么时候创建的?在Bean生命周期的哪个阶段?

提示: 下一篇会详细解答,并深入源码!


📢 下期预告(系列完结篇)

《Spring AOP原理(三):源码剖析代理创建时机,AOP失效场景全解!》

下一篇我们将:

  • 深入AbstractAutoProxyCreator源码
  • 分析代理对象的创建时机
  • 详解AOP失效的8种场景
  • 每种场景的解决方案
  • this调用问题的本质
  • 系列知识点总结

系列完结篇,彻底搞懂Spring AOP! 🚀


💬 互动时间

你最常用哪种通知类型?
有没有遇到过AOP不生效的情况?
对切点表达式还有疑问吗?

欢迎评论区讨论!💭


觉得有帮助?三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发

看完这篇,@Aspect注解完全掌握!


下一篇见(完结篇)! 👋