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
- Featured image by lumiere005 from Pixabay