核心概念图解
切面类
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 的黄金搭档,它们共同实现了:
- 精准控制:只拦截需要增强的方法
- 配置化:通过注解属性动态调整行为
- 解耦:业务代码无需知道横切逻辑
- 可扩展:轻松添加新的横切关注点
- 可读性:注解即文档,清晰表达意图
这种模式是现代 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);
// 可扩展:将统计结果发送到监控系统
}
}
}
比如这几行代码,在目标方法执行前后都要执行,目标方法就是连接点。。
切面: 就是切面类下面所有的代码可以堪看成一个切面,所有切面类 等于 切面 加 通知,通知里面又包括了切点,而切点对应路径下面的方法又叫做连接点。。。