乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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(可省略属性名直接写字符串),用于声明切入点表达式:

属性类型说明
valueString切入点表达式,支持 execution、within、args、bean、@annotation 等指示器

核心原理

Pointcut 表达式匹配流程

当 Spring 容器创建 Bean 的代理对象时,AspectJExpressionPointcut 负责解析 @Pointcut 中的表达式,并在运行时判断目标方法是否匹配:

常用 Pointcut 表达式指示器

指示器语法示例匹配范围
executionexecution(* com.feixiang.service.*.*(..))匹配 service 包下所有类的所有方法
withinwithin(com.feixiang.service.*)匹配 service 包下所有类的所有方法(类级别)
argsargs(java.lang.String)匹配参数为 String 类型的方法
beanbean(*Service)匹配 Bean 名以 Service 结尾的方法
@annotation@annotation(com.feixiang.annotation.Loggable)匹配带有 @Loggable 注解的方法
thisthis(com.feixiang.service.ScoreService)匹配代理对象实现了 ScoreService 接口的方法
targettarget(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 始终不执行,且没有任何报错。

排查方法:

  1. 检查包名是否完全匹配,注意子包层级
  2. 使用 .. 通配符匹配子包:execution(* com.feixiang..service.*.*(..))
  3. 在 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 不会匹配该类的方法(除非方法上也标注了)。

上一页
@Aspect
下一页
@Before