4.2 通知类型与切入点表达式


4.2 通知类型与切入点表达式

本节摘要:切面 = 通知(做什么)+ 切入点(在哪做)。本节给出五种通知的执行顺序、切入点表达式的常用写法,以及一个可直接运行的慢调用监控切面;顺带说明 @annotation 与包路径两种定位方式的选择。

五种通知与顺序

通知是切面在特定时机执行的动作,共五种:前置、后置、返回、异常、环绕。直接上代码看全貌:

@Aspect @Component public class SlowCallAspect { private static final Logger log = LoggerFactory.getLogger(SlowCallAspect.class); @Pointcut("within(com.example.orders..*)") public void orderLayer() {} @Around("orderLayer()") public Object time(ProceedingJoinPoint pjp) throws Throwable { long t = System.nanoTime(); try { return pjp.proceed(); } finally { long ms = (System.nanoTime() - t) / 1_000_000; if (ms > 300) { log.warn("慢调用 {} 耗时 {} 毫秒", pjp.getSignature().toShortString(), ms); } } } @AfterThrowing(pointcut = "orderLayer()", throwing = "ex") public void onError(JoinPoint jp, Exception ex) { log.error("调用 {} 失败", jp.getSignature().toShortString(), ex); } }

五种通知在一次调用中的顺序是:环绕进入 → 前置 → 目标方法 → (正常时)返回、后置 → 环绕退出;异常时则是异常、后置。环绕是能力最全的通知——能改参数、能改返回值、能吞异常、能量耗时,日常工程里一个环绕往往顶替其余四个的组合。新版本对同类中通知的默认顺序有调整,不同切面之间用 @Order 显式声明,别依赖默认。

切入点:在哪织

表达式回答"拦谁"。工程里最常用的四种写法:

写法 含义 适用
按执行方法匹配 匹配方法签名模式 精确到方法层
按类型所在包匹配 within 开头加包模式 整层拦截,上面示例用的就是它
按注解匹配 加在方法上的注解 业务代码打标,切面认标
按 Bean 名匹配 按容器内 Bean 名字 少用,改名易断

我更推荐注解驱动的切入点:定义一个语义化注解,切面拦注解,业务方法打标——切面与包结构解耦,移动类不会悄悄脱离监控:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuditLog { String value() default ""; } @Around("@annotation(audit)") public Object audit(ProceedingJoinPoint pjp, AuditLog audit) throws Throwable { log.info("审计:{} 操作 {}", pjp.getSignature().toShortString(), audit.value()); return pjp.proceed(); }

业务方法上加一行 @AuditLog("创建订单") 即纳入审计,不加则完全不受影响,团队成员读注解名就能懂。

图 4-2 一次调用中五种通知的排布

图 4-2 一次调用中五种通知的排布

⚠️ 切面里不要吞异常再返回空值:调用方以为成功,数据却没落库。确需转换异常时,抛出带上下文的新异常,保留原因链。

实战:上线前的慢接口清单是怎么来的

背景:性能治理第一轮要一份"超过三百毫秒的接口清单",人工翻代码估不靠谱,需要实测数据。操作:把上文慢调用监控切面稍作升级——按方法签名聚合次数与总耗时,定期输出汇总:

@Aspect @Component public class SlowCallStatsAspect { private final Map<String, LongAdder> counts = new ConcurrentHashMap<>(); private final Map<String, LongAdder> totals = new ConcurrentHashMap<>(); @Around("within(com.example..*)") public Object stat(ProceedingJoinPoint pjp) throws Throwable { String key = pjp.getSignature().toShortString(); long t = System.nanoTime(); try { return pjp.proceed(); } finally { counts.computeIfAbsent(key, k -> new LongAdder()).increment(); totals.computeIfAbsent(key, k -> new LongAdder()) .add((System.nanoTime() - t) / 1_000_000); } } @Scheduled(fixedDelay = 60_000) // 每分钟输出一次榜单 public void report() { counts.keySet().stream() .sorted(Comparator.comparingLong( k -> -totals.get(k).sum()).limit(10)) .forEach(k -> System.out.printf("%s 次数%d 总耗时%d毫秒%n", k, counts.get(k).sum(), totals.get(k).sum())); } }

操作步骤:部署到预发环境,用回放流量跑半小时,收集输出的榜单。结果:得到一张按总耗时排序的十行清单,榜首通常是两三个聚合查询接口。解读:环绕通知在方法边界计时,量到的是完整业务耗时(含内部数据库往返),与第 1 章实验场里过滤器层的全程耗时相互印证,两层相减还能估出"分发与拦截"本身的开销。变式:把统计对象换成注解驱动(只统计带审计注解的方法),一份切面就同时服务了性能与合规两个诉求。

本节要点回顾

  • 切面 = 通知 + 切入点,环绕通知能力最全
  • 后置两种路径都执行,清理逻辑放这里
  • 注解驱动的切入点比包路径更抗重构
  • 切面禁吞异常,转抛要带原因链

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U