SpringAOP 中切点切面通知以及切点表达式具体理解和应用

核心概念图解

切面类

1. 首先创建一个简单的服务类(连接点所在位置)

@Service
public class PaymentService {
    
    // 这些方法都是潜在的连接点
    public void processPayment(double amount) {
        System.out.println("处理支付: $" + amount);
        // 模拟业务逻辑
        if (amount > 1000) {
            throw new RuntimeException("金额过大!");
        }
    }
    
    public void refundPayment(double amount) {
        System.out.println("退款处理: $" + amount);
    }
}

2. 创建切面类(包含切点定义和通知)

@Aspect
@Component
public class PaymentAspect {

    // ===== 切点定义 =====
    // 定义匹配所有PaymentService方法的切点
    @Pointcut("execution(* com.example.service.PaymentService.*(..))")
    public void paymentOperations() {}
    
    // 定义匹配processPayment方法的切点
    @Pointcut("execution(* com.example.service.PaymentService.processPayment(double)) && args(amount)")
    public void processPaymentPointcut(double amount) {}

    // ===== 各种通知 =====
    // 前置通知:在方法执行前运行
    @Before("paymentOperations()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【前置通知】准备执行: " + methodName);
    }
    
    // 环绕通知:包裹方法执行
    @Around("processPaymentPointcut(amount)")
    public Object validateAmount(ProceedingJoinPoint joinPoint, double amount) throws Throwable {
        System.out.println("【环绕通知】验证金额: $" + amount);
        
        if (amount < 0) {
            System.out.println("⚠️ 金额不能为负数!");
            return null;
        }
        
        return joinPoint.proceed(); // 继续执行原方法
    }
    
    // 返回通知:方法成功返回后执行
    @AfterReturning(pointcut = "paymentOperations()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("【返回通知】" + joinPoint.getSignature().getName() + " 执行成功");
    }
    
    // 异常通知:方法抛出异常时执行
    @AfterThrowing(pointcut = "paymentOperations()", throwing = "ex")
    public void logException(JoinPoint joinPoint, Exception ex) {
        System.out.println("【异常通知】" + joinPoint.getSignature().getName() 
                          + " 发生异常: " + ex.getMessage());
    }
    
    // 后置通知:无论方法是否成功都会执行
    @After("paymentOperations()")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("【后置通知】" + joinPoint.getSignature().getName() + " 执行结束\n");
    }
}
创建切面类为什么要加@Aspect 和 @Component?

1. @Aspect 的作用

这个注解是声明一个类为切面的核心标识,它告诉 Spring:

  • 这个类包含 AOP 相关的定义

  • 需要解析其中的切点(@Pointcut)和通知(@Before, @Around 等)

  • 在运行时为匹配的 Bean 创建代理对象

没有 @Aspect的后果

// 缺少 @Aspect 注解
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore() {
        System.out.println("方法开始执行");
    }
}

Spring 会将其视为普通 Bean,不会执行任何 AOP 逻辑

2. @Component的作用

这个注解是将类注册为 Spring Bean,让 Spring 容器能够:

  • 实例化这个切面类

  • 管理其生命周期

  • 将其纳入 AOP 处理流程

没有 @Component的后果:

@Aspect // 只有 @Aspect
public class SecurityAspect {
    @Around("@annotation(RequiresAdmin)")
    public Object checkAdmin(ProceedingJoinPoint jp) {
        // 权限检查逻辑...
    }
}

Spring 会忽略这个类,因为容器不知道它的存在

3.什么是切点表达式

1. 基本结构
execution([修饰符] 返回类型 [包路径].[类名].[方法名](参数类型) [异常类型])
  • execution:最常用的切点指示器

  • 括号内的内容:定义了要拦截的方法的精确匹配规则

2. 示例解析
@Around("execution(* com.example.service.*.*(..))")

部分

含义

*

任意返回类型(可以是 void、String、Object 等)

com.example.service

包路径

.*

当前包下的任意类

.*

任意方法名

(..)

任意参数(可以是无参数、一个参数或多个参数)

整体含义:拦截 com.example.service 包下所有类的所有方法,无论返回类型和参数是什么。

最后通俗讲: @Before, @Around 等括号里面的就是切点表达式

