
本文介绍一种基于抽象 aspect 的优雅方案:将切点声明为抽象方法,使下游项目可通过继承并重写具体切点来灵活适配包路径,无需修改原始 jar 或重新编译切面逻辑。
本文介绍一种基于抽象 aspect 的优雅方案:将切点声明为抽象方法,使下游项目可通过继承并重写具体切点来灵活适配包路径,无需修改原始 jar 或重新编译切面逻辑。
在使用 AspectJ 进行横切关注点(如统一日志)开发时,一个常见需求是:将通用切面逻辑打包为可复用的 Maven 依赖(如 log-aspect.jar),供多个业务项目直接引用;同时,又希望各业务方能按需自定义“哪些包下的方法需要被织入”,而非硬编码在原始切面中。然而,Java 注解值必须是编译期常量,因此无法通过配置参数动态修改 @Pointcut 字符串——这是语言层面的限制,与 AspectJ 本身无关。
解决该问题的核心思路是面向抽象编程:将切面类声明为 abstract,并将需定制的切点定义为抽象方法。这样,原始 jar 中仅提供切面骨架与核心逻辑(如日志格式、异常捕获、耗时统计),而具体的织入范围则交由下游项目实现。
✅ 正确实践:抽象切面 + 具体继承
首先,在基础切面模块中定义抽象 Aspect:
@Aspect
public abstract class AutoLogAspect {
private static final Logger log = LoggerFactory.getLogger(AutoLogAspect.class);
// 抽象切点:由子类实现,决定包级织入范围
@Pointcut("")
public abstract void packagePC();
// 注解驱动切点(保持不变)
@Pointcut("execution(@com.stellariver.milky.aspectj.tool.log.Log * *(..))")
public void annotationPC() {}
// 组合切点:支持两种触发方式
@Pointcut("annotationPC() || packagePC()")
public void loggingPointcut() {}
@Around("loggingPointcut()")
public Object logExecution(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String methodName = pjp.getSignature().toShortString();
Object[] args = pjp.getArgs();
try {
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
log.info("=====msg: execution({})\n" +
"==header: {} [success:true] [cost:{}] [result:{}]\n" +
"====args: {}",
methodName,
LocalDateTime.now(), cost, result,
Arrays.toString(args));
return result;
} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;
log.error("=====msg: execution({})\n" +
"==header: {} [success:false] [cost:{}] [exception:{}]\n" +
"====args: {}",
methodName,
LocalDateTime.now(), cost, e.getClass().getSimpleName(),
Arrays.toString(args));
throw e;
}
}
}
⚠️ 注意:@Pointcut("") 中的空字符串是合法占位符,仅表示该方法将被子类重写为有效切点表达式;编译器不会校验其内容,实际织入由子类实现决定。
然后,在业务项目中继承该抽象类,并提供具体的包级切点:
package com.example.myapp.aspect;
import com.stellariver.milky.aspectj.tool.log.AutoLogAspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyAppLogAspect extends AutoLogAspect {
// ✅ 下游项目自由指定包路径,支持通配符和组合逻辑
@Pointcut("within(com.example.myapp.service..*) && execution(* *(..))")
public void packagePC() {}
}
此时,只要确保 MyAppLogAspect 被 AspectJ 编译器识别(例如通过 aspectj-maven-plugin 配置),所有 com.example.myapp.service 包及其子包下的方法调用,都会自动触发日志逻辑——无需添加 @Log 注解,也无需改动原始切面代码。
? 构建与部署说明
- 基础模块(jar):仅包含 AutoLogAspect 及其依赖,发布至私有 Maven 仓库;
- 业务模块:引入上述 jar,并编写 MyAppLogAspect(建议放在 src/main/java 下,参与 CTW 编译);
-
Maven 插件配置示例:
<plugin><groupid>org.codehaus.mojo</groupid><artifactid>aspectj-maven-plugin</artifactid><version>1.14.0</version><configuration><compliancelevel>17</compliancelevel><source>17</source><target>17</target><weavedependencies><weavedependency><groupid>com.stellariver.milky</groupid><artifactid>log-aspect</artifactid></weavedependency></weavedependencies></configuration><executions><execution><goals><goal>compile</goal></goals></execution></executions></plugin>
? 补充方案:运行时定制(LTW)
若业务项目倾向使用加载时织入(LTW),还可完全跳过编译步骤:在 src/main/resources/META-INF/aop.xml 中直接声明具体切点:
<aspectj><aspects><aspect name="com.stellariver.milky.aspectj.tool.log.AutoLogAspect"></aspect></aspects><weaver options="-verbose -showWeaveInfo"><include within="com.example.myapp.service..*"></include><!-- 可配合 aop.xml 的 <pointcut> 标签进一步细化 --></weaver></aspectj>
此时,甚至无需编写 Java 子类,仅靠 XML 即可完成织入范围配置(需启用 -javaagent:aspectjweaver.jar)。
✅ 总结
- ❌ 不要尝试在注解中传入动态字符串(Java 语法不支持);
- ✅ 推荐抽象切点 + 继承重写模式,兼顾复用性与灵活性;
- ✅ 切点组合(&&, ||, !)和 within() 等语义可在子类中自由发挥;
- ✅ CTW 和 LTW 均适用,选择取决于团队构建规范与运维约束;
- ✅ 所有日志格式、上下文提取、异常处理等通用逻辑,集中维护于抽象父类,保障一致性与可维护性。










