How Caching Works in Spring Boot and Why You Should Care
Understanding Proxy Design Pattern Usage in Spring Boot
Introduction
Caching mostly works on the service layer in your application. We can cache the result returned by a method if the application calls it over and over again with the same parameters. The proper use of caching enables the web page to render fast, improve application performance and minimizes database access.
Cache Abstraction In Spring Boot
In Spring Boot caching is applied to the Java methods. That means whenever these methods are called, the cache abstraction applies the cache behavior to these methods. The data is returned without having to execute the target method if it's already available in the cache, otherwise the target method is called and the returned data is cached and returned to the caller. Cache abstraction also provides other operations such as updating or removing cached data.
But how all of this magical logic is implemented?
Caching via Proxy Pattern
Spring Boot applies proxy around Spring Beans where you declare the methods that should be cached using the @Cacheable
annotation. This proxy will be in charge of adding caching behavior and will be used for dependency injection.
So what do you think will happen if you try to cache a private function? or when you call a method with @Cacheable
annotation directly by another method of the same class? Will the caching work?
Let's create a Spring Boot project and see what will happen.
Create Spring Boot Project
Go to Spring Initializr, then create a new project with this setup
We only need these dependencies: Spring Web, Spring Data Redis, Spring Cache Abstraction.
Redis Server
We will use Docker to setup and run a Redis Server. Use the following command to run Redis inside a Docker container
docker run --name rediscache -p 6379:6379 redis:6.2-alpine redis-server --save 60 1 --requirepass foobar
Configuring Redis Cache
Open application.properties
and add these 4 lines
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=foobar
then add @EnableCaching
annotation on Spring Boot main class
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CachedemoApplication {
public static void main(String[] args) {
SpringApplication.run(CachedemoApplication.class, args);
}
}
Let's now create a controller which will call a service.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.helloWorld();
}
}
Let's try first to cache the result returned by a private function.
Private Functions
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String helloWorld() {
return this.cachedHelloWorld();
}
@Cacheable(cacheNames = "helloWorldCache")
private String cachedHelloWorld() {
System.out.println("CachedHelloWorld - Start");
return "Hello World!";
}
}
If you try to call /hello
endpoint you will notice that you can't see any caching behavior and that's because Spring's cache abstraction module uses proxies and you should use the caching annotations only on public methods which can be called by the proxy.
Now if we change the access modifier to public
it should work, right? Let's see
Same Class Call
@Service
public class HelloService {
public String helloWorld() {
return this.cachedHelloWorld();
}
@Cacheable(cacheNames = "helloWorldCache")
public String cachedHelloWorld() {
System.out.println("CachedHelloWorld - Start");
return "Hello World!";
}
}
We can't see also any caching behavior, but why? The answer is because of how proxies work, you should never call a method annotated with @Cacheable
, @CachePut
or @CacheEvict
directly by another method from the same class because in this case Spring proxy is never applied.
Now move the @Cacheable
annotation to the helloWorld
function and try it to call /hello
.
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
@Cacheable(cacheNames = "helloWorldCache")
public String helloWorld() {
return this.cachedHelloWorld();
}
public String cachedHelloWorld() {
System.out.println("CachedHelloWorld - Start");
return "Hello World!";
}
}
You can see now that there's a caching behavior when you call the helloWorld
function.
Conclusion
Never call a method annotated with
@Cacheable
,@CachePut
or@CacheEvict
directly by another method from the same class.You should use the cache annotations only with public methods.
Thank you for reading!