一 写在前面
相信很多人都是用过Spring与Redis的整合框架Spring-data-redis-starter提供的@Cacheabl,@CacheEviict和@CachePut注解,它以注解的形式简化了在Spring Boot程序中使用Redis缓存的成本,真心非常好用。
但是出于一些特殊的原因,例如:公司自己有的缓存框架屏蔽了Redis连接地址,只允许通过namespace的方式执行访问的Redis集群,或者只是单纯地想要自己造个轮子,深入学习下这几个注解内部的底层实现原理,我们决定自己实现上述三个注解提供的能力支持。
注解名称 | 功能含义 |
@BossCacheable | 如果有缓存则从缓存中获取数据,否则从数据库中查询到之后返回并且添加到缓存中 |
@BossCahceEvict | 使得指定缓存key对应的缓存失效 |
@BossCachePut | 更新缓存到执行的key中 |
二 开始行动
2.1 引入基础的依赖包:
<properties>
<spring.version>5.3.18</spring.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
代码工程结构:
2.2 注解定义
定义注解BossCacheable
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 效果等同于@Cacheable
* 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface BossCacheable {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* Names of the caches in which method invocation results are stored.
* <p>Names may be used to determine the target cache (or caches), matching
* the qualifier value or bean name of a specific bean definition.
* @since 4.2
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 缓存key,可以为空,
* 如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
* <p>
* 例如:@BossCacheable(key="#userName")
*
* @return
*/
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>Default is {@code ""}, meaning all method parameters are considered as a key,
* unless a custom {@link #keyGenerator} has been configured.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String key() default "";
/**
* Spring Expression Language (SpEL) expression used for making the method
* caching conditional.
* <p>Default is {@code ""}, meaning the method result is always cached.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String condition() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
* to use.
* <p>Mutually exclusive with the {@link #key} attribute.
* @see CacheConfig#keyGenerator
*/
String keyGenerator() default "";
/**
* 超时时间
* 单位:seconds
*
* @return
*/
int expire() default 10 * 60;
}
定义注解BossCacheEvict
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BossCacheEvict {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* Names of the caches to use for the cache eviction operation.
* <p>Names may be used to determine the target cache (or caches), matching
* the qualifier value or bean name of a specific bean definition.
* @since 4.2
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>Default is {@code ""}, meaning all method parameters are considered as a key,
* unless a custom {@link #keyGenerator} has been set.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #result} for a reference to the result of the method invocation, which
* can only be used if {@link #beforeInvocation()} is {@code false}. For supported
* wrappers such as {@code Optional}, {@code #result} refers to the actual object,
* not the wrapper</li>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
* to use.
* <p>Mutually exclusive with the {@link #key} attribute.
* @see CacheConfig#keyGenerator
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) expression used for making the cache
* eviction operation conditional.
* <p>Default is {@code ""}, meaning the cache eviction is always performed.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String condition() default "";
}
定义注解BossCachePut
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* Annotation indicating that a method (or all methods on a class) triggers a
* {@link org.springframework.cache.Cache#put(Object, Object) cache put} operation.
*
* <p>In contrast to the {@link Cacheable @Cacheable} annotation, this annotation
* does not cause the advised method to be skipped. Rather, it always causes the
* method to be invoked and its result to be stored in the associated cache. Note
* that Java8's {@code Optional} return types are automatically handled and its
* content is stored in the cache if present.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em> with attribute overrides.
*
* @author Costin Leau
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sam Brannen
* @since 3.1
* @see CacheConfig
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface BossCachePut {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* Names of the caches to use for the cache put operation.
* <p>Names may be used to determine the target cache (or caches), matching
* the qualifier value or bean name of a specific bean definition.
* @since 4.2
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>Default is {@code ""}, meaning all method parameters are considered as a key,
* unless a custom {@link #keyGenerator} has been set.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #result} for a reference to the result of the method invocation. For
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
* object, not the wrapper</li>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
* to use.
* <p>Mutually exclusive with the {@link #key} attribute.
* @see CacheConfig#keyGenerator
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) expression used for making the cache
* put operation conditional.
* <p>Default is {@code ""}, meaning the method result is always cached.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String condition() default "";
/**
* 超时时间
* 单位:seconds
*
* @return
*/
int expire() default 10 * 60;
}
定义注解BossCacheEvict
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BossCacheEvict {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* Names of the caches to use for the cache eviction operation.
* <p>Names may be used to determine the target cache (or caches), matching
* the qualifier value or bean name of a specific bean definition.
* @since 4.2
* @see #value
* @see CacheConfig#cacheNames
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>Default is {@code ""}, meaning all method parameters are considered as a key,
* unless a custom {@link #keyGenerator} has been set.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #result} for a reference to the result of the method invocation, which
* can only be used if {@link #beforeInvocation()} is {@code false}. For supported
* wrappers such as {@code Optional}, {@code #result} refers to the actual object,
* not the wrapper</li>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
* to use.
* <p>Mutually exclusive with the {@link #key} attribute.
* @see CacheConfig#keyGenerator
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) expression used for making the cache
* eviction operation conditional.
* <p>Default is {@code ""}, meaning the cache eviction is always performed.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String condition() default "";
}
2.3 Redis配置注入
@Configuration
@ConfigurationProperties(prefix = "redis")
@Data
@Slf4j
@RefreshScope
public class CacheAutoConfig {
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
jedisPoolConfig.setBlockWhenExhausted(true);
jedisPoolConfig.setNumTestsPerEvictionRun(10);
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(120000);
jedisPoolConfig.setMinEvictableIdleTimeMillis(60000);
jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(60000);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
return jedisPoolConfig;
}
@Bean
public RedisPoolConfig redisPoolConfig(JedisPoolConfig jedisPoolConfig) {
RedisPoolConfig redisPoolConfig = new RedisPoolConfig(jedisPoolConfig);
redisPoolConfig.setEagerInit(true);
redisPoolConfig.setLoadBalancer(ConsistentHashSorted);
return redisPoolConfig;
}
@Bean
public ICacheService cacheService(RedisClusterPoolClient redisClusterPoolClient) {
CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl();
// 此处略去部分代码
return cacheService;
}
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName()).append("#");
sb.append(method.getName()).append("(");
sb.append(Arrays.asList(params).toString()).append(")");
return sb.toString();
};
}
@Bean
public ExpressionParser expressionParser() {
return new SpelExpressionParser();
}
}
注解处理类BossCacheAnnotationAspect
@Slf4j
@Aspect
@Component
@Order(1000)
public class BossCacheAnnotationAspect implements BeanFactoryAware {
@Resource
private ICacheManagerService cacheManagerService;
@Autowired
private KeyGenerator keyGenerator;
@Autowired
private ExpressionParser expressionParser;
private BeanFactory beanFactory;
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
private EvaluationContext createEvaluationContext(Object target, Method targetMethod, Object[] args) {
return new MethodBasedEvaluationContext(target, targetMethod, args, parameterNameDiscoverer);
}
@Around(value = "@annotation(bossCacheable)")
private Object around(ProceedingJoinPoint point, BossCacheable bossCacheable) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String key = bossCacheable.key();
String generator = bossCacheable.keyGenerator();
// 生成缓存key
String cacheKey = getCacheKey(point, method, key, generator);
log.debug("========= cache key:【{}】========", cacheKey);
long startTime = System.currentTimeMillis();
if (StringUtils.isNotBlank(cacheKey)) {
Object cacheValue = cacheManagerService.get(cacheKey, method.getReturnType());
if (cacheValue != null) {
log.debug("========= cache hit, cost time:{}ms, key:【{}】========", (System.currentTimeMillis() - startTime), cacheKey);
return cacheValue;
} else {
Object resultValue = point.proceed();
if (resultValue != null) {
addCacheKey(cacheKey, resultValue, bossCacheable.expire());
}
log.debug("========= cache miss, cost time:{}ms, key:【{}】========", (System.currentTimeMillis() - startTime), cacheKey);
return resultValue;
}
} else {
return point.proceed();
}
}
@AfterReturning(value = "@annotation(bossCachePut)", returning = "result")
private void put(JoinPoint point, Object result, BossCachePut bossCachePut) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String key = bossCachePut.key();
String generator = bossCachePut.keyGenerator();
// 生成缓存key
String cacheKey = getCacheKey(point, method, key, generator);
log.debug("========= put cache key:【{}】========", cacheKey);
if (StringUtils.isNotBlank(cacheKey) && result != null) {
addCacheKey(cacheKey, result, bossCachePut.expire());
}
}
@After(value = "@annotation(bossCacheEvict)")
private void evict(JoinPoint point, BossCacheEvict bossCacheEvict) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String key = bossCacheEvict.key();
String generator = bossCacheEvict.keyGenerator();
String cacheKey = getCacheKey(point, method, key, generator);
log.debug("========= del cache key:【{}】========", cacheKey);
cacheManagerService.del(cacheKey);
}
@Around(value = "@annotation(bossCacheLock)")
private Object around(ProceedingJoinPoint point, BossCacheLock bossCacheLock) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String key = bossCacheLock.key();
String generator = bossCacheLock.keyGenerator();
// 生成缓存key
String cacheKey = getCacheKey(point, method, key, generator);
Long expire = bossCacheLock.timeUnit().toMillis(bossCacheLock.lockTime());
String uuid = UUID.randomUUID().toString();
Boolean getLock = cacheManagerService.tryGetDistributedLock(cacheKey, uuid, expire);
if (getLock) {
return point.proceed();
} else {
return null;
}
}
private String getCacheKey(JoinPoint point, Method method, String key, String generator) {
// 生成缓存key
String cacheKey;
if (StringUtils.isNotBlank(key)) {
EvaluationContext evaluationContext = createEvaluationContext(point.getTarget(), method, point.getArgs());
cacheKey = getKey(key, evaluationContext).toString();
} else {
// 默认使用全局的keyGenerator
KeyGenerator cacheKeyGenerator = keyGenerator;
// 如果注解中的keyGenerator不为空,则使用自定义的keyGenerator
if (StringUtils.isNotBlank(generator)) {
cacheKeyGenerator = BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, KeyGenerator.class, generator);
}
cacheKey = cacheKeyGenerator.generate(point.getTarget(), method, point.getArgs()).toString();
}
return cacheKey;
}
private Object getKey(String key, EvaluationContext evaluationContext) {
Expression expression = expressionParser.parseExpression(key);
return expression.getValue(evaluationContext);
}
/**
* 增加缓存,并将key存到list中,方便删除
*
* @param key
* @param value
* @param expire
*/
private void addCacheKey(String key, Object value, Integer expire) {
cacheManagerService.set(key, value, Object.class, expire);
}
}
缓存服务
public interface ICacheManagerService {
/**
* 缓存字符串,可设置过期时间
*
* @param key key
* @param value 要换成的字符串
* @param seconds 过期时间
*/
void set(String key, String value, Integer seconds);
/**
* 缓存对象和过期时间
*
* @param key key
* @param object 所要缓存对象
* @param clazz 对象的类别信息; 木有办法,泛型类别在运行时被擦除了,还得多传一下类信息用于序列,反序列化,麻烦一下吧
* @param seconds 过期时间
* @param <T> 对象泛型类别
*/
<T> void set(String key, T object, Class<T> clazz, int seconds);
String get(String key);
Long del(String key);
<T> T get(String key, Class<T> tClass);
Boolean tryGetDistributedLock(String lockKey, String requestId, Long expireTime);
}
2.4 测试
import lombok.Data;
@Data
public class TestUser {
private Integer id;
private String name;
}
@Controller
public class CacheController {
@RequestMapping("/cache/get")
@ResponseBody
@BossCacheable(key = "'user:'+#testUser.id", expire = 180)
public String get(@RequestBody TestUser testUser) {
return testUser.getName();
}
@RequestMapping("/cache/update")
@ResponseBody
@BossCachePut(key = "'user:'+#testUser.id", expire = 180)
public String update(String key, @RequestBody TestUser testUser) {
return key;
}
@RequestMapping("/cache/delete")
@ResponseBody
@BossCacheEvict(key = "'user:'+#testUser.id")
public String delete(String key, @RequestBody TestUser testUser) {
return key;
}
}
三 参考资料
1. Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用_灼烧的疯狂的博客-CSDN博客_cacheevict注解