【spring】自定义缓存注解

为什么要自定义缓存注解?
Spring Cache本身提供@Cacheable、@CacheEvict、@CachePut等缓存注解,为什么还要自定义缓存注解呢?

@Cacheabe不能设置缓存时间,导致生成的缓存始终在redis中,当然这一点可以通过修改RedisCacheManager的配置来设置缓存时间:

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {RedisCacheConfiguration redisCacheConfiguration = getRedisCacheConfiguration();RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(redisCacheConfiguration).build();return cacheManager;
}
private RedisCacheConfiguration getRedisCacheConfiguration() {Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);jackson2JsonRedisSerializer.setObjectMapper(om);RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofDays(7)); // 设置全局key的有效事件为7天return redisCacheConfiguration;
}

不过这个设置时全局的,所有的key的失效时间都一样,要想实现不同的key不同的失效时间,还得自定义缓存注解。

自定义缓存注解
Cached
类似Spring Cache的@Cacheable。

package com.morris.spring.custom.cache.annotation;import org.springframework.core.annotation.AliasFor;import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cached {@AliasFor("cacheName")String value() default "";@AliasFor("value")String cacheName() default "";int expire() default 0;TimeUnit expireUnit() default TimeUnit.SECONDS;String key();String condition() default "";String unless() default "";boolean sync() default false;
}

CacheUpdate
类似于Spring Cache的@CachePut。

package com.morris.spring.custom.cache.annotation;import org.springframework.core.annotation.AliasFor;import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheUpdate {@AliasFor("cacheName")String value() default "";@AliasFor("value")String cacheName() default "";int expire() default 0;TimeUnit expireUnit() default TimeUnit.SECONDS;String key();String condition() default "";String unless() default "";
}

acheInvalidate
类似于Spring Cache的@CacheEvict。

package com.morris.spring.custom.cache.annotation;import org.springframework.core.annotation.AliasFor;import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheInvalidate {@AliasFor("cacheName")String value() default "";@AliasFor("value")String cacheName() default "";String key();String condition() default "";String unless() default "";
}

CachedAspect
@Cached注解的切面实现。

package com.morris.spring.custom.cache.annotation;import com.morris.spring.custom.cache.Level2Cache;
import com.morris.spring.custom.cache.Level2CacheManage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.cache.Cache;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.expression.EvaluationContext;import javax.annotation.Resource;
import java.util.Objects;@Configuration
@Aspect
@EnableAspectJAutoProxy // 开启AOP
public class CachedAspect {@Resourceprivate Level2CacheManage cacheManager;@Around(value = "@annotation(cached)")public Object around(ProceedingJoinPoint joinPoint, Cached cached) throws Throwable {Level2Cache cache = cacheManager.getCache(cached.cacheName());ExpressionEvaluator evaluator = new ExpressionEvaluator();EvaluationContext context = evaluator.createEvaluationContext(joinPoint);Object key = evaluator.key(cached.key(), context);if(cached.sync()) {//return cache.get(key, () -> {try {return joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();return null;}});}// 先查缓存Cache.ValueWrapper valueWrapper = cache.get(key);if (Objects.nonNull(valueWrapper)) {return valueWrapper.get();}Object result = joinPoint.proceed();context.setVariable("result", result);Boolean condition = evaluator.condition(cached.condition(), context);Boolean unless = evaluator.unless(cached.unless(), context);if (condition && !unless) {if (cached.expire() > 0) {cache.put(key, result, cached.expire(), cached.expireUnit());} else {cache.put(key, result);}}return result;}}

CacheUpdateAspect
@CacheUpdate注解的切面实现类。

package com.morris.spring.custom.cache.annotation;import com.morris.spring.custom.cache.Level2Cache;
import com.morris.spring.custom.cache.Level2CacheManage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.EvaluationContext;import javax.annotation.Resource;@Configuration
@Aspect
public class CacheUpdateAspect {@Resourceprivate Level2CacheManage cacheManager;@Around(value = "@annotation(cacheUpdate)")public Object around(ProceedingJoinPoint joinPoint, CacheUpdate cacheUpdate) throws Throwable {Level2Cache cache = cacheManager.getCache(cacheUpdate.cacheName());ExpressionEvaluator evaluator = new ExpressionEvaluator();EvaluationContext context = evaluator.createEvaluationContext(joinPoint);Object key = evaluator.key(cacheUpdate.key(), context);Object result = joinPoint.proceed();context.setVariable("result", result);Boolean condition = evaluator.condition(cacheUpdate.condition(), context);Boolean unless = evaluator.unless(cacheUpdate.unless(), context);if (condition && !unless) {if (cacheUpdate.expire() > 0) {cache.put(key, result, cacheUpdate.expire(), cacheUpdate.expireUnit());} else {cache.put(key, result);}}return result;}}

CacheInvalidateAspect
@CacheInvalidate注解的切面实现类。

package com.morris.spring.custom.cache.annotation;import com.morris.spring.custom.cache.Level2Cache;
import com.morris.spring.custom.cache.Level2CacheManage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.EvaluationContext;import javax.annotation.Resource;@Configuration
@Aspect
public class CacheInvalidateAspect {@Resourceprivate Level2CacheManage cacheManager;@Around(value = "@annotation(cacheInvalidate)")public Object around(ProceedingJoinPoint joinPoint, CacheInvalidate cacheInvalidate) throws Throwable {Level2Cache cache = cacheManager.getCache(cacheInvalidate.cacheName());ExpressionEvaluator evaluator = new ExpressionEvaluator();EvaluationContext context = evaluator.createEvaluationContext(joinPoint);Object key = evaluator.key(cacheInvalidate.key(), context);Object result = joinPoint.proceed();context.setVariable("result", result);Boolean condition = evaluator.condition(cacheInvalidate.condition(), context);Boolean unless = evaluator.unless(cacheInvalidate.unless(), context);if (condition && !unless) {cache.evict(key);}return result;}}

ExpressionEvaluator
ExpressionEvaluator主要用于解析注解中key、condition、unless属性中的表达式。

package com.morris.spring.custom.cache.annotation;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.StringUtils;import java.lang.reflect.Method;public class ExpressionEvaluator {private final SpelExpressionParser parser = new SpelExpressionParser();public EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method specificMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), joinPoint.getTarget().getClass());MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(joinPoint.getTarget(),specificMethod, joinPoint.getArgs(), new DefaultParameterNameDiscoverer());return context;}public Object key(String keyExpression, EvaluationContext evalContext) {Expression expression = parser.parseExpression(keyExpression);return expression.getValue(evalContext);}public boolean condition(String conditionExpression, EvaluationContext evalContext) {if(!StringUtils.hasText(conditionExpression)) {return true;}Expression expression = parser.parseExpression(conditionExpression);return (Boolean.TRUE.equals(expression.getValue(evalContext, Boolean.class)));}public boolean unless(String unlessExpression, EvaluationContext evalContext) {if(StringUtils.hasText(unlessExpression)) {return false;}Expression expression = parser.parseExpression(unlessExpression);return (Boolean.TRUE.equals(expression.getValue(evalContext, Boolean.class)));}}

总结
自定义缓存注解使用了AOP的通知功能,所以需要开启AOP,需要在配置类上加上@EnableAspectJAutoProxy注解。
改进:可使用类似@EnableCache注解导入一个入口类,不再需要多个切面,多个注解的处理逻辑放在一起,参考Spring Cache。
扩展:解析表达式可使用缓存,加快解析速度;方法上面支持多个@Cached注解。