在 Spring AOP 中,可以通过 @Pointcut 注解将切点表达式提取出来,实现复用和集中管理。这样同一个切面类中的多个通知都可以引用同一个切点定义,避免重复编写表达式

3.提取切点表达式
使用 @Pointcut 提取切点表达式
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MultiAdviceAspect {

    // 步骤1:使用 @Pointcut 提取切点表达式
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {} // 方法名即为切点标识符

    // 步骤2:多个通知复用同一个切点
    @Before("serviceLayer()")
    public void logMethodStart() {
        System.out.println("----- 方法开始执行 -----");
    }

    @Around("serviceLayer()")
    public Object monitorPerformance(ProceedingJoinPoint jp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = jp.proceed();
        long duration = System.currentTimeMillis() - start;
        System.out.println("执行耗时: " + duration + "ms");
        return result;
    }

    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logResult(Object result) {
        System.out.println("方法返回: " + result);
    }
}
4.组合多个切点表达式:
@Aspect
@Component
public class SecurityAspect {

    // 切点1:所有服务方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void allServiceMethods() {}

    // 切点2:带@AdminOnly注解的方法
    @Pointcut("@annotation(com.example.security.AdminOnly)")
    public void adminOnlyMethods() {}

    // 组合切点:服务方法且需要管理员权限
    @Pointcut("allServiceMethods() && adminOnlyMethods()")
    public void securedServiceMethods() {}

    // 权限校验(使用组合切点)
    @Before("securedServiceMethods()")
    public void checkAdminRole() {
        if (!currentUser.isAdmin()) {
            throw new SecurityException("需要管理员权限");
        }
    }
}
5.带参数的切点表达式
@Aspect
@Component
public class ValidationAspect {

    // 提取带参数的切点
    @Pointcut("execution(* com.example.service.*.*(..)) && args(userId, ..)")
    public void methodsWithUserId(Long userId) {}

    // 参数自动传递到通知
    @Before("methodsWithUserId(userId)")
    public void validateUserId(Long userId) {
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("无效的用户ID: " + userId);
        }
    }
}
6.复用外部切点(跨切面类)
// 集中管理切点的类
public class CommonPointcuts {
    
    @Pointcut("execution(* com.example..repository.*.*(..))")
    public void repositoryLayer() {}
    
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethods() {}
}

// 其他切面复用切点
@Aspect
@Component
public class TransactionAspect {

    // 引用外部切点(全限定名)
    @Around("com.example.aop.CommonPointcuts.repositoryLayer() && " +
            "com.example.aop.CommonPointcuts.transactionalMethods()")
    public Object manageTransaction(ProceedingJoinPoint jp) throws Throwable {
        // 事务管理逻辑...
    }
}

切⾯优先级@Order

当我们在⼀个项⽬中,定义了多个切⾯类时,并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候,这些切⾯类中的通知⽅法都会执⾏,那么这⼏个通知⽅法的执⾏顺序是什么样 的呢?

定义多个切⾯类: 为防⽌⼲扰,我们把AspectDemo这个切⾯先去掉(把@Component注解去掉就可以) 为简单化,只写了 @Before 和  @After 两个通知

@Component
public class AspectDemo2 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    // 前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2 -> Before ⽅法");
    }
    
    // 后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo2 -> After ⽅法");
    }
}

@Component
public class AspectDemo3 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    // 前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo3 -> Before ⽅法");
    }
    
    // 后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo3 -> After ⽅法");
    }
}

@Component
public class AspectDemo4 {
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    // 前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo4 -> Before ⽅法");
    }
    
    // 后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo4 -> After ⽅法");
    }
}
//执行结果
AspectDemo2 ->Before方法

AspectDemo3 ->Before方法

AspectDemo4 ->Before方法

AspectDemo4  ->After方法

AspectDemo3  ->After方法

AspectDemo2  ->After方法

通过上述程序的运⾏结果,可以看出:

存在多个切⾯类时,默认按照切⾯类的类名字⺟排序: • @Before 通知:字⺟排名靠前的先执⾏ • @A fter 通知:字⺟排名靠前的后执⾏ 但这种⽅式不⽅便管理,我们的类名更多还是具备⼀定含义的. Spring 给我们提供了⼀个新的注解,来控制这些切⾯通知的执⾏顺序: 使⽤⽅式如下:

