那么在经历了前面两片文章《基于AOP的Redis缓存注解功能开发设计》和《JexlEngine表达式引擎的简单使用》之后,我们便可以将JexlEngine表达式引擎在AOP缓存中使用起来,通过表达式引擎的定义来自定义每个缓存的Key或者每组类型的缓存的Key,从而达到在不同的代码逻辑中增删改有相同组别或者类型关联的缓存数据的需求

如何指定生成Key规则

在之前的Redis缓存注解一文中,我们定义的注解中有个Key变量

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCache {
    String value();

    String key() default "";
}

原本我们在使用的时候,value()用来指定在Redis中的Key,而hashKey则用注解作用在的方法的所有参数组合之后的Array来担当。现在我们把注解中的key()用上,用来填写对应的hashKey的生成规则,填写的内容则是一个JexlEngine的表达式。下面,写个Demo。

@RedisCache(value = "dict_cache", key = "paramStr+'_'+paramInteger+'_'+chars[0]+'_'+paramInt+'_'+paramDto.getCodeParam()")
public Result<DictionaryInfo> cacheParamExample(String paramStr, Integer paramInteger, ParamDto paramDto, char[] chars, int paramInt, Integer integer, double v){
    DictionaryInfo dictionaryInfo = new DictionaryInfo();
    dictionaryInfo.setDictName("DIC");
    dictionaryInfo.setDictCode("CODE");
    return new Result<>(dictionaryInfo);
}

表达式写得有点夸张,不过主要是做个展示,这里也不会用到太复杂的JexlEngine表达式,其根本在于用几个给予的对象拼接一个字符串,不会涉及到JexlEngine提供的各种复杂的功能。这边更新下之前文章中切面类RedisCacheAspect的代码

/**
 * RedisCacheAspect<br>
 *
 * @author CheungQ
 * @date 2023/4/12 10:57
 */
@Aspect
@Component
public class RedisCacheAspect {

    private static Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    private StringRedisTemplate stringRedisTemplate;

    @PostConstruct
    public void init() {
        stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        stringRedisTemplate.setHashKeySerializer(RedisSerializer.byteArray());
        stringRedisTemplate.setHashValueSerializer(RedisSerializer.byteArray());
        stringRedisTemplate.afterPropertiesSet();
    }

    @Pointcut(value = "@annotation(com.cheungq.demo.cache.annotation.RedisCache)")
    public void pointcut(){}

    @SneakyThrows
    @Around(value = "pointcut()")
    public Object redisCacheAround(ProceedingJoinPoint joinPoint){
        MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
        Method method = joinPointObject.getMethod();
        RedisCache cacheAnnotation = method.getAnnotation(RedisCache.class);
        Class<?> returnType = method.getReturnType();
        String key = cacheAnnotation.value();
        Map<String, Object> paramMap = paramMap(joinPointObject.getMethod().getParameters(), joinPoint.getArgs());
        String jexlExp = cacheAnnotation.key();
        String hKey = (String)generateHashKey(jexlExp,paramMap);
        Object fromRedis = getFromRedis(key, hKey, returnType);
        if (null == fromRedis){
            logger.info("new CACHE to " +key+" hashKey: "+ hKey);
            Object res = joinPoint.proceed();
            cacheToRedis(key,hKey,res);
            return res;
        }
        logger.info("get from CACHE to " +key+" hashKey: "+ hKey);
        return fromRedis;
    }

    Map<String,Object> paramMap(Parameter[] parameters, Object[] args){
        if(parameters.length != args.length){
            return null;
        }
        Map<String,Object> map = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            map.put(parameters[i].getName(),args[i]);
        }
        return map;
    }


    private void cacheToRedis(String key, Object hashKey, Object hashVal){
        if (null == hashVal){
            return;
        }
        try {
            stringRedisTemplate.opsForHash().put(
                    key,
                    SerializeUtil.serialize(hashKey),
                    SerializeUtil.serialize(hashVal)
            );
        }catch (Exception e){
            logger.error("写入Redis异常",e);
        }
    }

    private <T> T getFromRedis(String key, Object hashKey, Class<T> clazz){
        try {
            byte[] res = (byte[])stringRedisTemplate.opsForHash().get(
                    key,
                    SerializeUtil.serialize(hashKey)
            );
            if (res == null) {
                return null;
            }
            return SerializeUtil.deserialize(res,clazz);
        }catch (Exception e){
            logger.error("读取Redis异常",e);
            return null;
        }
    }



    public static Object generateHashKey(String jexlExp, Map<String, Object> map) {
        JexlEngine jexl = new Engine();
        JexlExpression e = jexl.createExpression(jexlExp);
        JexlContext jc = new MapContext();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            jc.set(entry.getKey(), entry.getValue());
        }
        Object res = e.evaluate(jc);
        return null == res?"":res;
    }
}

如果嫌代码太多不大方便看,可以摘取出来

第一部分,在@Around注解作用的redisCacheAround方法中,修改了如下部分

Map<String, Object> paramMap = paramMap(joinPointObject.getMethod().getParameters(), joinPoint.getArgs());
String jexlExp = cacheAnnotation.key();
String hKey = (String)generateHashKey(jexlExp,paramMap);

将方法参数封装成一个Map,为了之后给JexlEngine提供参数,以及获取JexlEngine表达式,再调用generateHashKey方法,使用JexlEngine根据表达式和参数Map构建出我们需要的hashKey。

Map<String,Object> paramMap(Parameter[] parameters, Object[] args){
    if(parameters.length != args.length){
        return null;
    }
    Map<String,Object> map = new HashMap<>();
    for (int i = 0; i < parameters.length; i++) {
        map.put(parameters[i].getName(),args[i]);
    }
    return map;
}
public static Object generateHashKey(String jexlExp, Map<String, Object> map) {
    JexlEngine jexl = new Engine();
    JexlExpression e = jexl.createExpression(jexlExp);
    JexlContext jc = new MapContext();
    for (Map.Entry<String, Object> entry : map.entrySet()) {
        jc.set(entry.getKey(), entry.getValue());
    }
    Object res = e.evaluate(jc);
    return null == res?"":res;
}

实际测试

改造好这些之后,我们再写个方法调用下

@RequestMapping(value = "test", params = "paramStr")
public Object testCache(@RequestParam String paramStr) {
    ParamDto paramObj = new ParamDto();
    paramObj.setCodeParam("code_param_ABC");
    List<Long> list = new ArrayList<>();
    list.add(12345L);
    list.add(67890L);
    paramObj.setListParam(list);
    return dictionaryService.cacheParamExample(
            paramStr,
            123,
            paramObj,
            new char[]{'c','h','a','r','s'},
            -10086,
            new Integer(9527),
            Math.PI
    );
}

执行代码,打个断点看下,请求地址为“/cache/test?paramStr=a_param_str”,即paramStr变量赋值为“a_param_str”,在断点中可以得到hashKey

Redis中缓存写入成功

这样我们就对原来的缓存注解功能完成了改造,允许开发者自行设计hashKey的生成规则