需求背景:在使用springbot cache時,發現@cacheabe不能設置緩存時間,導致生成的緩存始終在redis中。
環境:springboot 2.1.5 + redis
解決辦法:利用AOP自定義注解,用SPEL來解釋key表達式。
1.定義注解
package com.test.entity.util.annotation.cache; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface MyCacheable { /** * 緩存key * * @return */ String key(); /** * 是否緩存空值 * * @return */ boolean cacheNull() default false; /** * 生存時間,單位是秒,默認為-1(永不過期) * * @return */ int ttl() default -1; /** * 生存狀態 * * true:每訪問一次,將刷新存活時間 * * false:不刷新存活時間,時間一到就清除 * * @return */ boolean state() default true; }
2.實現AOP
package com.test.service.aop; import java.lang.reflect.Method; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheConfig; import org.springframework.context.annotation.Lazy; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import com.test.entity.util.annotation.cache.MyCacheable; import com.test.util.redis.RedisUtil; @Aspect @Component @Lazy(false) public class AspectCacheable { private Logger log = LoggerFactory.getLogger(AspectCacheable.class); @Autowired private RedisUtil redisUtil; /** * 定義切入點 */ @Pointcut("@annotation(com.test.entity.util.annotation.cache.MyCacheable)") private void cut() { // do nothing } /** * 環繞通知 */ @Around("cut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 讀取緩存注解 MyCacheable myCacheable = this.getMethodAnnotation(joinPoint); // 讀取類注解 CacheConfig cacheConfig = this.getClassAnnotation(joinPoint); // 獲取方法傳入參數 Object[] params = joinPoint.getArgs(); // 獲得解釋之后的key String strKey = this.getKey(cacheConfig, myCacheable, params); log.debug("解釋之后的key:{}", strKey); // 在方法執行前判斷是否存在緩存 Object object = this.getCache(strKey, myCacheable.state(), myCacheable.ttl()); if (object == null) { // 創建緩存 object = this.createCache(joinPoint, strKey, myCacheable); } return object; } /** * 獲取方法中聲明的注解 * * @param joinPoint * @return * @throws NoSuchMethodException */ private MyCacheable getMethodAnnotation(JoinPoint joinPoint) throws NoSuchMethodException { // 獲取方法名 String methodName = joinPoint.getSignature().getName(); // 反射獲取目標類 Class<?> targetClass = joinPoint.getTarget().getClass(); // 拿到方法對應的參數類型 Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes(); // 根據類、方法、參數類型(重載)獲取到方法的具體信息 Method objMethod = targetClass.getMethod(methodName, parameterTypes); // 拿到方法定義的注解信息 return objMethod.getDeclaredAnnotation(MyCacheable.class); } /** * 獲取類中聲明的注解 * * @param joinPoint * @return * @throws NoSuchMethodException */ private CacheConfig getClassAnnotation(JoinPoint joinPoint) throws NoSuchMethodException { // 反射獲取目標類 Class<?> targetClass = joinPoint.getTarget().getClass(); return targetClass.getDeclaredAnnotation(CacheConfig.class); } /** * 讀取現有緩存 * * @param key * 實際key,非key表達式 * @param state * 是否刷新存活時間 * @return */ private Object getCache(String key, boolean state, int ttl) { Object obj = redisUtil.get(key); if (obj != null && state && ttl != -1) { // 存在緩存&每次訪問重置TTL&非永不過期 // 每次訪問后重新刷新TTL,還原為原來值 redisUtil.expire(key, ttl); } return obj; } /** * 解析key表達式,得到實際的key * * @param myCacheable * @param params * @return */ private String getKey(CacheConfig cacheConfig, MyCacheable myCacheable, Object[] params) { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); // 獲得原始key的表達式 String strSourceKey = myCacheable.key(); int intSeq = -1; String strSearchSeq = null; int intStartPos = 0; // 用SPEL解析表達式 while (++intSeq < params.length) { strSearchSeq = "#p" + intSeq; intStartPos = StringUtils.indexOf(strSourceKey, strSearchSeq, intStartPos); if (intStartPos < 0) { break; } else { ctx.setVariable("p" + intSeq, params[intSeq]); } } // 執行表達式 Expression expression = parser.parseExpression(strSourceKey); String strKey = expression.getValue(ctx).toString(); // 拼接上緩存名稱,spring cache會加上前綴,是在CacheConfig中配置的。 if (cacheConfig != null) { strKey = cacheConfig.cacheNames()[0] + ":" + strKey; } return strKey; } /** * 創建緩存 * * @param joinPoint * @param strKey * @param myCacheable * @return * @throws Throwable */ private Object createCache(ProceedingJoinPoint joinPoint, String strKey, MyCacheable myCacheable) throws Throwable { // 沒有緩存則執行目標方法 // 獲取目標方法的名稱 String methodName = joinPoint.getSignature().getName(); log.debug("目標執行方法:{}", methodName); // 執行源方法 Object object = joinPoint.proceed(); if (object != null) { // 設置緩存 redisUtil.set(strKey, object); redisUtil.expire(strKey, myCacheable.ttl()); } else { // 判斷是否緩存null if (myCacheable.cacheNull()) { redisUtil.set(strKey, object); } } return object; } }
3.在類上應用注解
@CacheConfig(cacheNames = "coach")
@Service @Transactional public class ServiceImplCoach implements ServiceCoach { private Logger log = LoggerFactory.getLogger(ServiceImplCoach.class); @Autowired private DaoCoach daoCoach;@MyCacheable(key = "'coachnum:'+#p0", ttl = 3600, state = false)
@Override public EntityCoach select(String coachnum) { EntityCoach entityCoach = null; if (StringUtils.isNotBlank(coachnum)) { try { entityCoach = daoCoach.selectByPrimaryKey(coachnum); } catch (Exception e) { log.error("查詢教練員發生錯誤:{}", e); } } else { log.info("查詢教練員,輸入不符合要求"); } return entityCoach; } @CacheEvict(key = "'coachnum:'+#p0") @Override public EntityRes delete(String coachnum) throws Exception { EntityRes entityRes = new EntityRes(); log.debug("刪除教練員,id={}", coachnum); if (StringUtils.isBlank(coachnum)) { log.info("刪除教練員,輸入不符合要求。"); entityRes.setErrorcode(INVALID); } else { daoCoach.deleteByPrimaryKey(coachnum); entityRes.setErrorcode(CODE_SUCC); } return entityRes; } }
RedisUtil 是 redis操作公共類,大家可以用自己的。