@Aspect
 @Component
 @Order(2)
 public class AspectDemo2 {
     //...代码省略
}


 @Order(3)
 public class AspectDemo3 {
     //...代码省略
}


@Order(4)
 public class AspectDemo4 {
     //...代码省略
}

@Order注解标识的切⾯类,执⾏顺序如下:

• @Before 通知:数字越⼩先执⾏ • @After 通知:数字越⼤先执⾏

@Order 控制切⾯的优先级,先执⾏优先级较⾼的切⾯,再执⾏优先级较低的切⾯,最终执⾏⽬标⽅法

@annotation 和自定义注解 

@annotation 切点指示符必须和自定义注解一起使用,这是它的核心设计目的。这种组合是 Spring AOP 中最强大、最灵活的实现横切关注点的方式 

为什么必须配合自定义注解使用?

@annotation 本身是一个匹配机制它需要指定一个具体的注解类型作为参数:

 定义业务注解

// 审计日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String module();  // 必填:业务模块
    String action() default "执行"; // 可选:操作类型
}

在业务方法上标记

public class OrderService {
    
    @AuditLog(module = "订单管理", action = "创建")
    public Order createOrder(OrderRequest request) {
        // 业务逻辑
    }
    
    @AuditLog(module = "订单管理", action = "取消")
    public void cancelOrder(String orderId) {
        // 业务逻辑
    }
}

切面处理带注解的方法

@Aspect
@Component
public class AuditAspect {

    // 核心:匹配所有带@AuditLog注解的方法
    @AfterReturning("@annotation(auditLog)")
    public void logAuditEvent(JoinPoint jp, AuditLog auditLog) {
        String module = auditLog.module();
        String action = auditLog.action();
        String method = jp.getSignature().getName();
        
        // 获取当前用户(实际项目从SecurityContext获取)
        String user = "当前用户";
        
        // 构造审计日志
        AuditRecord record = new AuditRecord(user, module, action, method);
        
        // 发送到审计系统
        auditService.sendRecord(record);
    }
}
为什么比 execution 更好?

假设我们需要给财务相关方法添加特别监控:

传统 execution 方式:

// 切点表达式会变得复杂且难以维护
@Pointcut("execution(* com.example.service.FinanceService.*(..)) || " +
          "execution(* com.example.service.PaymentService.pay*(..)) || " +
          "execution(* com.example.controller.BillingController.*(..))")
public void financialOperations() {}
注解方式:
// 1. 创建注解
@Target(ElementType.METHOD)
public @interface FinancialOperation {}

// 2. 标记方法
public class PaymentService {
    @FinancialOperation
    public void processPayment() {...}
}

// 3. 简单切点
@Pointcut("@annotation(FinancialOperation)")
public void financialOperations() {}
总结

@annotation 和自定义注解的组合是 Spring AOP 的黄金搭档,它们共同实现了:

  1. 精准控制:只拦截需要增强的方法
  2. 配置化:通过注解属性动态调整行为
  3. 解耦:业务代码无需知道横切逻辑
  4. 可扩展:轻松添加新的横切关注点
  5. 可读性:注解即文档,清晰表达意图

这种模式是现代 Java 开发中实现横切关注点的首选方案,特别适合构建可维护的大型应用系统。

连接点:

上述已经将springAOP创建的过程已经讲解清楚了, 一个切面类是由切点+切面组成的,连接到其实就是在切面类中定义了通知执行代码的时候,拦截的代码,也就是说:如@Around()等通知里面的路径下对应的方法可以看成连接点

@Aspect
@Component
public class PerformanceAspect {

    @Around("@annotation(monitor)")  // 拦截自定义注解
    public Object logPerformance(ProceedingJoinPoint jp, MonitorPerformance monitor) throws Throwable {
        String tag = monitor.value();
        long start = System.currentTimeMillis();
        
        try {
            return jp.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.printf("[%s] 执行耗时: %dms%n", tag, duration);
            // 可扩展:将统计结果发送到监控系统
        }
    }
}

比如这几行代码,在目标方法执行前后都要执行,目标方法就是连接点。。

切面: 就是切面类下面所有的代码可以堪看成一个切面,所有切面类 等于  切面 加  通知,通知里面又包括了切点,而切点对应路径下面的方法又叫做连接点。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值