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.