1. 概述
本文将介绍 jcabi-aspects 这个 Java 库,它是一组基于面向切面编程(AOP)的实用注解,能显著简化常见横切逻辑的实现。
通过 @Async、*@Loggable、@RetryOnFailure* 等注解,你可以轻松实现异步执行、日志记录、失败重试等能力,避免大量模板代码。这些注解的背后是 AspectJ 实现的字节码增强,因此项目需要集成 AspectJ 来完成编译期织入(weaving)。
⚠️ 注意:jcabi-aspects 依赖 AspectJ 编译织入,不是运行时代理,这点和 Spring AOP 不同。
2. 环境搭建
首先,在 pom.xml
中引入 jcabi-aspects 核心依赖:
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-aspects</artifactId>
<version>0.26.0</version>
</dependency>
由于底层使用 AspectJ,还需添加运行时依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.20.1</version>
<scope>runtime</scope>
</dependency>
最关键的是,使用 jcabi-maven-plugin 在编译阶段自动织入切面:
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<version>0.14.1</version>
<executions>
<execution>
<goals>
<goal>ajc</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.20.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.20.1</version>
</dependency>
</dependencies>
</plugin>
最后执行:
mvn clean package
构建日志中会看到类似输出,表示织入成功:
[INFO] --- jcabi-maven-plugin:0.14.1:ajc (default) @ jcabi ---
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of
@Loggable annotated methods
[INFO] Unwoven classes will be copied to /jcabi/target/unwoven
[INFO] Created temp dir /jcabi/target/jcabi-ajc
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated
cleaning of expired @Cacheable values
[INFO] ajc result: 11 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)
✅ 踩坑提示:如果没看到 ajc result
,说明织入失败,检查插件是否正确配置或版本兼容性。
3. @Async:异步执行
@Async 注解可将方法标记为异步执行,但有严格限制:
- ✅ 方法返回类型必须是
void
或Future<T>
- ❌ 否则运行时会抛出异常
示例:异步打印阶乘
@Async
public static void displayFactorial(int number) {
long result = factorial(number);
System.out.println(result);
}
构建后运行,会看到日志:
[main] INFO com.jcabi.aspects.aj.NamedThreads -
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution
这说明库已创建名为 jcabi-async
的守护线程池来处理异步任务。
返回 Future
的写法:
@Async
public static Future<Long> getFactorial(int number) {
Future<Long> factorialFuture = CompletableFuture.supplyAsync(() -> factorial(number));
return factorialFuture;
}
⚠️ 注意:
@Async
使用的是内部线程池,无法自定义,适合轻量级异步任务。
4. @Cacheable:方法结果缓存
@Cacheable 用于缓存方法返回值,避免重复计算或远程调用。
示例:缓存汇率数据 2 秒
@Cacheable(lifetime = 2, unit = TimeUnit.SECONDS)
public static String cacheExchangeRates() {
String result = null;
try {
URL exchangeRateUrl = new URL("https://api.exchangeratesapi.io/latest");
URLConnection con = exchangeRateUrl.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
result = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
也可永久缓存:
@Cacheable(forever = true)
首次调用日志:
[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates():
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
cached in 560ms, valid for 2s
2 秒内再次调用,命中缓存:
[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates():
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
from cache (hit #1, 563ms old)
✅ 重要规则:
- 方法抛异常时,结果不会被缓存
- 库内部启动了两个守护线程:
jcabi-cacheable-clean
(清理过期项)和jcabi-cacheable-update
(异步刷新)
5. @Loggable:方法执行日志
@Loggable 基于 SLF4J 自动记录方法执行信息,无需手动写 log.info()
。
给方法加上注解:
@Loggable
@Async
public static void displayFactorial(int number) {
// ...
}
@Loggable
@Cacheable(lifetime = 2, unit = TimeUnit.SECONDS)
public static String cacheExchangeRates() {
// ...
}
调用后自动输出:
[main] INFO com.baeldung.jcabi.JcabiAspectJ - #displayFactorial(): in 1.16ms
[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates():
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
in 556.92ms
内容包括:方法名、返回值(非 void)、执行耗时。
6. @LogExceptions:异常日志
@LogExceptions 仅在方法抛出异常时记录日志,正常执行不输出。
示例:
@LogExceptions
public static void divideByZero() {
int x = 1/0;
}
执行结果:
[main] WARN com.baeldung.jcabi.JcabiAspectJ - java.lang.ArithmeticException: / by zero
at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)
java.lang.ArithmeticException: / by zero
at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)
...
注意:异常仍会被向上抛出。
7. @Quietly:静默处理异常
@Quietly 和 @LogExceptions 类似,但关键区别是:
- ✅ 异常被“吞掉”,不会抛出
- ✅ 只记录日志
@Quietly
public static void divideByZero() {
int x = 1/0;
}
输出日志但程序不中断:
[main] WARN com.baeldung.jcabi.JcabiAspectJ - java.lang.ArithmeticException: / by zero
at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)
⚠️ 限制:仅适用于 void
返回类型的方法。
8. @RetryOnFailure:失败自动重试
@RetryOnFailure 在方法抛异常时自动重试,适合处理瞬时故障(如网络抖动)。
简单重试 2 次:
@RetryOnFailure(attempts = 2)
@Quietly
public static void divideByZero() {
int x = 1/0;
}
日志显示重试过程:
[main] WARN com.baeldung.jcabi.JcabiAspectJ -
#divideByZero(): attempt #1 of 2 failed in 147µs with java.lang.ArithmeticException: / by zero
[main] WARN com.baeldung.jcabi.JcabiAspectJ -
#divideByZero(): attempt #2 of 2 failed in 110µs with java.lang.ArithmeticException: / by zero
支持更精细控制:
@RetryOnFailure(
attempts = 3,
delay = 5,
unit = TimeUnit.SECONDS,
types = {NumberFormatException.class}
)
含义:
- 最多重试 3 次
- 每次间隔 5 秒
- 仅对
NumberFormatException
重试
9. @UnitedThrow:异常统一包装
@UnitedThrow 将方法抛出的多种异常统一包装为指定类型,简化上层异常处理。
示例:将 IOException
和 InterruptedException
统一为 IllegalStateException
@UnitedThrow(IllegalStateException.class)
public static void processFile() throws IOException, InterruptedException {
BufferedReader reader = new BufferedReader(new FileReader("baeldung.txt"));
reader.readLine();
// additional file processing
}
异常堆栈:
java.lang.IllegalStateException: java.io.FileNotFoundException: baeldung.txt (No such file or directory)
at com.baeldung.jcabi.JcabiAspectJ.processFile(JcabiAspectJ.java:92)
at com.baeldung.jcabi.JcabiAspectJ.main(JcabiAspectJ.java:39)
Caused by: java.io.FileNotFoundException: baeldung.txt (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
...
上层调用者只需捕获 IllegalStateException
即可,代码更简洁。
10. 总结
jcabi-aspects 通过 AOP 提供了一套简单粗暴的注解工具集,适用于:
- ✅ 快速添加日志、缓存、重试等横切逻辑
- ✅ 减少样板代码,提升开发效率
- ✅ 适合非 Spring 环境或想避开 Spring AOP 动态代理的场景
但也要注意:
- ⚠️ 依赖编译期织入,构建流程变复杂
- ⚠️ 内部线程池/缓存不可配置,灵活性较低
所有示例代码已上传至 GitHub:https://github.com/baeldung-tutorials/tutorials(路径:libraries-3)