【JavaEE29-后端部分】Spring AOP 切点表达式详解——精准定位,想切哪里切哪里

张开发
2026/4/9 19:15:06 15 分钟阅读

分享文章

【JavaEE29-后端部分】Spring AOP 切点表达式详解——精准定位,想切哪里切哪里
开篇快递员怎么知道把包裹送到你家老铁们你有没有想过一个问题快递员每天要送几百个包裹他是怎么知道哪个包裹送到哪一家的答案是地址。地址写得越详细快递员就越容易找到你家。中国→ 范围太大了不知道哪个省中国贵州省→ 范围缩小了一点中国贵州省黔南布依族苗族自治州→ 更具体了中国贵州省黔南布依族苗族自治州××小区1号楼202室→ 精准定位在 Spring AOP 中切点表达式就相当于这个“地址”。它告诉 AOP你要对哪些方法进行增强是所有的 Controller 方法还是某个包下的所有类还是某个特定的方法本期我们就来学习切点表达式的语法规则让你能够精准定位任何你想要增强的方法。一、切点表达式是什么1.1 官方定义切点表达式Pointcut Expression是一组规则用来匹配一个或多个连接点方法。它的作用就是告诉 AOP“我要对哪些方法下手”。1.2 生活化理解想象你是一个快递公司的调度员你要给快递员分派任务“把本区域所有快递都送过去”→ 匹配所有方法“只送图书相关的快递”→ 匹配controller包下的方法“只送给张三的快递”→ 匹配addBook方法“只送贵州省内的快递”→ 匹配com.zhongge包下的方法切点表达式就是这种“指令”它用一套固定的语法来编写。二、最常用的切点表达式executionexecution 译为执行aop中表示匹配方法执行2.1 基本语法execution(访问修饰符 返回类型 包名.类名.方法名(参数)异常)其中访问修饰符public、private等可以省略异常可以省略最简形式execution(返回类型 包名.类名.方法名(参数))2.2 举个例子// 匹配 BookController 中的 addBook 方法无参数execution(*com.zhongge.controller.BookController.addBook())// 匹配 BookController类 中的所有方法execution(*com.zhongge.controller.BookController.*(..))// 匹配 controller 包下所有类的所有方法execution(*com.zhongge.controller.*.*(..))// 匹配 com.zhongge 包下所有类的所有方法execution(*com.zhongge..*.*(..))三、通配符详解*和..3.1*—— 匹配任意一个元素*可以匹配返回类型、包名一层、类名、方法名、参数类型一个位置写法含义返回类型*任意返回类型包名一层com.zhongge.*.controller匹配com.zhongge.book.controller等类名*Controller匹配BookController、UserController等方法名*任意方法名参数一个(*)任意一个参数示例// 匹配任意返回类型execution(*com.zhongge.controller.BookController.addBook())// 匹配 BookController 中以 get 开头的方法execution(*com.zhongge.controller.BookController.get*(..))// 匹配 BookController 中只有一个参数的方法execution(*com.zhongge.controller.BookController.*(*))3.2..—— 匹配多个连续的元素..可以匹配多层包路径、任意个数的参数位置写法含义包名com.zhongge..controller匹配com.zhongge.controller、com.zhongge.book.controller等参数(..)任意个数、任意类型的参数参数(String, ..)第一个参数是 String后面任意示例// 匹配 com.zhongge 包及其子包下所有类的所有方法execution(*com.zhongge..*.*(..))// 匹配任意个数参数的方法execution(*com.zhongge.controller.BookController.*(..))// 匹配第一个参数是 Integer后面任意参数的方法execution(*com.zhongge.controller.BookController.*(Integer,..))四、实战演练一步步写出精准的切点表达式假设我们的包结构如下com.zhongge ├── controller │ ├── BookController │ │ ├── addBook(BookInfo) │ │ ├── getListByPage(PageRequest) │ │ ├── queryBookById(Integer) │ │ ├── updateBook(BookInfo) │ │ └── batchDelete(ListInteger) │ └── UserController │ └── login(String, String) ├── service │ ├── BookService │ └── UserService └── mapper ├── BookMapper └── UserMapper4.1 匹配单个方法// 匹配 BookController 中的 addBook 方法参数是 BookInfoexecution(*com.zhongge.controller.BookController.addBook(com.zhongge.model.BookInfo))// 简化写法用 * 代替全限定类名execution(*com.zhongge.controller.BookController.addBook(*))4.2 匹配一个类中的所有方法// 匹配 BookController 中的所有方法execution(*com.zhongge.controller.BookController.*(..))4.3 匹配一个包下的所有类中的所有方法// 匹配 controller 包下所有类的所有方法execution(*com.zhongge.controller.*.*(..))4.4 匹配一个包及其子包下的所有类中的所有方法// 匹配 com.zhongge 包及其子包下所有类的所有方法execution(*com.zhongge..*.*(..))4.5 匹配符合命名规则的方法// 匹配所有以 get 开头的方法execution(*com.zhongge.controller.*.get*(..))// 匹配所有以 query 开头的方法execution(*com.zhongge.controller.*.query*(..))4.6 匹配特定参数的方法// 匹配只有一个参数的方法execution(*com.zhongge.controller.*.*(*))// 匹配第一个参数是 Integer 的方法execution(*com.zhongge.controller.*.*(Integer,..))// 匹配无参数的方法execution(*com.zhongge.controller.*.*())4.7 组合多个条件// 匹配 BookController 中返回类型为 Result 的方法execution(com.zhongge.model.Resultcom.zhongge.controller.BookController.*(..))// 匹配 public 方法execution(public*com.zhongge.controller.*.*(..))五、第二种切点表达式annotationannotation译为注解5.1 什么时候用annotationexecution表达式虽然强大但它有一个缺点必须按照规则来匹配。如果你的需求是“匹配多个无规则的方法”比如BookController中的addBook方法UserController中的login方法TestController中的test方法这几个方法没有共同的包名、类名、方法名前缀用execution很难匹配。这时候我们可以用自定义注解 annotation的方式。5.2 自定义注解先创建一个自定义注解比如叫Logpackagecom.zhongge.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;// 这个注解只能用在方法上Target(ElementType.METHOD)// 这个注解在运行时保留这样 AOP 才能读取到Retention(RetentionPolicy.RUNTIME)publicinterfaceLog{// 可以定义属性比如模块名称Stringvalue()default;}注解的两个关键注解Target指定这个注解可以用在哪里类、方法、字段等Retention指定这个注解保留到什么时候源代码、编译期、运行时5.3 在需要增强的方法上添加注解RestControllerRequestMapping(/book)publicclassBookController{Log(添加图书)RequestMapping(/addBook)publicResultaddBook(BookInfobookInfo){...}RequestMapping(/queryBookById)publicBookInfoqueryBookById(IntegerbookId){...}// 这个不会被增强}RestControllerRequestMapping(/user)publicclassUserController{Log(用户登录)RequestMapping(/login)publicBooleanlogin(Stringname,Stringpassword,HttpSessionsession){...}}5.4 编写切面用annotation匹配Slf4jAspect//告诉Spring这是一个切面类Component//将这个切面类交给Spring管理publicclassLogAspect{// 匹配所有加了 Log 注解的方法Before(annotation(com.zhongge.annotation.Log))publicvoidbefore(JoinPointjoinPoint){log.info(开始执行方法{},joinPoint.getSignature().getName());}After(annotation(com.zhongge.annotation.Log))publicvoidafter(JoinPointjoinPoint){log.info(方法执行完毕{},joinPoint.getSignature().getName());}// 如果需要获取注解的属性值Around(annotation(log))publicObjectaround(ProceedingJoinPointjoinPoint,Loglog)throwsThrowable{log.info(模块{}方法{} 开始执行,log.value(),joinPoint.getSignature().getName());longstartSystem.currentTimeMillis();ObjectresultjoinPoint.proceed();longendSystem.currentTimeMillis();log.info(模块{}方法{} 执行耗时{} ms,log.value(),joinPoint.getSignature().getName(),end-start);returnresult;}}JoinPoint连接点可以用在Before、After、AfterReturning、AfterThrowing中。ProceedingJoinPoint可执行连接点只能用在Around中。1、为什么会有两个不同的对象AOP 在“环绕通知”Around时需要你手动决定是否执行原始方法proceed()并且可以修改返回值、处理异常。所以 Spring 专门给你一个ProceedingJoinPoint它继承自JoinPoint多了proceed()方法。而在其他通知Before、After等中原始方法一定会被执行你拦不住你只需要读一些信息方法名、参数等所以给你JoinPoint就够了。2、正确用法对照表通知类型能用的参数是否必须调用proceed()BeforeJoinPoint否AfterJoinPoint否AfterReturningJoinPoint还可以加returning接收返回值否AfterThrowingJoinPoint还可以加throwing接收异常否Around必须用ProceedingJoinPoint是否则原始方法不执行3、一句话记忆Around要用ProceedingJoinPoint因为要 proceed其他通知用JoinPoint因为只需要读信息。5.5 执行效果访问/book/addBook控制台输出模块添加图书方法addBook 开始执行 ... 业务执行 ... 模块添加图书方法addBook 执行耗时35 ms访问/user/login控制台输出模块用户登录方法login 开始执行 ... 业务执行 ... 模块用户登录方法login 执行耗时12 ms访问/book/queryBookById没有加Log注解没有任何日志输出。这就是annotation的强大之处想增强哪个方法就在哪个方法上加个注解非常灵活5.6 简单理解原理逻辑是这样的我们要对某个连接点也就是某个方法进行统一处理说白了就是要对这个方法“下手”。但是这些方法比较特殊可能不在同一个包、同一个类里也没有什么规律可循所以我们不能用 execution 表达式去统一匹配它们。那怎么办呢用注解。我们自己定义一个“标签”——也就是自定义注解。第一步先把这个标签定义出来自定义一个注解。定义好之后你想对哪个方法下手就在哪个方法上贴上这个标签。贴了标签就代表这个方法要被统一处理。贴好标签之后我们再写一个切面类专门负责“收拾”这些贴了标签的方法。在切面类里我们用 annotation 来引出这些方法并写上自定义注解的全限定类名包名 类名。这样切面就知道要去处理哪些方法了。六、切点表达式速查表6.1 常用 execution 表达式需求表达式匹配 controller 包下所有类的所有方法execution(* com.zhongge.controller.*.*(..))匹配 controller 包及其子包下所有类的所有方法execution(* com.zhongge.controller..*.*(..))匹配 BookController 中的所有方法execution(* com.zhongge.controller.BookController.*(..))匹配 BookController 中的无参方法execution(* com.zhongge.controller.BookController.*())匹配 BookController 中返回类型为 Result 的方法execution(com.zhongge.model.Result com.zhongge.controller.BookController.*(..))匹配所有 public 方法execution(public * *(..))匹配所有以 get 开头的方法execution(* com.zhongge.controller.*.get*(..))匹配只有一个参数的方法execution(* com.zhongge.controller.*.*(*))匹配第一个参数是 Integer 的方法execution(* com.zhongge.controller.*.*(Integer, ..))6.2 annotation 表达式需求表达式匹配所有加了 Log 注解的方法annotation(com.zhongge.annotation.Log)匹配所有加了任意注解的方法比如匹配加了 RequestMapping 注解的方法annotation(org.springframework.web.bind.annotation.RequestMapping)6.3 组合表达式、||、!// 匹配 controller 包下所有方法但排除 BookControllerPointcut(execution(* com.zhongge.controller.*.*(..)) !execution(* com.zhongge.controller.BookController.*(..)))// 匹配 BookController 或 UserController 中的方法Pointcut(execution(* com.zhongge.controller.BookController.*(..)) || execution(* com.zhongge.controller.UserController.*(..)))七、结语记住三句话execution按“地址”匹配适合有规律的方法同一个包、同一个类、同一个命名规则。annotation按“标签”匹配适合无规律的方法想增强哪个就加个注解。通配符*匹配一个..匹配多个组合使用威力无穷。掌握了切点表达式你就掌握了 AOP 的“瞄准镜”——想切哪里就能切到哪里下一篇预告我们将学习 Spring AOP 的底层原理——动态代理了解它是如何在运行时“偷偷”增强你的方法的。敬请期待如果觉得有用别忘了点赞、收藏、关注我们下期见

更多文章