
本文介绍一种基于抽象 aspect 的设计模式,使已打包发布的日志切面(jar)支持被下游项目灵活定制包级切入点,无需修改原始字节码或重新编译切面模块。
本文介绍一种基于抽象 aspect 的设计模式,使已打包发布的日志切面(jar)支持被下游项目灵活定制包级切入点,无需修改原始字节码或重新编译切面模块。
在使用 AspectJ 进行横切关注点(如统一日志、监控)开发时,一个常见需求是:将核心切面逻辑封装为可复用的 Maven 依赖(如 aspectj-tool.jar),供多个业务项目直接引入;同时允许各业务方按需指定「哪些包下的方法需要被织入」——即动态变更 @Pointcut 表达式。然而,Java 规范要求注解属性必须是编译期常量,因此无法通过配置参数、系统属性或 Spring Bean 属性等方式,在运行时或编译时动态注入切入点字符串。这不是 AspectJ 的限制,而是 JVM 层面的语言约束。
解决方案是采用 面向切面的继承建模(Aspect Inheritance):将核心切面定义为抽象类,并将需定制的切入点声明为抽象方法。这样,原始切面 JAR 中仅提供通用增强逻辑(如参数记录、异常捕获、耗时统计),而具体的织入范围则交由下游项目通过子类实现。
✅ 正确实践:抽象切面 + 具体子类
首先,在基础切面模块中定义抽象 @Aspect 类:
package com.stellariver.milky.aspectj.tool.log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
public abstract class AutoLogAspect {
private static final Logger log = LoggerFactory.getLogger(AutoLogAspect.class);
// 【1】注解驱动的切入点(固定逻辑)
@Pointcut("@annotation(com.stellariver.milky.aspectj.tool.log.Log)")
public abstract void annotationPointcut();
// 【2】包路径驱动的切入点(由子类实现)
@Pointcut("")
public abstract void packagePointcut();
// 【3】组合两个切入点,统一处理
@Pointcut("annotationPointcut() || packagePointcut()")
public void combinedPointcut() {}
@Around("combinedPointcut()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String signature = pjp.getSignature().toShortString();
try {
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
log.info("[success:true] [cost:{}ms] [result:{}]", cost, result);
return result;
} catch (Throwable t) {
long cost = System.currentTimeMillis() - start;
log.error("[success:false] [cost:{}ms] [exception:{}]", cost, t.getClass().getSimpleName(), t);
throw t;
}
}
}
⚠️ 注意:@Pointcut("") 是合法的占位写法,其实际表达式将在子类中被重写覆盖;AspectJ 编译器会识别该方法签名并绑定子类的具体实现。
然后,在业务项目中创建具体子类,覆盖 packagePointcut() 方法以声明目标包范围:
package com.example.log.demo;
import com.stellariver.milky.aspectj.tool.log.AutoLogAspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyLogAspect extends AutoLogAspect {
// ✅ 动态指定:所有 com.example.log.demo 包及其子包下的任意方法
@Pointcut("within(com.example.log.demo..*) && execution(* *(..))")
@Override
public void packagePointcut() {}
}
此时,业务方只需:
- 引入 aspectj-tool 依赖;
- 编写一个继承自 AutoLogAspect 的具体切面类;
- 在 pom.xml 中确保 aspectj-maven-plugin 启用(CTW 模式);
即可完成无侵入式织入 —— 无需修改原始 JAR,也不需要复制粘贴整段切面代码。
? 补充说明:LTW 场景下的替代方案
若业务项目采用加载时织入(Load-Time Weaving),还可完全跳过源码继承,直接在 src/main/resources/META-INF/aop.xml 中声明:
<aspectj><aspects><aspect name="com.stellariver.milky.aspectj.tool.log.AutoLogAspect"></aspect><!-- 子类化切面,但此处不写 Java 类,而是用 XML 显式绑定 pointcut --><concrete-aspect name="com.example.log.demo.MyLogAspect" extends="com.stellariver.milky.aspectj.tool.log.AutoLogAspect"><pointcut name="packagePointcut" expression="within(com.example.log.demo..*) && execution(* *(..))"></pointcut></concrete-aspect></aspects></aspectj>
此方式甚至无需编写 Java 子类,适合对编译流程控制较弱的环境(如遗留系统集成)。
✅ 总结
| 方式 | 是否需编译子类 | 是否需修改原始 JAR | 灵活性 | 推荐场景 |
|---|---|---|---|---|
| 抽象 Aspect 继承(CTW) | ✅ 是 | ❌ 否 | 高 | 主流 Maven 多模块项目 |
| aop.xml concrete-aspect(LTW) | ❌ 否 | ❌ 否 | 中高 | 容器托管、Spring Boot 应用、灰度发布 |
最终,这种“契约式抽象 + 实现解耦”的设计,既保障了切面能力的标准化分发,又赋予了业务侧充分的织入策略自主权,是构建企业级 AOP 基础设施的最佳实践之一。










