How to configure AspectJ in Spring Boot

How to configure AspectJ in Spring Boot

Some Spring Boot annotations by default use Spring AOP to create proxy classes. Of course, Spring allows using other libraries like AspectJ that can provide some advantages. In this article, we cover how to configure AspectJ in Spring Boot.

Introduction

To handle annotations like @Cacheable and @Transactional, Spring Boot relies on Spring AOP, which by default uses JDK dynamic proxy if the target class implements an interface. Otherwise, Spring uses CGLIB to create a dynamic proxy of the target class by subclassing.

Spring AOP is configured at run time and removes the need for a compilation step or load-time weaving, making things simpler.

On the other hand, it only works on public methods that are not invoked in the same class. To overcome the drawback of Spring AOP, we can swap it with AspectJ at the cost of some configurations and an extra compilation step.

An imaginary use case

Let’s say we have a microservice, the User service, with an endpoint to return a list of users. Assuming hypothetically, we have a situation in which we must intercept a private method on the user controller to log some stuff. We also need to use @Cacheable annotation on a private method.

Naturally, Spring AOP cannot cater to our requirements according to what we described above. However, by utilizing AspectJ, we can fulfill the requirements.

The followings are the codes for UserController, UserService, and LoggingInterceptor,

package com.madadipouya.sample.aspectj.controller;

import com.madadipouya.sample.aspectj.dto.User;
import com.madadipouya.sample.aspectj.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping(value = "/v1/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> getUsers() {
        return getUsersInternal();
    }

    private List<User> getUsersInternal() {
        return userService.getAllUsers();
    }
}
package com.madadipouya.sample.aspectj.service.impl;

import com.madadipouya.sample.aspectj.dto.User;
import com.madadipouya.sample.aspectj.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.madadipouya.sample.aspectj.config.CacheManagerConfig.USER_CACHE;

@Service
public class DefaultUserService implements UserService {

    @Override
    public List<User> getAllUsers() {
        return getMockUsers();
    }

    @Cacheable(USER_CACHE)
    private List<User> getMockUsers() {
        return IntStream.range(0, 1000).mapToObj(i -> new User(i, UUID.randomUUID().toString(), UUID.randomUUID().toString()))
                .collect(Collectors.toList());
    }
}
package com.madadipouya.sample.aspectj.interceptor;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;

@Aspect
@Component
public class LoggingInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Before(value = "execution(* com.madadipouya.sample.aspectj.controller.UserController.getUsersInternal(..))")
    public void addCommandDetailsToMessage() throws Throwable {
        logger.info("User controller getUsers method called at {}", ZonedDateTime.now(ZoneOffset.UTC));
    }
}

As you can see @Cacheable annotation is applied to getMockUsers, a private method. Furthermore, the interceptor is set to getUsersInternal, another private method.

Using AspectJ in Spring Boot

Configuring AspectJ in Spring Boot involves multiple changes. We have broken it down into the following steps to make it easier to grasp.

Adding AspectJ dependencies

We need to add all AspectJ dependencies to the project. That means we must have spring-aspects, aspectjweaver, and aspectjrt dependencies as well as configure aspectj-maven-plugin Maven plugin to weave AspectJ aspects into the classes using the AspectJ compiler (“ajc”).

Let’s add the dependencies,

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>6.0.10</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.19</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.19</version>
    </dependency>
</dependencies>

Adding AspectJ maven plugin

AspectJ supports two types of weaving, compile-time weaving (CTW) and load-time weaving (LTW). The former is simpler since ajc compiles source codes and produces woven class files. Whereas, in LTW the binary weaving is deferred until to the point that the class loader loads a class file and defines the class to the JVM. That means we have to use Spring Agent when running the project to add classes to the class loader at runtime.

Here we stick to CTW for simplicity’s sake. To use CTW we need to configure aspectj-maven-plugin in pom.xml as follows,

<plugins>
    <plugin>
        <groupId>dev.aspectj</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.13.1</version>
        <configuration>
            <complianceLevel>17</complianceLevel>
            <source>17</source>
            <target>17</target>
            <showWeaveInfo>true</showWeaveInfo>
            <verbose>true</verbose>
            <Xlint>ignore</Xlint>
            <encoding>UTF-8</encoding>
            <aspectLibraries>
                <aspectLibrary>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-aspects</artifactId>
                </aspectLibrary>
            </aspectLibraries>
            <showWeaveInfo>true</showWeaveInfo>
        </configuration>
        <executions>
            <execution>
                <goals>
                    <goal>compile</goal>
                    <goal>test-compile</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

The Mojo aspectj-maven-plugin does not yet support JDK 17. The maximum supported version is 16. Hence, we must use a forked plugin, dev-aspectj aspectj-maven-plugin, instead. You can still rely on the Mojo AspectJ plugin if your project uses JDK 8 or 11.

Enabling AspectJ in the app

Since we are using @SpringBootApplication annotation, we must not add @EnableAspectJAutoProxy anymore. If you use plain Spring, you still need to add that annotation.

However, we must still change the cache configuration in the Spring Boot app. For that, we only need to modify the caching configuration annotation as follows,

@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
public class CacheManagerConfig {
  // Cache manager implementation, removed for bravity
}

Running the app

To run the app, we use the maven command like below,

$ mvn spring-boot:run

Now we can open the browser, head to localhost:8080/v1/users, and hit enter. The following messages should be logged,

2020-03-28 19:44:03.863  INFO 40925 --- [nio-8080-exec-1] c.m.s.a.interceptor.LoggingInterceptor   : User controller getUsers method called at 2020-03-28T18:44:03.863024Z
2020-03-28 19:44:03.874  INFO 40925 --- [nio-8080-exec-1] c.m.s.a.service.impl.DefaultUserService  : Generating all the mock users!

The first line is the interceptor message, and the second is from the getMockUsers private method, annotated with @Cacheable. If you refresh the page, you should only see the interceptor message, not the other one

That means we successfully managed not only to intercept a private method but also made @Cacheable work on our private method 🙂

Conclusion

In this article, we explored how to configure AspectJ in a Spring Boot application. We used compile-time weaving (LTW) which requires an additional compilation step to generate woven classes. For that, we used the dev-aspectj’s aspectj-maven-plugin that supports JDK 17. By using AspectJ, one can also use Spring annotations such as @Cacheable on self-invocation.

You can find this article’s source code on GitHub,
https://github.com/kasramp/sample-spring-aspectj

Inline/featured images credits