Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Spring provides an annotation-based approach to enable caching on a Spring-managed bean. Based on the AOP technology, it’s easy to make a method cacheable by adding the annotation @Cacheable on it. However, the cache will be ignored when called from within the same class.

In this tutorial, we’ll explain why it happens and how to solve it.

2. Reproducing the Problem

First, we create a Spring Boot application with cache enabled. In this article, we created a MathService with a @Cacheable annotated square method:

@Service
@CacheConfig(cacheNames = "square")
public class MathService {
    private final AtomicInteger counter = new AtomicInteger();

    @CacheEvict(allEntries = true)
    public AtomicInteger resetCounter() {
        counter.set(0);
        return counter;
    }

    @Cacheable(key = "#n")
    public double square(double n) {
        counter.incrementAndGet();
        return n * n;
    }
}

Second, we create a method sumOfSquareOf2 in MathService that invokes the square method twice:

public double sumOfSquareOf2() {
    return this.square(2) + this.square(2);
}

Third, we create a test for the method sumOfSquareOf2 to check how many times the square method is invoked:

@SpringBootTest(classes = Application.class)
class MathServiceIntegrationTest {

    @Resource
    private MathService mathService;

    @Test
    void givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsNotTriggered() {
        AtomicInteger counter = mathService.resetCounter();

        assertThat(mathService.sumOfSquareOf2()).isEqualTo(8);
        assertThat(counter.get()).isEqualTo(2);
    }

}

Since the invocation from the same class doesn’t trigger the cache, the number of the counter is equal to 2, which indicates the method square with argument 2 is called twice and the cache is ignored. It isn’t our expectation, so we need to determine the reason for this behavior.

3. Analyzing the Problem

The caching behavior for the @Cacheable method is supported by Spring AOP. We’ll find some clues if we use an IDE to debug this code. The variable mathService in MathServiceIntegrationTest points to an instance of MathService$$EnhancerBySpringCGLIB$$5cdf8ec8, whereas this in MathService points to an instance of MathService.

MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 is a proxy class generated by Spring. It intercepts all requests on the @Cacheable method for MathService and responds with the cached value.

On the other hand, MathService itself doesn’t have the ability to cache, so internal calls within the same class won’t get the cached value.

Now that we understand the mechanism, let’s look for the solutions to this problem. Apparently, the simplest way is to move the @Cacheable method to another bean. But, if we have to keep the methods in the same bean for some reason, we have three possible solutions:

  • Self-injection
  • Compile-time weaving
  • Load-time weaving

In our Intro to  AspectJ article introduces aspect-oriented programming (AOP) and different weaving approaches in detail. Weaving is a way to insert the code that will occur when we compile the source code into .class files. It includes compile-time weaving, post-compile weaving, and load-time weaving in AspectJ. Since post-compile weaving is used to weave for third-party libraries, which isn’t our case, we just focus on compile-time weaving and load-time weaving.

4. Solution 1: Self-Injection

Self-injection is a commonly used solution for bypassing Spring AOP limitations. It allows us to get a reference to the Spring-enhanced bean and call the method through that bean. In our case, we can autowire the mathService bean to a member variable called self, and call the square method by self instead of using the this reference:

@Service
@CacheConfig(cacheNames = "square")
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MathService {

    @Autowired
    private MathService self;

    // other code

    public double sumOfSquareOf3() {
        return self.square(3) + self.square(3);
    }
}

The @Scope annotation helps create and inject a stub proxy to self due to the circular reference. It will later be filled with the same MathService instance. The test shows that the square method is only executed once:

@Test
void givenCacheableMethod_whenInvokingByExternalCall_thenCacheIsTriggered() {
    AtomicInteger counter = mathService.resetCounter();

    assertThat(mathService.sumOfSquareOf3()).isEqualTo(18);
    assertThat(counter.get()).isEqualTo(1);
}

5. Solution 2: Compile-Time Weaving

The weaving process in compile-time weaving, as the name suggests, happens at compile-time. It’s the simplest approach to weaving. When we have both the source code of the aspect and the code that we’re using aspects in, the AspectJ compiler will compile from the source and produce a woven class file as output.
In a Maven project, we can use Mojo’s AspectJ Maven Plugin to weave AspectJ aspects into our classes using the AspectJ compiler. For the @Cacheable annotation, the source code of the aspect is provided by the library spring-aspects, so we need to add it as the Maven dependency and the aspect library for the AspectJ Maven Plugin.

There are three steps to enable compile-time wavring. First, let’s enable caching with AspectJ mode by adding @EnableCaching annotation on any configuration class:

@EnableCaching(mode = AdviceMode.ASPECTJ)

Second, we need to add the spring-aspects dependency:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

Third, let’s define the aspectj-maven-plugin for the compile goal:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>${aspectj-plugin.version}</version>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <complianceLevel>${java.version}</complianceLevel>
        <Xlint>ignore</Xlint>
        <encoding>UTF-8</encoding>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The AspectJ Maven plugin shown above will weave the aspects when we execute mvn clean compile. With compile-time weaving, we don’t need to change the code, and the square method would be executed only once:

@Test
void givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered() {
    AtomicInteger counter = mathService.resetCounter();

    assertThat(mathService.sumOfSquareOf2()).isEqualTo(8);
    assertThat(counter.get()).isEqualTo(1);
}

6. Solution 3: Load-Time Weaving

Load-time weaving is simply binary weaving deferred until the point that a classloader loads a class file and defines the class to the JVM. AspectJ load-time weaving can be enabled using an AspectJ agent to get involved in the class-loading process and weave any types before they’re defined in the VM.

There are also three steps to enable load-time weaving. First, enable caching with AspectJ mode and load-time weaver by adding two annotations on any configuration class:

@EnableCaching(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving

Second, let’s add the spring-aspects dependency:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

Finally, we specify the javaagent option to the JVM -javaagent:path/to/aspectjweaver.jar or use the Maven plugin to configure the javaagent:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <configuration>
                <argLine>
                    --add-opens java.base/java.lang=ALL-UNNAMED
                    --add-opens java.base/java.util=ALL-UNNAMED
                    -javaagent:"${settings.localRepository}"/org/aspectj/aspectjweaver/${aspectjweaver.version}/aspectjweaver-${aspectjweaver.version}.jar
                    -javaagent:"${settings.localRepository}"/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar
                </argLine>
                <useSystemClassLoader>true</useSystemClassLoader>
                <forkMode>always</forkMode>
                <includes>
                    <include>com.baeldung.selfinvocation.LoadTimeWeavingIntegrationTest</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

The test givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered will also pass for load-time weaving.

7. Conclusion

In this article, we explained why cache doesn’t take effect when the @Cacheable method is invoked from the same bean. Then, we shared self-injection and two weaving solutions to solve this problem. As usual, the source code for this article is available over on GitHub.
Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.