Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this quick tutorial, we'll explore the jcabi-aspects Java library, a collection of handy annotations that modify the behavior of Java application using aspect-oriented programming (AOP).

The jcabi-aspects library provides annotations like @Async, @Loggable, and @RetryOnFailure, that are useful in performing certain operations efficiently using AOP. At the same time, they help to reduce the amount of boilerplate code in our application. The library requires AspectJ to weave the aspects into compiled classes.

2. Setup

First, we'll add the latest jcabi-aspects Maven dependency to the pom.xml:

<dependency>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-aspects</artifactId>
    <version>0.22.6</version>
</dependency>

The jcabi-aspects library requires AspectJ runtime support to act. Therefore, let's add the aspectjrt Maven dependency:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
    <scope>runtime</scope>
</dependency>

Next, let's add the jcabi-maven-plugin plugin that weaves the binaries with AspectJ aspects at compile-time. The plugin provides the ajc goal that does the automatic weaving:

<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.2</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
    </dependencies>
</plugin>

Last, let's compile the classes using the Maven command:

mvn clean package

The logs generated by the jcabi-maven-plugin at compilation will look like:

[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)

Now that we know how to add the library to our project, let's see some if its annotations in action.

3. @Async

The @Async annotation allows executing the method asynchronously. However, it is only compatible with methods that return a void or Future type.

Let's write a displayFactorial method that displays the factorial of a number asynchronously:

@Async
public static void displayFactorial(int number) {
    long result = factorial(number);
    System.out.println(result);
}

Then, we'll recompile the class to let Maven weave the aspect for the @Async annotation. Last, we can run our example:

[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution

As we can see from the log, the library creates a separate daemon thread jcabi-async to perform all asynchronous operations.

Now, let's use the @Async annotation to return a Future instance:

@Async
public static Future<Long> getFactorial(int number) {
    Future<Long> factorialFuture = CompletableFuture.supplyAsync(() -> factorial(number));
    return factorialFuture;
}

If we use @Async on a method that does not return void or Future, an exception will be thrown at runtime when we invoke it.

4. @Cacheable

The @Cacheable annotation allows caching a method's results to avoid duplicate calculations.

For instance, let's write a cacheExchangeRates method that returns the latest exchange rates:

@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;
}

Here, the cached result will have a lifetime of 2 seconds. Similarly, we can make a result cacheable forever by using:

@Cacheable(forever = true)

Once we recompile the class and execute it again, the library will log the details of two daemon threads that handle the caching mechanism:

[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-cacheable-clean for automated 
  cleaning of expired @Cacheable values
[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-cacheable-update for async 
  update of expired @Cacheable values

When we invoke our cacheExchangeRates method, the library will cache the result and log the details of the execution:

[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

So, if invoked again (within 2 seconds), cacheExchangeRates will return the result from the cache:

[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)

If the method throws an exception, the result won't be cached.

5. @Loggable

The library provides the @Loggable annotation for simple logging using the SLF4J logging facility.

Let's add the @Loggable annotation to our displayFactorial and cacheExchangeRates methods:

@Loggable
@Async
public static void displayFactorial(int number) {
    ...
}

@Loggable
@Cacheable(lifetime = 2, unit = TimeUnit.SECONDS)
public static String cacheExchangeRates() {
    ...
}

Then, after recompilation, the annotation will log the method name, return value, and execution time:

[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

6. @LogExceptions

Similar to @Loggable, we can use the @LogExceptions annotation to log only the exceptions thrown by a method.

Let's use @LogExceptions on a method divideByZero that will throw an ArithmeticException:

@LogExceptions
public static void divideByZero() {
    int x = 1/0;
}

The execution of the method will log the exception and also throw the exception:

[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

The @Quietly annotation is similar to @LogExceptions, except that it doesn't propagate any exception thrown by the method. Instead, it just logs them.

Let's add the @Quietly annotation to our divideByZero method:

@Quietly
public static void divideByZero() {
    int x = 1/0;
}

Hence, the annotation will swallow the exception and only log the details of the exception that would've otherwise been thrown:

[main] WARN com.baeldung.jcabi.JcabiAspectJ - java.lang.ArithmeticException: / by zero
    at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)

The @Quietly annotation is only compatible with methods that have a void return type.

8. @RetryOnFailure

The @RetryOnFailure annotation allows us to repeat the execution of a method in the event of an exception or failure.

For example, let's add the @RetryOnFailure annotation to our divideByZero method:

@RetryOnFailure(attempts = 2)
@Quietly
public static void divideByZero() {
    int x = 1/0;
}

So, if the method throws an exception, the AOP advice will attempt to execute it twice:

[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

Also, we can define other parameters like delay, unit, and types, while declaring the @RetryOnFailure annotation:

@RetryOnFailure(attempts = 3, delay = 5, unit = TimeUnit.SECONDS, 
  types = {java.lang.NumberFormatException.class})

In this case, the AOP advice will attempt the method thrice, with a delay of 5 seconds between attempts, only if the method throws a NumberFormatException.

9. @UnitedThrow

The @UnitedThrow annotation allows us to catch all exceptions thrown by a method and wrap it in an exception we specify. Thus, it unifies the exceptions thrown by the method.

For instance, let's create a method processFile that throws IOException and InterruptedException:

@UnitedThrow(IllegalStateException.class)
public static void processFile() throws IOException, InterruptedException {
    BufferedReader reader = new BufferedReader(new FileReader("baeldung.txt"));
    reader.readLine();
    // additional file processing
}

Here, we've added the annotation to wrap all exceptions into IllegalStateException. Therefore, when the method is invoked, the stack trace of the exception will look like:

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)
    ...

10. Conclusion

In this article, we've explored the jcabi-aspects Java library.

First, we've seen a quick way to set up the library in our Maven project using jcabi-maven-plugin.

Then, we examined a few handy annotations, like @Async, @Loggable, and @RetryOnFailure, that modify the behavior of the Java application using AOP.

As usual, all the code implementations are available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!