Redis cache using Spring AOP

Yogendra
4 min readSep 25, 2023

--

When I first started working on Spring, I was surprised 🤔 to see the power of Annotations. An Annotation like @Transactional is so powerful that you can achieve so many things (which happens behind the scene) by just adding a simple Annotation. But how does it work ? Spring uses Spring AOP behind the scene. I will try to relate how does it actually works in this article.

First lets start with cross cutting concerns. Anything that you perform repetitively apart from business logic like:

  • Logging
  • Security
  • Data validation
  • Error handling
  • Transaction management

can be termed as cross cutting concerns in Aspect Oriented Programming.

AOP cannot be compared with OOPS, it just complements OOPS concepts

Lets go through the multiple concepts of AOP:

  • Pointcut: A predicate used to match Join Point.
  • Advice: Actual action that will be taken at matching Pointcut. There are multiple types of Advice which are discussed below.
  • Join point: Point of program which will be intercepted. Spring supports only method execution.
  • Aspect: It a concern that combines above concepts. In Spring AOP, classes need to annotated with @Aspect to act as an Aspect and class should be a bean itself.

Weaving

Let us understand little bit about Weaving. It is the process of applying aspect which updates code at Join points which have matching Pointcuts. In other words, it is the process that intercepts call to the target object and modify its byte code. There are three types of weaving:

  • Compile time weaving
  • Load time weaving
  • Runtime weaving

Spring AOP, supports only Runtime weaving. In Spring AOP, weaving is achieved either using JDK proxy or CGLIB proxy:

  • JDK proxy: It creates proxy object by implementing an interface of target object. It is only possible when a class implements an interface.
  • CGLIB proxy: It creates proxy object by extending the target object. It is used when class does not implement an interface.

One thing to note here is that both above types do not support self invocation

If we want to enforce to use only CGLIB proxy, then we can use following:
@EnableAspectJAutoProxy(proxyTargetClass = true)

Types of Advice

  • Before: An advice that execute before join point.
  • AfterReturning: An advice that execute after successful execution of method.
  • AfterThrowing: An advice that executes after method throws an exception.
  • After: An advice that executes irrespective of whether method execution was successful or not.
  • Around: It is a most powerful advice which combines features of rest of the advice. In this article we will use Around Advice to implement our custom cache and log execution time of method.

Now let’s start with actual implementation:

Folder Structure

Create an Annotation @Cacheable:

public @interface Cacheable {
}

Followed by Aspect class:

@Component
@Aspect
public class CacheableAspect {

Logger logger = Logger.getLogger("Cache");

@Autowired
RedisDataCache redisDataCache;

@Pointcut("@annotation(com.yogendra.sbaoprediscache.aspect.Cacheable)")
public void cacheMethodResult(){

}

@Around("cacheMethodResult()")
public Object cacheMethodResult(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object argument = proceedingJoinPoint.getArgs()[0];
if (redisDataCache.ifKeyExists(argument.toString())){
logger.info("Fetching data from Redis cache for key " + argument);
return redisDataCache.fetchDataFromRedisCache(argument.toString());
} else {
Object response = proceedingJoinPoint.proceed(new Object[]{argument});
logger.info("Adding data to Redis cache for key " + argument);
redisDataCache.insertDataInRedisCache(argument.toString(), response);
return response;
}
}
}

Here you are expected to have little knowledge of Redis

Create another Annotation to log execution time:

public @interface PerformanceLogger {
}

Followed by Aspect class:

@Component
@Aspect
public class PerformanceAspect {

private Logger logger = Logger.getLogger("performance");

@Pointcut("@annotation(com.yogendra.sbaoprediscache.aspect.PerformanceLogger)")
public void logExecutionTime() {

}

@Around("logExecutionTime()")
public Object logExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
logger.info(proceedingJoinPoint.getSignature().toShortString() + " execution took " + executionTime + " milli seconds !!");
}
}
}

Now all we need is to use @Cacheable and @PerformanceLogger Annotations where we want to cache method result and log execution time respectively. Here is my code snippet for Service and Controller classes:

@Cacheable
public GoogleBookServiceResponse getBookVolumes(String name) {
return restTemplate
.getForObject("https://www.googleapis.com/books/v1/volumes?q={search}&max_results=2",
GoogleBookServiceResponse.class,
name);
}
@GetMapping("books")
@PerformanceLogger
public ResponseEntity<List<GoogleBook>> getBookVolumes(@RequestParam("search") String search) {
GoogleBookServiceResponse response = bookService.getBookVolumes(search);
return ResponseEntity.ok(response.items);
}

That’s all we need. Now you can just run the project and see response time changing significantly once the cache hit. 😌.

Find complete code in my GitHub repo

--

--

Yogendra

Software Engineer with 8 years of experience in the field. AWS Certified | Curious about JavaScript | Angular | Java | Spring Boot | Cloud | System Design