Spring cache custom resolver

This article introduces the custom cache resolver in spring. By customizing the resolver, additional processing can be added to the spring cache annotation.
For specific code, refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-redis-resolver

I. Overview

The cache-aside mode is a common cache usage mode. The usage flow is as follows:

[External link picture transfer failed, the source site may have. insert image description here
When the data in the database is updated, the cache is invalidated, and the latest data in the database can be read later, so that the cached data is consistent with the database data.

In spring, the cache annotation is used for cache processing, and the cache processing is generally encapsulated in the dao layer, so that the business layer does not need to perceive the details of the cache operation, and can focus on the processing of business logic.

2. Cache reading and invalidation

The operation of the dao layer usually uses springdatajpa, and the database method is an interface, and the cache processing is realized by adding the corresponding cache annotation on the interface.

read data:

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

Through the Cacheable annotation, after the data is read from the database, it will be written to the cache synchronously.

save data:

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

Through the CacheEvict annotation, after the data is written to the database, the cache is invalidated.
If we want to perform other operations after the cache expires, such as writing the key of the invalid cache to Kafka for other systems to delete the cache synchronously, what should we do?

3. Custom cache resolver

Spring provides a way to customize the cache resolver. By customizing the resolver, additional operations can be added to the cache processing.

@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 redis cache, RedisCacheManagerpart is the configuration of the conventional cacheManager, and customCacheResolverpart of it is the configuration of the custom resolver. By defining the bean customCacheResolver, the custom resolver can be referenced in the cache annotation.

After defining the bean of customCacheResolver, we can reference it in the cache annotation. The modified code of the data saving method mentioned above:

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

Compared with the previous implementation, the specified cacheResolver is added to CacheEvict.

4. Implementation of custom resolver

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

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 the custom resolver class CustomCacheResolver, which inherits SimpleCacheResolver. The SimpleCacheResolver class is the resolver used by spring in cache annotations by default.
We add additional operations by extending the SimpleCacheResolver class. Among them resolveCachesis the part of parsing cache operations.
In this part of the code, what we need is to get the key value of the invalidated cache in @CacheEvict(value = "testCache", cacheResolver = "customCacheResolver", key = "#p0.id")the annotation . The definition of key can be read from
the parameter context, that is, this definition is a spel expression, which is different from ordinary spel expressions. The variable p0 is a unique variable in the jpa method, indicating the first in the method parameter, and p1 also represents the second parameter in the method. This spel expression cannot be parsed by normal spel processing. Spring provides classes for parsing this special spel expression.context.getOperation()).getKey()#p0.id
MethodBasedEvaluationContext

Through four lines of code, we can get the value of the specific key:

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 obtaining the value of the key, we can perform many operations on the key. We can write the key into kafka and notify other systems to clean up the key synchronously.

V. Summary

We usually encapsulate the cache operation into the dao layer to simplify the overall logic of the program. When using springdatajpa as the implementation of the dao layer, the specific dao methods are interfaces. For the cache annotation added on the interface, there is no way to add additional operations .
When it is necessary to do additional processing on the cache operation, it can be realized by customizing the resolver, and using our custom resolver in the cache annotation.
In this way, there is no need to break the finishing logic of the program, and it also expands the operation of the cache, which is a better implementation method.

Guess you like

Origin blog.csdn.net/haiyan_qi/article/details/123468492