@Pointcut
定义与作用
@Pointcut 用于定义可复用的切入点表达式。切入点(Pointcut)是匹配连接点(Join Point)的谓词,它决定了 Advice(通知)应该在哪些方法上触发。
在实际开发中,同一个切点表达式往往被多个 Advice 复用(例如日志记录需要 @Before 和 @After 共用同一套匹配规则)。@Pointcut 将这些表达式抽取为命名方法,避免在多个 Advice 注解中重复书写冗长的 execution(...) 字符串。
适用位置与常用属性
适用位置
@Pointcut 只能标注在方法上,且该方法必须位于 @Aspect 切面类内部。方法体通常为空,因为方法本身不会被调用,其存在的意义是承载 @Pointcut 注解中的表达式字符串。
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.feixiang.service.*.*(..))")
public void serviceLayer() {} // 方法体为空
}
常用属性
@Pointcut 只有一个核心属性 value(可省略属性名直接写字符串),用于声明切入点表达式:
| 属性 | 类型 | 说明 |
|---|---|---|
value | String | 切入点表达式,支持 execution、within、args、bean、@annotation 等指示器 |
核心原理
Pointcut 表达式匹配流程
当 Spring 容器创建 Bean 的代理对象时,AspectJExpressionPointcut 负责解析 @Pointcut 中的表达式,并在运行时判断目标方法是否匹配:
常用 Pointcut 表达式指示器
| 指示器 | 语法示例 | 匹配范围 |
|---|---|---|
execution | execution(* com.feixiang.service.*.*(..)) | 匹配 service 包下所有类的所有方法 |
within | within(com.feixiang.service.*) | 匹配 service 包下所有类的所有方法(类级别) |
args | args(java.lang.String) | 匹配参数为 String 类型的方法 |
bean | bean(*Service) | 匹配 Bean 名以 Service 结尾的方法 |
@annotation | @annotation(com.feixiang.annotation.Loggable) | 匹配带有 @Loggable 注解的方法 |
this | this(com.feixiang.service.ScoreService) | 匹配代理对象实现了 ScoreService 接口的方法 |
target | target(com.feixiang.service.StudentScoreService) | 匹配目标对象为 StudentScoreService 类型的方法 |
execution 表达式语法详解
execution 是最常用的指示器,其完整语法为:
execution(修饰符? 返回值 包名.类名.方法名(参数) throws 异常?)
| 通配符 | 含义 |
|---|---|
* | 匹配任意字符(一个段) |
.. | 匹配任意字符(多个段,用于包名或参数) |
+ | 匹配指定类及其子类 |
典型示例:
| 表达式 | 含义 |
|---|---|
execution(public * *(..)) | 所有 public 方法 |
execution(* set*(..)) | 所有以 set 开头的方法 |
execution(* com.feixiang.service.*.*(..)) | service 包下所有类的所有方法 |
execution(* com.feixiang..*.*(..)) | feixiang 包及其子包下所有类的所有方法 |
execution(* *..service.*.*(..)) | 任意包下 service 子包中所有类的所有方法 |
execution(* com.feixiang.service.ScoreService.*(..)) | ScoreService 类的所有方法 |
execution(* com.feixiang.service.*+.*(..)) | service 包下所有类及其子类的所有方法 |
execution(* save*(..)) | 所有以 save 开头的方法 |
execution(* *(String, ..)) | 第一个参数为 String,后面任意参数的方法 |
execution(* *(..) throws Exception) | 声明抛出 Exception 的方法 |
完整示例:飞翔科技学生管理系统切点设计
场景简述
广州飞翔科技公司的后端开发小崔需要为学生成绩管理系统设计一套完整的切点规则:
- 所有 Service 层方法需要记录性能日志
- 所有以
query或get开头的方法需要校验查询权限 - 所有标注了
@RequireTeacher的方法需要校验教师身份 - 所有参数中包含
Long studentId的方法需要记录操作日志 - 所有 Bean 名以
Service结尾的方法需要事务监控
架构师白歌要求小崔使用 @Pointcut 将这些规则抽取为可复用的切点方法,避免在 Advice 中重复书写表达式。
操作前代码/配置
在未使用 @Pointcut 之前,小崔在每个 Advice 中重复书写表达式:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.feixiang.student.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("[LOG] 方法开始:" + joinPoint.getSignature().getName());
}
@After("execution(* com.feixiang.student.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("[LOG] 方法结束:" + joinPoint.getSignature().getName());
}
@Around("execution(* com.feixiang.student.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println("[LOG] 耗时:" + duration + "ms");
return result;
}
}
问题:execution(* com.feixiang.student.service.*.*(..)) 重复出现了三次。如果白歌要求将切点范围从 service 包改为 service.impl 包,需要修改三处,极易遗漏。
使用该注解的完整代码
第一步:定义可复用的切点方法
@Aspect
@Component
public class SystemPointcuts {
// 切点一:Service 层所有方法
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// 切点二:所有查询方法(方法名以 query 或 get 开头)
@Pointcut("execution(* com.feixiang.student.service.*.query*(..)) || " +
"execution(* com.feixiang.student.service.*.get*(..))")
public void queryOperation() {}
// 切点三:所有标注了 @RequireTeacher 的方法
@Pointcut("@annotation(com.feixiang.student.annotation.RequireTeacher)")
public void teacherRequired() {}
// 切点四:参数中包含 Long studentId 的方法
@Pointcut("args(studentId)")
public void studentIdOperation(Long studentId) {}
// 切点五:Bean 名以 Service 结尾的方法
@Pointcut("bean(*Service)")
public void serviceBean() {}
// 切点六:service 包及其子包下的所有方法(within 类级别匹配)
@Pointcut("within(com.feixiang.student.service..*)")
public void withinServicePackage() {}
// 组合切点:Service 层且是查询操作
@Pointcut("serviceLayer() && queryOperation()")
public void serviceQuery() {}
// 组合切点:Service 层且需要教师权限
@Pointcut("serviceLayer() && teacherRequired()")
public void serviceTeacherOperation() {}
}
第二步:在 Advice 中引用切点方法
@Aspect
@Component
public class LogAspect {
// 引用 SystemPointcuts 中定义的切点
@Before("com.feixiang.student.aspect.SystemPointcuts.serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("[飞翔科技-日志] 方法开始:" + joinPoint.getSignature().getName());
}
@After("com.feixiang.student.aspect.SystemPointcuts.serviceLayer()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("[飞翔科技-日志] 方法结束:" + joinPoint.getSignature().getName());
}
@Around("com.feixiang.student.aspect.SystemPointcuts.serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println("[飞翔科技-日志] 耗时:" + duration + "ms");
return result;
}
}
@Aspect
@Component
public class PermissionAspect {
@Before("com.feixiang.student.aspect.SystemPointcuts.teacherRequired()")
public void checkTeacher(JoinPoint joinPoint) {
String role = getCurrentRole();
if (!"TEACHER".equals(role)) {
throw new AccessDeniedException("需要教师权限");
}
System.out.println("[飞翔科技-权限] 教师校验通过");
}
@Before("com.feixiang.student.aspect.SystemPointcuts.queryOperation() && args(studentId)")
public void checkQueryPermission(JoinPoint joinPoint, Long studentId) {
System.out.println("[飞翔科技-权限] 查询学号 " + studentId + " 的权限校验通过");
}
private String getCurrentRole() {
return "TEACHER"; // 模拟获取角色
}
}
第三步:业务 Service 类
@Service
public class StudentScoreService {
public Score queryScore(Long studentId) {
System.out.println("[业务] 查询学生 " + studentId + " 的成绩");
return new Score(studentId, 88.5);
}
@RequireTeacher
public void updateScore(Long studentId, Double newScore) {
System.out.println("[业务] 更新学生 " + studentId + " 的成绩为 " + newScore);
}
@RequireTeacher
public void deleteScore(Long studentId) {
System.out.println("[业务] 删除学生 " + studentId + " 的成绩");
}
}
操作后运行结果及分析
测试代码:
public class PointcutDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
// 测试查询方法(触发 serviceLayer + queryOperation)
service.queryScore(2024001L);
System.out.println("---");
// 测试更新方法(触发 serviceLayer + teacherRequired)
service.updateScore(2024001L, 95.0);
ctx.close();
}
}
控制台输出:
[飞翔科技-日志] 方法开始:queryScore
[飞翔科技-权限] 查询学号 2024001 的权限校验通过
[业务] 查询学生 2024001 的成绩
[飞翔科技-日志] 方法结束:queryScore
[飞翔科技-日志] 耗时:12ms
---
[飞翔科技-日志] 方法开始:updateScore
[飞翔科技-权限] 教师校验通过
[业务] 更新学生 2024001 的成绩为 95.0
[飞翔科技-日志] 方法结束:updateScore
[飞翔科技-日志] 耗时:8ms
分析:
@Pointcut将表达式抽取为命名方法,实现了"一次定义,多处复用"- 组合切点
serviceLayer() && queryOperation()实现了更精确的匹配规则 - 当需要调整切点范围时,只需修改
@Pointcut方法中的表达式,所有引用它的 Advice 自动生效 args(studentId)与args绑定的结合,使得 Advice 方法可以直接获取方法参数值
易错场景与面试考点
易错场景一:execution 表达式包名写错导致切点不匹配
反例:
@Pointcut("execution(* com.feixiang.service.*.*(..))")
public void serviceLayer() {}
而实际包路径是 com.feixiang.student.service。
现象:Advice 始终不执行,且没有任何报错。
排查方法:
- 检查包名是否完全匹配,注意子包层级
- 使用
..通配符匹配子包:execution(* com.feixiang..service.*.*(..)) - 在 Advice 中打印
joinPoint.getSignature().getDeclaringTypeName(),确认实际包路径
正确做法:
// 使用 .. 匹配任意子包,避免层级遗漏
@Pointcut("execution(* com.feixiang..*Service.*(..))")
public void serviceLayer() {}
易错场景二:@Pointcut 方法被声明为 private 导致其他切面无法引用
反例:
@Aspect
@Component
public class SystemPointcuts {
@Pointcut("execution(* com.feixiang.service.*.*(..))")
private void serviceLayer() {} // 错误!声明为 private
}
@Aspect
@Component
public class LogAspect {
// 编译错误!无法访问 private 的 serviceLayer
@Before("com.feixiang.aspect.SystemPointcuts.serviceLayer()")
public void logBefore() {}
}
正确做法:@Pointcut 方法应声明为 public,以便其他切面类引用。
@Pointcut("execution(* com.feixiang.service.*.*(..))")
public void serviceLayer() {} // 正确:public
易错场景三:args 表达式与 Advice 方法参数类型不匹配
反例:
@Pointcut("args(studentId)")
public void studentIdOperation(Long studentId) {}
@Before("studentIdOperation(studentId)")
public void checkStudent(JoinPoint joinPoint, String studentId) { // 错误!类型不匹配
}
现象:应用启动时抛出 IllegalArgumentException,提示参数类型无法绑定。
正确做法:Advice 方法中的参数类型必须与 args 表达式匹配。
@Before("studentIdOperation(studentId)")
public void checkStudent(JoinPoint joinPoint, Long studentId) { // 正确:Long 匹配 Long
}
面试高频考点
考点一:execution(* com.feixiang.service.*.*(..)) 各部分的含义?
execution是指示器;第一个*表示任意返回值;com.feixiang.service是包名;第二个*表示任意类;第三个*表示任意方法;(..)表示任意参数。整体含义:匹配com.feixiang.service包下所有类的所有方法。
考点二:within 和 execution 的区别?
within是类级别匹配,只能指定到类或包,不能精确到方法签名,例如within(com.feixiang.service.*)匹配该包下所有类的所有方法。execution是方法级别匹配,可以精确到方法名、参数、返回值、修饰符,粒度更细。within性能略优于execution,因为只需判断类名。
考点三:args 和 execution 中参数匹配的区别?
execution(* *(String))匹配方法签名中声明了 String 参数的方法,是静态签名匹配。args(String)匹配运行时实际传入的参数是 String 类型的方法,是动态类型匹配。如果方法声明为Object但实际传入String,args(String)会匹配而execution(* *(String))不会匹配。
考点四:如何匹配注解?@annotation 和 @within 的区别?
@annotation匹配方法上有指定注解的方法,例如@annotation(com.feixiang.annotation.Loggable)。@within匹配类上有指定注解的类中的所有方法,例如@within(com.feixiang.annotation.Loggable)。如果注解标注在类上,@annotation不会匹配该类的方法(除非方法上也标注了)。