ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AOP双配置实战:从切点表达式到注解驱动与XML完整解析

Spring AOP双配置实战:从切点表达式到注解驱动与XML完整解析 1. 为什么Spring AOP要同时留XML和注解两套方案先把话放前面上篇我们聊完了AOP的底层原理JDK动态代理和CGLIB到底怎么生成代理对象也搞清楚了切面、切点、通知三者之间的关系。但原理归原理落到实际项目里你手上的Spring版本可能还是XML时代的老项目也可能是Spring Boot这种注解驱动的新工程甚至可能两套写法混着来。所以这篇就把XML配置和注解驱动这两条路线完整过一遍从配置文件的标签含义到切点表达式的匹配逻辑再到切面优先级怎么控制全部讲透。在开始之前先给还没看过上篇的朋友补个上下文。Spring AOP本质上是Spring IoC容器的一个扩展能力它通过动态代理技术在不修改原有业务代码的前提下把横切逻辑插入到目标方法的执行链路上。所谓“横切逻辑”就是日志记录、权限校验、事务管理、性能监控、参数脱敏这类在很多方法里都会重复出现的公共逻辑。AOP解决的问题就是把这些公共逻辑从业务代码中抽出来让核心业务只关心自己的事公共逻辑统一交给切面去处理。Spring之所以保留两套配置方式背后是有历史原因的。最早的Spring版本只有XML一种配置方式所有Bean定义、依赖注入、AOP切面都写在XML文件里。后来Spring 2.0引入了基于Schema的aop命名空间XML配置AOP的能力才真正完善。再后来Spring 2.5开始支持注解配置Spring 3.0又强化了基于Java类的配置到了Spring Boot时代注解驱动成了绝对主流。但这并不意味着XML方式就废弃了——在很多遗留系统中XML配置仍然是主力而且有些场景下XML反而更好维护比如某些通用的切面需要跨模块复用通过XML集中配置比散落在各个类上更直观。我的建议很简单新项目无脑用注解驱动Spring Boot环境下代码简洁、自动配置省心老项目的维护和改造必须熟练阅读XML配置因为你要改的代码长什么样你说了不算。两种方式都掌握面试基本不会在这个环节翻车工作中也不会被老代码卡脖子。接下来我按两条线展开先讲切面本身怎么定义也就是切点表达式和通知类型的细节再分别演示XML配置和注解驱动各自的写法最后重点讲自调用失效、切面顺序控制这类高频踩坑点。2. 先啃核心切点表达式与五种通知类型2.1 切点表达式execution怎么把方法精确框住切点表达式是AOP里最需要下功夫的地方它决定了切面会作用到哪些方法上。最常用的是execution修饰符完整语法长这样execution(修饰符? 返回类型 类路径?方法名(参数) 异常?)拆开看问号表示可省略的部分。实际使用中我相信绝大多数人根本不会把修饰符和异常写全但返回类型和方法名是重点。举个例子execution(public * com.example.service.*.*(..))这个表达式的含义是匹配com.example.service包下所有类的所有方法返回类型任意参数任意但是必须带public修饰符。注意这里的*通配符覆盖了类名和方法名..表示任意参数列表包括零个参数。匹配单个类可以直接写类名路径execution(* com.example.service.UserServiceImpl.*(..))如果要匹配某个接口的所有实现类更稳妥的做法是匹配到接口路径execution(* com.example.service.UserService.*(..))因为AOP代理是基于接口时代理对象实现的还是接口方法切点如果精确到实现类的save方法某些情况下会因为方法签名不匹配导致代理失效。这一点后面细说。类名模糊匹配是重点难点。com.example.service.*只能匹配该包下的直接类不包含子包。要想连子包一起匹配execution(* com.example.service..*.*(..))注意..放在包路径里表示任意子包层级。匹配特定注解标注的方法也是高频需求语法是在表达式中加入annotationannotation(com.example.annotation.SensitiveField)意思是匹配所有标注了SensitiveField注解的方法这种写法配合自定义注解使用是做参数脱敏、接口幂等、日志埋点的利器。还有within、this、target、args、bean等其他切点指示符其中bean和args在实际项目中用得多些。bean(userServiceImpl)能精确匹配容器中名为userServiceImpl的Beanargs(String)匹配所有只有一个参数且参数类型为String的方法。组合切点表达式是躲不掉的功课。核心是三种逻辑运算表示且比如execution(* com.example.service..*.*(..)) annotation(log)意思是包路径满足条件、同时方法上还要有log注解才会触发通知。||表示或用来合并多个匹配规则。!表示非用来排除某些方法。举一个组合表达的完整例子Pointcut(execution(* com.example.service..*.*(..)) !execution(* com.example.service.config.*.*(..)) annotation(com.example.annotation.ApiLog)) public void pointcut() {}这段切点的含义是匹配com.example.service包及其子包下的所有方法排除了config子包的类并且方法上必须带ApiLog注解才生效。在实际工作中切点表达式写错是通知不执行最常见的根因。排查思路就是先简化表达式验证问题。比如先把表达式降级成execution(* *.*(..))看切面是否触发如果这时候触发了那就是原表达式匹配不正确如果还是触发不了就是切面本身注册有问题。2.2 五种通知类型执行时机决定一切通知类型一共有五种理解它们最直接的方法是把AOP通知看作埋在执行流程中的不同钩子。Before目标方法执行前执行。常用来做参数校验、权限检查、开启计时。AfterReturning目标方法正常返回后执行。可以用returning属性把返回值绑定到通知方法参数上对返回结果做二次加工。AfterThrowing目标方法抛出异常后执行。throwing属性绑定异常对象用于异常记录和预警。After目标方法无论正常返回还是抛出异常都会执行。相当于finally块常用于释放资源、统一收尾。Around最强通知类型掌握目标方法的整个执行过程。通知方法接收ProceedingJoinPoint参数手动调用proceed()才会进入目标方法这意味着你可以在调用前做预处理调用后做后处理也可以直接不调proceed()实现“假执行”或者限流降级。用代码直观展示每种通知的写法Aspect Component public class LogAspect { // 定义切点 Pointcut(execution(* com.example.service..*.*(..))) public void servicePointcut() {} // 前置通知执行前 Before(servicePointcut()) public void beforeAdvice(JoinPoint joinPoint) { System.out.println(开始执行方法: joinPoint.getSignature().getName()); } // 返回后通知拿到返回值 AfterReturning(value servicePointcut(), returning result) public void afterReturningAdvice(Object result) { System.out.println(方法正常返回: result); } // 抛出异常后通知 AfterThrowing(value servicePointcut(), throwing ex) public void afterThrowingAdvice(Exception ex) { System.err.println(方法抛出异常: ex.getMessage()); } // 最终通知相当于 finally After(servicePointcut()) public void afterAdvice() { System.out.println(方法执行完毕无论成功失败); } // 环绕通知最灵活 Around(servicePointcut()) public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println(环绕通知 - 方法调用前); long start System.currentTimeMillis(); Object result proceedingJoinPoint.proceed(); long cost System.currentTimeMillis() - start; System.out.println(环绕通知 - 方法调用后耗时: cost ms); return result; } }有一点必须强调Around通知里如果忘记调用proceed()目标方法根本不会执行而你的方法又没有返回值的话调用方拿到的就是null。这种问题排查起来最隐蔽因为日志和异常都不一定有提示。除了Around之外其余四种通知在应用中要区分好“逻辑往哪放”。比如做日志记录优先考虑Around做缓存用Around做幂等校验Before和Around都可以。2.3 切面执行顺序和JoinPoint参数获取多个切面作用于同一方法时执行顺序是个容易被忽视的点。在Spring中切面的顺序由Order注解控制数值越小优先级越高。数字小的切面在进入方法时先执行在方法返回时后执行可以类比成洋葱剥法外层切面先被“包裹”所以进入时先经过外层返回时最后经过外层。用代码说明一下Aspect Order(1) Component public class FirstAspect {} Aspect Order(2) Component public class SecondAspect {}这个配置下目标方法的执行链路是FirstAspect的前置逻辑 -SecondAspect的前置逻辑 - 目标方法 -SecondAspect的后置逻辑 -FirstAspect的后置逻辑。如果不写Order两个切面的执行顺序就无法保证同一个部署环境下可能稳定换环境就变了这是极度危险的事情。另一个重点问题通知方法中如何获取目标方法的信息。普通通知方法可以声明JoinPoint参数比如Before(servicePointcut()) public void beforeAdvice(JoinPoint joinPoint) { // 获取方法签名 Signature signature joinPoint.getSignature(); // 获取目标对象 Object target joinPoint.getTarget(); // 获取传入参数 Object[] args joinPoint.getArgs(); // 获取代理对象本身 Object thisObj joinPoint.getThis(); // 获取目标方法对象需要反射转一下 MethodSignature methodSignature (MethodSignature) signature; Method method methodSignature.getMethod(); }有两点经常有人搞混getTarget()拿到的是被代理的原始对象getThis()拿到的是代理对象本身。而在Around通知里参数类型是ProceedingJoinPoint它是JoinPoint的子接口所以JoinPoint提供的所有方法它都有同时多了proceed()方法。获取方法参数有一个常用技巧直接用getArgs()拿到的Object[]强转成具体类型但类型不匹配时容易抛异常。更稳的写法是先用MethodSignature拿到方法参数类型再逐个处理。3. 老项目最常碰到的XML配置AOP完整实战3.1 用XML定义切面、切点和通知XML配置AOP在Spring 2.0以后推荐使用aop命名空间核心配置集中在aop:config标签内部。先看一个完整的示例。添加依赖Spring 5.3.x版本下这样引入dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-aspects/artifactId version5.3.20/version /dependency切面类本身就是一个普通类不用加任何注解public class LogAspect { public void beforeLog(JoinPoint joinPoint) { System.out.println(XML前置通知方法 joinPoint.getSignature().getName()); } public void aroundLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println(XML环绕通知-开始); Object result proceedingJoinPoint.proceed(); System.out.println(XML环绕通知-结束); } }接下来是核心文件applicationContext.xml把切面声明成Bean再用aop:config把切面、切点、通知关联起来?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:aophttp://www.springframework.org/schema/aop xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd !-- 1. 注册切面类 -- bean idlogAspect classcom.example.aspect.LogAspect/ !-- 2. 目标业务对象 -- bean iduserService classcom.example.service.UserServiceImpl/ !-- 3. 配置AOP -- aop:config !-- 声明切点表达式id给下面通知引用 -- aop:pointcut idservicePointcut expressionexecution(* com.example.service..*.*(..))/ !-- 声明切面ref指向切面类Bean -- aop:aspect reflogAspect !-- 各种通知 -- aop:before methodbeforeLog pointcut-refservicePointcut/ aop:around methodaroundLog pointcut-refservicePointcut/ /aop:aspect /aop:config /beans这段XML要拆开理解才不容易出错。aop:pointcut可以在aop:config下声明作为全局切点也可以嵌套在aop:aspect内部声明作为切面私有的切点。全局切点可以被多个切面引用避免重复定义切面内嵌切点更适合只被当前切面使用的场景。aop:aspect的ref属性必须指向已经在Spring容器中注册的Bean这里的logAspect就是上面声明的切面类Bean。切面类里不需要加任何Aspect注解Spring会通过XML配置识别角色。再看通知的接参问题。XML配置下通知方法如果要接收参数有几种做法。通过pointcut中的参数绑定实现。切点表达式可以用args暴露方法参数然后编译进切点表达式aop:pointcut idservicePointcut expressionexecution(* com.example.service..*.*(..)) and args(param)/这里的and对应Java里的XML里会直接解析报错所以必须写成and这是XML配置AOP最容易踩的坑。args(param)的意思是把目标方法的第一个参数传给名为param的绑定变量然后在通知方法中声明同名参数接收public void beforeLog(String param) { System.out.println(拿到参数: param); }如果两个参数要绑定这样写expressionexecution(* com.example.service..*.*(..)) and args(param, userId)对应通知方法签名为beforeLog(String param, Long userId)参数顺序要和args中声明的顺序一致。3.2 XML方式下的常见坑命名空间、格式化、代理方式XML配置AOP虽然没有注解那么舒服但老项目维护绕不开。列举几个我自己维护老项目时踩过的具体坑。第一个坑idea社区版用户最头大的XML格式化问题。使用IDEA社区版时既没有Spring插件也没有企业版那些智能提示编辑XML文件特别容易出问题。具体表现是格式化XML时空格和换行被重新排布看起来是好事但XML里的表达式和文本节点一旦被重排某些版本下会出现解析异常或书写混乱。更搞笑的是很多人在社区版里格式化完XMLpointcut表达式的空格被吃掉导致表达式失效切面完全不生效。社区版里要让XML文件不格式化可以在File菜单的Settings里搜索Formatter然后在对应文件类型的Formatter控制里关闭自动格式化或者直接把该XML文件排除出格式化范围。一般我建议配置AOP的XML文件单独放一个目录格式化时手动跳过。第二个坑命名空间和Schema漏写。xmlns:aop和xsi:schemaLocation忘写或者写错版本Spring启动时直接报cvc-elt.1: 找不到元素aop:config的声明的错误。遇到这种报错不要慌检查命名空间是不是和Spring版本对得上Spring 5.x的XSD路径用spring-aop.xsd没问题老项目如果是Spring 3.x就要换对应的版本。第三个坑and和的替换。前面说了XML中不能直接写必须转义或写成and||对应or!对应not。这个语法很多人从注解切点表达式直接复制到XML里就报错根本原因就在这里。第四个坑代理目标类的问题。默认情况下Spring AOP用的是JDK动态代理代理出来的对象实现了接口但如果目标是类而非接口JDK动态代理就无能为力了。用CGLIB可以针对类代理配置方式是在XML里加aop:aspectj-autoproxy proxy-target-classtrue/加这行之后Spring会优先使用CGLIB创建子类代理代理对象可以强转成目标类类型。老项目里如果遇到“代理对象不能强转成目标类”的ClassCastException大概率就是这里没配置。3.3 XML配置AOP的应用场景老系统维护还是新项目降级方案现在写新项目理论上没人会从零开始用XML配置AOP。但有几个场景下XML依然是稳妥的选择或者说是必须的选择。Scene 1老系统存量代码。很多传统企业的Spring项目还是基于XML配置的所有Bean靠配置文件管理切面当然也在配置文件里维护。这种项目你不可能因为要用AOP就把全部Bean改成注解最合理的做法就是在已有XML体系里补AOP配置。Scene 2基础组件发布和复用。如果你在维护一个公共的权限处理组件、日志组件通过XML的aop:config可以把切面配置外置到专门的XML文件里调用方只需import这份配置不需要在代码上做任何改动。对于框架开发者来说这是很优雅的侵入方式。Scene 3动态修改切面规则。注解方式的切点表达式是编译期写死在代码里的要改必须改代码重新发布。XML方式下可以通过外部化配置文件随时调整切点表达式和通知逻辑对运维体系友好的场景里这种方式反而更灵活。4. 今天的默认选项注解驱动AOP落地指南4.1 基于Aspect、Pointcut、Before等注解的最小可运行配置注解驱动是Spring Boot时代最常用的AOP方案。一个最小的注解驱动的切面配置需要三步。第一步在Spring Boot项目的启动类或者任意配置类上加EnableAspectJAutoProxy开启AOP支持。Spring Boot的自动配置环境下其实你只要在classpath里引入了spring-boot-starter-aopEnableAspectJAutoProxy会由自动配置类自动开启不写也行。但为了明确性和可控性我习惯显式加一下尤其是非Spring Boot的普通Spring MVC项目这个注解是必不可少的。第二步编写切面类用Aspect标注同时注册成Spring BeanAspect Component public class ApiLogAspect { Pointcut(annotation(com.example.annotation.ApiLog)) public void apiLogPointcut() {} Around(apiLogPointcut()) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature methodSignature (MethodSignature) joinPoint.getSignature(); ApiLog apiLog methodSignature.getMethod().getAnnotation(ApiLog.class); // 记录入参 Object[] args joinPoint.getArgs(); Object result null; try { result joinPoint.proceed(); return result; } finally { // 记录出参和异常 } } }第三步在目标方法上加ApiLog注解ApiLog PostMapping(/user/save) public Result saveUser(RequestBody User user) { return userService.save(user); }Aspect和Component都缺一不可。Aspect告诉Spring这是一个切面Component把它交给容器管理。如果只在方法上标注了Aspect而没有Component容器里等于没有这个Bean切面不生效如果只有Component没有Aspect那它就是个普通Bean切面同样不生效。有两点值得单独拿出来说。第一在Spring Boot中引入依赖时要注意版本兼容Spring Boot 3.x基于Spring Framework 6.xAOP相关依赖会自动带入不用额外手动引入。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency第二EnableAspectJAutoProxy的proxyTargetClass属性对应XML配置里的proxy-target-class默认值是false。如果你希望强制用CGLIB代理可以设为trueSpring Boot 2.x以后默认支持CGLIB。4.2 自调用导致通知失效最坑的一个问题注解驱动下有一个非常隐蔽的坑那就是自调用。看一下这个例子Service public class UserServiceImpl implements UserService { Override ApiLog public void saveUser(User user) { // ... } Override public void batchSave(ListUser users) { // 同一个类中直接调用 this.saveUser(user) this.saveUser(user); } }如果直接调用batchSavesaveUser上的ApiLog注解根本不会触发。原因是Spring AOP代理的本质是生成代理对象外部通过代理对象调用方法时切面生效而类内部this.saveUser(user)直接调用的是原始对象绕过了代理对象。这个问题俗称“自调用失效”。解决这个问题最常用的方法是把内部调用改为通过代理对象调用。做法是在当前类中注入自身的代理对象Service public class UserServiceImpl implements UserService { Autowired private UserService self; public void batchSave(ListUser users) { self.saveUser(user); } }注意这里注入的是UserService接口类型注入到的是代理对象而不是原始对象。Spring容器中注入的都是代理后的Bean因此self是切面生效的代理对象。另一个办法是把被调用的方法拆到另一个Bean里通过另一个Bean的方法来调用Service public class UserServiceImpl implements UserService { Autowired private UserServiceProxyHolder holder; public void batchSave(ListUser users) { holder.saveUser(user); } } Service public class UserServiceProxyHolder { ApiLog public void saveUser(User user) { // 真正的方法逻辑 } }第三个办法是用AopContext.currentProxy()强制获取当前代理对象但需要在启动类配置EnableAspectJAutoProxy(exposeProxy true)public void batchSave(User user) { ((UserService) AopContext.currentProxy()).saveUser(user); }这个方法看起来简洁但exposeProxy true会让代理对象暴露到线程本地生产环境有多线程并发时需要特别注意一般不建议默认开启。4.3 自定义注解AOP的实战示例参数脱敏注解驱动一个非常典型的实战场景就是基于自定义注解做参数脱敏。这套方案能彻底解决这样一个问题接口日志或者核心数据入库的时候敏感字段如手机号、身份证号、银行卡号不能明文存储和打印。先定义一个脱敏注解Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface SensitiveField { String value() default ; }给实体字段打上这个注解public class User { SensitiveField(phone) private String phone; SensitiveField(idCard) private String idCard; }然后写一个切面把所有标注了SensitiveField注解字段的对象统一做脱敏处理。这里切点用annotation配合自定义注解比用execution匹配包路径更精准// 定义脱敏注解作为切点标志 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Desensitized {} // 切面凡是方法上标注了 Desensitized就对返回值中的敏感字段脱敏 Aspect Component public class DesensitizationAspect { Around(annotation(com.example.annotation.Desensitized)) public Object desensitize(ProceedingJoinPoint joinPoint) throws Throwable { Object result joinPoint.proceed(); if (result ! null) { handleDesensitization(result); } return result; } private void handleDesensitization(Object obj) { if (obj null) { return; } // 处理集合 if (obj instanceof Collection) { ((Collection?) obj).forEach(this::handleDesensitization); return; } // 遍历字段 Field[] fields obj.getClass().getDeclaredFields(); for (Field field : fields) { SensitiveField annotation field.getAnnotation(SensitiveField.class); if (annotation ! null) { field.setAccessible(true); try { Object value field.get(obj); if (value instanceof String) { String original (String) value; field.set(obj, mask(original)); } } catch (IllegalAccessException e) { // 异常处理 } } } } private String mask(String original) { if (original null || original.length() 3) { return original; } int middleStart Math.max(1, original.length() / 3); int middleEnd Math.max(3, original.length() * 2 / 3); StringBuilder sb new StringBuilder(original); for (int i middleStart; i middleEnd i original.length(); i) { sb.setCharAt(i, *); } return sb.toString(); } }使用的时候只要在Controller的方法上标注Desensitized返回的User对象里手机号和身份证号就会被自动打码Desensitized GetMapping(/user/{id}) public User getUser(PathVariable Long id) { return userService.getUserById(id); }这个示例的价值在于它把自定义注解、切入表达式、环绕通知、反射处理整合到了一起是注解驱动AOP非常经典的落地方式。顺带一提sneakythrows注解在AOP切面里也很好用它代替了在切面方法中声明大量throws Throwable的样板代码。用上排列组合时切面里各种反射操作和proceed()调用都可以直接甩给sneakyThrows去处理代码看起来清爽多了。4.4 事务注解和AOP的关系事务管理是最早、也是最多被问到的AOP实际应用。Spring的声明式事务Transactional本质上就是AOP进入方法前开启事务方法正常返回后提交事务方法抛出运行时异常时回滚事务。有些经验不深的人会把事务和AOP当成两个独立的知识点面试一深挖才发现这其实是一条线上的东西。这里有个很重要的执行顺序问题当自定义切面和事务切面同时作用在同一个方法上时哪个切面在外层决定了你做的某些业务处理到底在事务提交前还是提交后执行。最典型的例子就是日志记录要带事务状态。假如你想做一个操作日志切面在Transactional方法执行后记录一条日志如果日志插入操作在事务内部执行方法随后抛出异常回滚日志也会跟着回滚。很多场景下日志丢失就是回滚造成的。Spring切面顺序默认靠Order控制Spring事务切面的顺序默认是Ordered.LOWEST_PRECEDENCE也就是最低优先级这意味着事务切面通常处于最内层。自定义切面如果不指定Order默认也是最低优先级顺序很难预料。要让自定义切面的事务提交之后记录日志需要把切面的Order设为比事务切面更低的数字也就是更外层的包裹顺序。具体来说可以把操作日志切面设为Order(100)让它在事务切面之前被包裹这样proceed()执行链路是日志切面外部 - 事务切面 - 真正方法。但日志记录若要读到事务提交状态还得设计成异步或二段提交这是事务AOP联动的尖端玩法在实际业务中有很多落地点。5. 避坑实录与问题排查速查手册5.1 通知不生效八层检查梳理通知不执行是所有人的初学噩梦也是最常被问到的问题。根据我多次排查的经验按下面顺序逐项检查基本能定位。第一层切面类是否被Spring扫描到。Aspect标注了但类没有注册成Bean切面不会出现在容器中。检查方式启动日志中看Spring是否打印了这个Bean相关的初始化信息没有打印基本就是没被扫描到。第二层EnableAspectJAutoProxy是否开启。非Spring Boot项目容易漏这个注解一旦漏掉切面类虽然注册成了Bean但切点识别机制没打开。Spring Boot项目则检查spring-boot-starter-aop依赖有没有引入。第三层切点表达式是否匹配目标方法。把表达式逐渐简化比如先用execution(* *.*(..))通配所有方法逐步缩小范围测试是哪个条件导致匹配失败这是排查表达式的最佳实践。第四层目标方法是否有public修饰。Spring AOP默认只代理public方法protected和private方法不会走代理逻辑。这在上篇原理里说过JDK动态代理和CGLIB的接口方法/继承方法限制就决定了非public方法进不了通知链路。第五层目标对象和代理对象是否正确获取。从容器中拿到的是代理对象时切面才生效直接new一个对象调方法肯定不行。使用Autowired注入时Spring容器默认注入的是代理对象手动new出来的对象则没有AOP包装。第六层自调用问题。之前单独讲过this.xxx()绕过代理属于方法论层面的经典坑。第七层切面顺序和多个切面的相互干扰。多个切面时内层切面抛出的异常会被外层切面捕获并处理可能导致你预期中会触发的外层AfterThrowing没有触发反而由Around的异常处理逻辑接管。这种问题要结合日志链路综合判断。第八层代理方式问题。目标是类而非接口时确保CGLIB代理开启。检查方式是打印代理对象的class如果是com.sun.proxy.$Proxy开头的那是JDK动态代理如果是类名带$$EnhancerBySpringCGLIB那是CGLIB代理。5.2 五大高频报错与解决套路把实际工作中碰到最多的几个报错拿出来当速查表用报错信息问题根源解决方案NoSuchMethodException或IllegalArgumentException通知方法签名与切点绑定参数不匹配检查通知方法参数名和个数是否与切点表达式中绑定变量一致ClassCastException: $Proxy... cannot be cast to XxxServiceImplJDK动态代理下强转目标实现类开启CGLIB代理或在代码内避免强转代理为具体实现类EmptyPointcut或AOP configuration seems to be not activeEnableAspectJAutoProxy未开启或XML的aop命名空间没加载检查配置类注解或XML Schema位置事务不生效日志里有Self-invocation迹象事务方法自调用绕过代理用Transactional的方法通过代理对象调用或拆分到另一个BeanXML中解析报错Attribute value is invalidXML字符转义问题写成and5.3 调试切面生效与否的几个实用技巧搭个小项目来验证AOP没有太多技术难度但调试的效率和技巧是长期积累的。我总结几个实战技巧都试过有效。技巧一打印代理类名称。在调用目标方法的地方把userService.getClass().getName()打出来能看到代理类名就能确认代理是否生效JDK代理以$Proxy开头CGLIB代理会在类名后追加$$EnhancerBySpringCGLIB。这个方法在排查代理方式问题时极高效。技巧二在切面里加方法调用栈日志。通过Thread.currentThread().getStackTrace()打印调用链确认目标方法是从代理入口进来的还是被内部直接调用绕过。自调用问题能靠这个一眼定位。技巧三利用单元测试覆盖AOP场景。写一个Spring Boot测试直接注入代理对象并调用方法然后断言日志输出或返回值是否被切面处理。测试通过说明切面逻辑正确测试失败再对照上面的八层检查逐项排查。技巧四关闭某些切面做对比实验。排查多个切面互相干扰时可以临时用一个Order控制切面的启停比如把切面顺序调整成极端值或者先注释掉其中一个切面的Around看看目标方法的行为变化由此定位到具体是哪个切面干的。收尾两条路线怎么选以及上篇没来得及说的补充最后分享一点我在实际项目里体会最深的事。很多初学者纠结“到底是学XML还是学注解”其实工作几年之后你会意识到这两者不是选择题而是必答题。Spring框架的功能随着版本演进不断丰富但AOP的底层原理和设计思想没有变过。你现在花时间把注解驱动用得很熟练对自己梳理业务逻辑、做公共组件非常有帮助而XML配置的老项目也不会因为你艺高人胆大就不存在接手老系统的第一周基本都耗在读XML上。我个人更推荐的学习路径是先用注解驱动把切面实战中的所有细节吃透因为注解方式的代码量少、调试链路短能帮你快速建立“切点-通知-代理”的心智模型然后把XML配置作为补充技能重点练会怎么读懂老项目的aop:config以及怎么在不改动业务代码的前提下追加一个新切面。最后再补充两个小技巧。第一在Idea里写自定义注解时和Spring的注解自动联想有细微差别如果发现注解的候选提示不出现检查一下是否被Idea标记成了纯文本文件或语言注入异常。第二proxy-target-class这个配置在Spring Boot 2.x之后默认值已经是true了不必再专门设置但在手动搭建的Spring框架项目里这个坑依然存在。通关秘籍到这篇就结束了上篇的原理下篇的配置都摸过一遍剩下就是多写多测多踩坑。祝你在切面编程的路上代码少出bug事务回滚少一点监控日志齐全一点。
返回列表