Build Custom SpringBoot Cache Annotations with Redis and AOP

This guide walks through creating a lightweight SpringBoot cache component using custom @Cacheable and @CacheEvict annotations, configuring Redis, implementing an AOP aspect to handle caching logic, and demonstrating usage with sample services and controller endpoints, while also discussing key generation and potential enhancements.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build Custom SpringBoot Cache Annotations with Redis and AOP

1. Introduction

To demonstrate our capability we will build a simplified cache component similar to Spring Cache, focusing on core caching principles.

2. Practical Example

2.1 Environment Setup

Since the implementation is based on Redis, add the following Maven dependency:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Configure Redis connection in application.yml (or properties):

spring:
  redis:
    timeout: 10000
    connectTimeout: 20000
    host: 127.0.0.1
    password: xxxooo
    lettuce:
      pool:
        maxActive: 8
        maxIdle: 100
        minIdle: 10
        maxWait: -1

2.2 Custom Annotations

Define two annotations that mimic Spring Cache:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
  // cache key
  String key() default "";
  // cache name (group)
  String name() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheEvict {
  // cache key
  String key() default "";
  // cache name (group)
  String name() default "";
}

@Cacheable reads from the cache or stores the method result when absent; @CacheEvict removes the specified key.

2.3 Define Aspect

The aspect intercepts methods annotated with the custom annotations and performs caching logic using SpEL for dynamic keys.

@Component
@Aspect
public class CacheAspect {
  private static final Logger logger = LoggerFactory.getLogger(CacheAspect.class);
  private final StringRedisTemplate stringRedisTemplate;
  public CacheAspect(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
  }
  private DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

  @Pointcut("@annotation(com.pack.redis.cache.Cacheable)")
  private void cacheable() {}
  @Pointcut("@annotation(com.pack.redis.cache.CacheEvict)")
  private void cacheevict() {}

  @Around("cacheable() || cacheevict()")
  public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
    Method method = ((MethodSignature) pjp.getSignature()).getMethod();
    Object[] args = pjp.getArgs();
    SpelExpressionParser parser = new SpelExpressionParser();
    StandardEvaluationContext context = new StandardEvaluationContext();
    String[] parameterNames = discoverer.getParameterNames(method);
    for (int i = 0; i < parameterNames.length; i++) {
      context.setVariable(parameterNames[i], args[i]);
    }
    Cacheable cacheable = method.getAnnotation(Cacheable.class);
    if (cacheable != null) {
      String name = cacheable.name();
      String expression = cacheable.key();
      Object value = parser.parseExpression(expression).getValue(context);
      String cacheKey = name + ":" + value;
      boolean hasKey = this.stringRedisTemplate.hasKey(cacheKey);
      if (!hasKey) {
        Object ret = pjp.proceed();
        this.stringRedisTemplate.opsForValue().set(cacheKey,
            new ObjectMapper().writeValueAsString(ret));
        logger.info("Write cache [{}], data: {}", cacheKey, ret);
        return ret;
      }
      logger.info("Read cache [{}]", cacheKey);
      String result = this.stringRedisTemplate.opsForValue().get(cacheKey);
      return new ObjectMapper().readValue(result, method.getReturnType());
    }
    CacheEvict cacheevict = method.getAnnotation(CacheEvict.class);
    if (cacheevict != null) {
      String name = cacheevict.name();
      String expression = cacheevict.key();
      Object value = parser.parseExpression(expression).getValue(context);
      String cacheKey = name + ":" + value;
      this.stringRedisTemplate.delete(cacheKey);
    }
    return pjp.proceed();
  }

  private String getKey(Class<?> targetType, Method method) {
    StringBuilder builder = new StringBuilder();
    builder.append(targetType.getSimpleName());
    builder.append('#').append(method.getName()).append('(');
    Class<?>[] types = method.getParameterTypes();
    for (Class<?> clazz : types) {
      builder.append(clazz.getSimpleName()).append(",");
    }
    if (method.getParameterTypes().length > 0) {
      builder.deleteCharAt(builder.length() - 1);
    }
    return builder.append(')').toString().replaceAll("[^a-zA-Z0-9]", "");
  }
}

2.4 Test

Service example using the annotations:

@Service
public class UserService {
  private final UserRepository userRepository;
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Cacheable(name = "user", key = "#id")
  public User queryById(Long id) {
    return this.userRepository.findById(id).orElse(null);
  }

  @CacheEvict(name = "user", key = "#user.id")
  public void clearUserById(User user) {
    this.userRepository.deleteById(user.getId());
  }
}

Controller endpoints:

@GetMapping("/{id}")
public User getUser(@PathVariable("id") Long id) {
  return this.userService.queryById(id);
}

@DeleteMapping("/{id}")
public void removeUser(@PathVariable("id") Long id) {
  User user = new User();
  user.setId(id);
  this.userService.clearUserById(user);
}

First GET request logs a cache write; subsequent requests retrieve data directly from Redis, as shown in the screenshots below.

After the second request the data is fetched from the cache, and Redis now holds the entry for the given ID.

The simple cache component is functional, though further improvements such as cache updating, lock mechanisms for high concurrency, and multi‑level caching with local stores are possible.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CacheredisSpringBootCustomAnnotations
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.