springboot:自定義緩存注解,實現生存時間需求


需求背景:在使用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操作公共類,大家可以用自己的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM