This article introduces the custom cache resolver in spring, by customizing the resolver, you can add additional processing in spring’s cache annotation.

Specific code reference example project

1. Overview

The cache-aside pattern is a commonly used cache usage pattern. The usage flow is as follows.

cache-aside pattern

When the data in the database is updated, the cache is invalidated so that the latest data in the database can be read subsequently, making the cached data consistent with the database data.

In spring, cache processing is done through cache annotations, and cache processing is generally encapsulated in the dao layer so that the business layer does not need to be aware of the details of cache operations and can focus on the business logic.

2. the cache read and invalidate

dao layer operations usually use springdatajpa, database methods are an interface, by adding the corresponding cache annotation to the interface to achieve cache processing.

Read data.

1
2
@Cacheable(value = "testCache", key = "#p0", unless = "#result == null")
Optional<DemoEntity> findById(Long id);

With the Cacheable annotation, data read from the database is written to the cache synchronously.

Saving data.

1
2
@CacheEvict(value = "testCache", key = "#p0.id")
DemoEntity save(DemoEntity entity);

With the CacheEvict annotation, the cache is invalidated after the data is written to the database. What if we want to perform other operations after cache invalidation, such as writing the key of the invalidated cache to kafka for synchronous deletion of the cache by other systems?

3. Custom Cache Resolver

spring provides a way to customize the cache resolver. By customizing the resolver, you can add additional operations to the cache processing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(cacheName -> cacheName.concat(":"));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(cacheConfiguration)
                .build();

    }

    @Bean
    public CacheResolver customCacheResolver(RedisConnectionFactory redisConnectionFactory) {
        return new CustomCacheResolver(redisCacheManager(redisConnectionFactory));
    }
}

The above code is the configuration of the redis cache, where the RedisCacheManager part is the configuration of the regular cacheManager, and the customCacheResolver part is the configuration of the custom resolver. By defining the customCacheResolver bean, the custom resolver can be referenced in the cache annotation.

After defining the bean of customCacheResolver, we can refer to it in the cache annotation, and the code of the above mentioned data saving method is modified as follows

1
2
@CacheEvict(value = "testCache", cacheResolver = "customCacheResolver", key = "#p0.id")
DemoEntity save(DemoEntity entity);

Compared to the previous implementation, add the specified cacheResolver to CacheEvict.

4. Implementation of custom resolver

Above we have described how to configure and reference the cacheResolver, the following describes the implementation of the custom cacheResolver.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CustomCacheResolver extends SimpleCacheResolver {

    public CustomCacheResolver(CacheManager cacheManager) {
        super(cacheManager);
    }

    @Override
    @NonNull
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
        EvaluationContext evaluationContext = new MethodBasedEvaluationContext(context.getOperation(), context.getMethod(), context.getArgs(), paramNameDiscoverer);
        Expression exp = (new SpelExpressionParser()).parseExpression(((CacheEvictOperation) context.getOperation()).getKey());
        Collection<? extends Cache> caches = super.resolveCaches(context);
        context.getOperation().getCacheNames().forEach(cacheName -> {
            String key = cacheName + ':' + exp.getValue(evaluationContext, String.class);
            log.info("cache key={}", key);
        });
        return caches;
    }
}

The above code defines CustomCacheResolver, a custom resolver class that inherits from SimpleCacheResolver. The SimpleCacheResolver class is the default resolver used by spring in the cache annotation. We add additional operations by extending the SimpleCacheResolver class. Where resolveCaches is the part that resolves the cache operations. In this part of the code, all we need is to get the value of the key of the cache that is invalidated in the @CacheEvict(value = "testCache", cacheResolver = "customCacheResolver", key = "#p0.id") annotation. The definition of the key can be read from the parameter context by context.getOperation()).getKey(), i.e. #p0.id, which is a spel expression, unlike a normal spel expression, the variable p0 is a variable specific to the jpa method, indicating the first parameter in the method p0 is a variable unique to the jpa method and represents the first argument in the method, while p1 represents the second argument in the method. This spel expression cannot be parsed through normal spel processing. spring provides the MethodBasedEvaluationContext class for parsing such special spel expressions.

With the following four lines of code, we will be able to get the value of the specific key.

1
2
3
4
ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
EvaluationContext evaluationContext = new MethodBasedEvaluationContext(context.getOperation(), context.getMethod(), context.getArgs(), paramNameDiscoverer);
Expression exp = (new SpelExpressionParser()).parseExpression(((CacheEvictOperation) context.getOperation()).getKey());
String key = cacheName + ':' + exp.getValue(evaluationContext, String.class);

After getting the value of the key, we can do a lot of operations on this key, we can write this key to kafka and notify other systems to clean up the key synchronously.

5. Summary

When using springdatajpa as a dao layer implementation, the specific dao methods are interfaces, and there is no way to add additional operations to the cache annotations added to the interfaces. When additional processing of cache operations is required, this can be achieved by customizing the resolver and using our custom resolver in the cache annotation. This is a better way to achieve this without breaking the finishing logic of the program, but also extends the operation of the cache.

Reference http://springcamp.cn/spring-redis-resolver/