Create custom spring AOP annotation for caching in Spring boot

  Reading Time:

Lets see how to add custom annotation to cache data to redis , we do have spring @Cacheable but we will not use it because it doesn't give us the flexibility to cache different objects with different TTL

We might have a use case where a object from a method A as to reside in cache for 5 minutes and same object from another method B to reside in cache for few seconds

To get this flexibility we shall create our own custom annotation

Lets create a custom annotation called CacheTTL  , here the cache has a TTL (Time to live) config for every entry that is cached , the TTL has the key expiration value in minutes . The reason we create this cache annotation  instead of using spring boot @Cacheable is to have TTL for every method call

Create a new custom annotation called CacheTTL .java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.stereotype.Component;

@Component
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheTTL {
	
	String value() default "";
	
	String cacheName() default "";
	
	int ttlMinutes() default 1;
}

 

Lets configure Redis as the caching layer (I am big fan of redis) but any caching library can be used like

Caches supported by the spring framework

  • JDK (i.e. java.util.concurrent.ConcurrentMap) Based Caches
  • EhCache 2.x
  • Gemfire Cache
  • Guava Cache
  • JCache (JSR 107)
  • Infinispan
  • Couchbase
  • Redis cache

   

Lets configure Redis in our project

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

@Configuration
@EnableCaching
public class RedisCacheConfig {

	@Value("${spring.redis.host:localhost}")
	private String redisHost;

	@Value("${spring.redis.port:6379}")
	private int redisPort;

	@Value("${spring.redis.ttl.hours:1}")
	private int redisDataTTL;

	@Bean
	JedisConnectionFactory jedisConnectionFactory() {
		JedisConnectionFactory factory = new JedisConnectionFactory();
		factory.setHostName(redisHost);
		factory.setPort(redisPort);
		factory.setUsePool(true);
		return factory;
	}

	@Bean
	public RedisTemplate<String, Object> redisTemplate() {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(jedisConnectionFactory());
		redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer());
		return redisTemplate;
	}

}

 

Now we need to process our custom annotation in spring and we will be using Aspect to do that

Now, let’s create our pointcut and advice. When our annotated method is called,  our advice will be called first so we can add caching logic to this method

import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.starbeat.cosmos.service.CacheService;

@Aspect
@Component
public class CacheTTLProcessorAspect {

	@Autowired
	private CacheService cacheService;

	@Around("@annotation(CacheTTL)")
	public Object cacheTTL(ProceedingJoinPoint joinPoint) throws Throwable {

		// get the current method that is called , we need this to extract the method name 
		Method method = getCurrentMethod(joinPoint);

		// Get all the method params
		Object[] parameters = joinPoint.getArgs();

		// Use the method name and params to create a key
		String key = CacheKeyGenerator.generateKey(method.getName(),parameters);

		// call cache service to get the value for the given key if not execute  method to get the return object to be cached
		Object returnObject = cacheService.cacheGet(key, method.getReturnType());
		if (returnObject != null)
			return returnObject;

		// execute method to get the return object
		returnObject = joinPoint.proceed(parameters);
		
		CacheTTL cacheTTL = method.getAnnotation(CacheTTL.class);
		
		// cache the method return object to redis cache with the key generated 
		cacheService.cachePut(key, returnObject, cacheTTL.ttlMinutes());

		return returnObject;
	}

	private Method getCurrentMethod(JoinPoint joinPoint) {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		return signature.getMethod();
	}
}

 

We will be using cache service to access Redis cache to store or retrieve the cache values

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

	@Autowired
	private RedisTemplate<String, Object> template;

	@Value("${spring.redis.cache.enabled:true}")
	private boolean cacheEnabled;

	/**
	 * Clear redis cache, clear all keys
	 */
	public void clearAllCaches() {
		template.execute((RedisCallback<Object>) connection -> {
		     connection.flushAll();
		     return null;
		 });
	}



	/**
	 * Add key and value to redis cache with ttlMinutes as the key expiration in minutes
	 * 
	 * @param key
	 * @param toBeCached
	 * @param ttlMinutes
	 */
	public void cachePut(String key, Object toBeCached, long ttlMinutes) {
		if (!cacheEnabled)
			return;

		template.opsForValue().set(key, toBeCached, ttlMinutes, TimeUnit.MINUTES);
	}

	/**
	 * Add key and value to redis cache with no expiration of key
	 * 
	 * @param key
	 * @param toBeCached
	 */
	public void cachePut(String key, Object toBeCached) {
		if (!cacheEnabled)
			return;

		if (toBeCached == null)
			return;

		cachePut(key, toBeCached, -1);
	}

	/**
	 * Get the value for the given key from redis cache
	 * 
	 * @param key
	 * @param type
	 * @return
	 */
	public <T> T cacheGet(String key, Class<T> type) {
		if (!cacheEnabled)
			return null;

		return (T) template.opsForValue().get(key);

	}

 

We will be using a standard Redis cache key generation in our example .The key generation is very simple  and can be complex based on project requirement

import java.util.Arrays;

public class CacheKeyGenerator {

	/**
	 * Append the method name , param to an array and create a deepHashCode of the array as redis cache key
	 * @param methodName
	 * @param params
	 * @return
	 */
	public static String generateKey(String methodName , Object... params) {
		if (params.length == 0) {
			return new Integer(methodName.hashCode()).toString();
		}
		Object[] paramList = new Object[params.length+1];
		paramList[0] = methodName;
		System.arraycopy(params, 0, paramList, 1, params.length);
		int hashCode = Arrays.deepHashCode(paramList);
		return new Integer(hashCode).toString();
	}
}

 

Now lets see how to use our custom annotation in our project

	@CacheTTL(ttlMinutes = 5)
	public List<User> getUsers() {
		List<User> users = dbService.getAllUsersFromDB();
		return users;
	}

If we notice the ttlMinutes is 5 , it means all the users fetched from DB will be cached in redis for 5 minutes so subsequent calls to getUsers() will not make a DB call but will return from cache

 

Summary

We just saw how to create custom annotation and process that using spring AOP annotation . We saw how to use Redis as a caching layer at method level using the custom annotation , this annotation helps us to have TTL for every method based on the method usage in the application , some methods can cache data for longer time and some for shorter duration . We can create custom cache evict and update annotation in a similar fashion but not within the scope of this blog post .

How to create SaaS style multi tenant web app with Spring Boot 2 ,Spring Security 5 and MySQL

PrerequisitesJava 8Spring Boot 2MySQLOverviewWe are going to see how to build JPA Multi Tenancy in Spring boot 2 and use Flyway for DB migration We are...

How to setup NGINX reverse proxy for application server dev and prod setup with Let’s Encrypt SSL certificate on Ubuntu 18.04

Assume we have two app servers running on port 8181 and 8080 Prod on 8080 and dev on 8181We want to redirecthttp --> https --&...

App & Geek   Never miss a story from App & Geek, get updates in your inbox.