切面的三要素
写一个切面只需要三样:切点(在哪儿织)、通知(织什么)、织入(谁织)。前两个是写代码的事,第三个是 Spring 的事——上篇的代理在这里接上。
切点表达式:圈住目标
@Pointcut("execution(* com.demo.service..*.*(..))")
public void serviceLayer() {}
// * 任意返回值
// .. 包及其子包
// * 任意类 .* 任意方法 (..) 任意参数
@Pointcut("@annotation(com.demo.anno.AuditLog)")
public void auditLog() {} // 标了 @AuditLog 注解的方法execution 按位置记忆:修饰符(可省)、返回值、包.类.方法、参数。业务里更推荐 @annotation 注解切点——想审计谁就标个注解,不用为每个包维护表达式,意图显式、改动局部化。
五种通知,一个万能
@Before 前置、@After 后置(finally 语义)、@AfterReturning 正常返回后、@AfterThrowing 抛异常后、@Around 环绕。Around 是唯一全能的:能改参数、改返回值、吞异常、控制目标是否执行。业务切面默认用它,其余四种按语义需要选用。
织入:切面怎么变成代理
把原理串回第 2、4 篇:@EnableAspectJAutoProxy 导入 AnnotationAwareAspectJAutoProxyCreator——正是第 4 篇四大金刚里 AbstractAutoProxyCreator 的子类。每个 Bean 走到生命周期第 7 步(初始化后)时它出面:扫描全部切面的 Advisor、按切点匹配当前 Bean——匹配上就用 JDK 或 CGLIB 生成代理,把切面逻辑翻译成一条拦截器链塞进代理。运行时调方法,链上通知依次推进,@Around 的 proceed() 就是「把链推进到下一个」。所以织入不是编译期魔法,而是启动期把 Bean 换成代理、运行期走拦截器链。声明式事务也是同一套机器——TransactionInterceptor 就是一个通知,第 8 篇展开。
实操:注解审计切面
@Aspect
@Component
public class AuditAspect {
@Around("@annotation(auditLog)") // 直接绑定注解实例
public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
log(auditLog.value(), true, System.currentTimeMillis() - start);
return result;
} catch (Throwable e) {
log(auditLog.value(), false, System.currentTimeMillis() - start);
throw e; // 务必原样抛出,别吞
}
}
}两个提醒:catch 里务必原样 throw——吞了异常,业务方一脸懵,事务也跟着失明;多个切面有先后要求时用 @Order,值小的优先级高。
AOP 的机器拆完了。下一篇看它最重要的应用:声明式事务——@Transactional 为什么必须靠 AOP 加 ThreadLocal 才能转起来。
评论 (0)