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 注解可将方法标记为异步执行,但有严格限制:

  • ✅ 方法返回类型必须是 voidFuture<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 将方法抛出的多种异常统一包装为指定类型,简化上层异常处理。

示例:将 IOExceptionInterruptedException 统一为 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)


原始标题:Introduction to the jcabi-aspects AOP Annotations Library | Baeldung

« 上一篇: CRaSH 入门指南
» 下一篇: cache2k 介绍