search.png
关于我
menu.png
java 实现的数据查询缓存通用模型——SPEL表达式支持(6)

收录于墨的2020~2021开发经验总结

接续上文(java 实现的数据查询缓存通用模型——那些我在缓存模型中用到的工具类(5)

7、SPEL表达式支持

spel表达式,SpEL(Spring Expression Language),即spring 表达式语言,它是一种轻量的、灵活的语言,通过spring的解析工具进行解析。
在前文中,我们搭建实现的数据查询缓存通用模型,是通过cacheKeyCreater的机制来生成key的,这种方式可以实现很高的定制化,但是也有个缺点,那就是要写的代码更多,也不够直观。例如,先前的构造缓存按ID查询产品的Key,它的cacheKeyCreater代码需要写这么多:

public class ProductIdCacheKeyCreator implements DawnCacheKeyCreator {

    public final static String CACHE_PER = "DAWN-CACHE-PRODUCT-ID";

    /**
     * 它的规则是:如果从参数中直接取到了id,那么直接使用该ID;如果参数中不包含ID,而是包含product,那么取product的id;之后使用前缀DAWN-CACHE-PRODUCT-ID加上`.`加上ID生成了KEY。
     *
     * @param request 请求
     * @param target  注解的方法的所在对象
     * @param method  注解的方法
     * @param params  方法携带的参数Map<String,Object>
     * @return Key 字符串
     */
    @Override
    public String createCacheKey(HttpServletRequest request, Object target, Method method, Map<String, Object> params) {
        Object id = params.get("id");
        if (id == null) {
            Object product = params.get("product");
            if (product instanceof Product p) {
                id = p.getId();
            }
        }
        if ((!(id instanceof Long))) {
            return null;
        }
        return String.format("%s.%d", CACHE_PER, id);
    }

    @Override
    public String createCacheEvictKey(String cacheMethodName, HttpServletRequest request, Object target, Method method, Map<String, Object> params) {
        return createCacheKey(request, target, method, params);
    }
}

spel表达式可以很好的弥补这个缺点。改成spel表达式之后:

设置缓存:

@DawnCacheable(sync = false, cacheTimeUnit = TimeUnit.MILLISECONDS, expire = 50000,
            spel = "'DAWN-CACHE-PRODUCT-ID.' + #params['id']")

清除缓存:

@DawnCacheEvict(name = "evictGetProductById", spel = "'DAWN-CACHE-PRODUCT-ID.' + #params['id']"),

一行就可以了,代码写的很少。

为了添加对spel表达式的支持,增加了一个spel handler类:

package cn.hengyumo.dawn.core.cache.core;

import cn.hengyumo.dawn.utils.SpelUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.EvaluationContext;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;

@Slf4j
public class DawnCacheSpelHandler {

    public static EvaluationContext buildContext(DawnCacheable dawnCacheable,
                                                 DawnCacheEvict dawnCacheEvict,
                                                 HttpServletRequest request,
                                                 Object target,
                                                 Method method,
                                                 Map<String, Object> params) {
        EvaluationContext context = SpelUtil.buildBaseContext(request, target, method, params);
        context.setVariable("dawnCacheable", dawnCacheable);
        context.setVariable("dawnCacheEvict", dawnCacheEvict);
        context.setVariable("user", null);
        context.setVariable("userName", null);
        context.setVariable("userId", null);
        return context;
    }

    public static String parseLogSpel(EvaluationContext context, String spel) {
        return SpelUtil.parseLogSpel(context, spel);
    }
}

然后在AOP 拦截方法中做了一些改造:


    /**
     * 解析spel表达式生成key
     *
     * @param dawnCacheable 注解
     * @param target 目标对象
     * @param method 目标方法
     * @param params 参数
     * @param spel spel表达式
     * @return key
     */
    private String createKeyBySpel(DawnCacheable dawnCacheable, DawnCacheEvict dawnCacheEvict,
                                   Object target, Method method,
                                   Map<String, Object> params, String spel) {
        EvaluationContext context = DawnCacheSpelHandler.buildContext(
                dawnCacheable, dawnCacheEvict, AspectUtil.getRequest(), target, method, params);
        return DawnCacheSpelHandler.parseLogSpel(context, spel);
    }

    /**
     * 使用 DawnCacheKeyCreator 生成key
     *
     * @param dawnCacheable DawnCacheable注解
     * @param dawnCacheEvict DawnCacheEvict注解
     * @param target 目标对象
     * @param method 目标方法
     * @param params 参数
     *
     * @return key
     */
    private String createKeyByDawnCacheKeyCreator(DawnCacheable dawnCacheable, DawnCacheEvict dawnCacheEvict,
                                                  Object target, Method method,
                                                  Map<String, Object> params) {
        if (dawnCacheable != null) {
            return getDawnCacheKeyCreator(dawnCacheable.cacheKeyCreator())
                    .createCacheKey(AspectUtil.getRequest(), target, method, params);
        } else if (dawnCacheEvict != null) {
            return getDawnCacheKeyCreator(dawnCacheEvict.cacheKeyCreator())
                    .createCacheEvictKey(dawnCacheEvict.method(), AspectUtil.getRequest(), target, method, params);
        }
        return null;
    }

    /**
     * 生成缓存 key
     *
     * @param dawnCacheable 注解
     * @param proceedingJoinPoint 切点
     * @return key字符串
     */
    private String createKey(DawnCacheable dawnCacheable, DawnCacheEvict dawnCacheEvict,
                             ProceedingJoinPoint proceedingJoinPoint) {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        String key = null;
        boolean created = false;
        Class<? extends DawnCacheKeyCreator> cacheKeyCreator;
        String spel;
        if (dawnCacheable != null) {
            cacheKeyCreator = dawnCacheable.cacheKeyCreator();
            spel = dawnCacheable.spel();
        } else if (dawnCacheEvict != null) {
            cacheKeyCreator = dawnCacheEvict.cacheKeyCreator();
            spel = dawnCacheEvict.spel();
        } else {
            return null;
        }
        // 如果自定义设置了cacheKeyCreator,那么其优先级高于 spel表达式
        if (cacheKeyCreator == DawnDefaultCacheKeyCreator.class) {
            if (StringUtil.isNotEmpty(spel)) {
                created = true;
                key = createKeyBySpel(dawnCacheable, dawnCacheEvict, proceedingJoinPoint.getTarget(),
                        signature.getMethod(), AspectUtil.getParams(proceedingJoinPoint), spel);
            }
        }
        // 如果未用spel表达式创建,则使用cacheKeyCreator创建
        if (!created) {
            key = createKeyByDawnCacheKeyCreator(dawnCacheable, dawnCacheEvict,
                    proceedingJoinPoint.getTarget(), signature.getMethod(),
                    AspectUtil.getParams(proceedingJoinPoint));
        }
        return key;
    }

原有的createKey被拆分成两个,根据传入的注解参数动态变化,如果自定义设置了cacheKeyCreator,那么其优先级高于 spel表达式。

这一点是考虑到 cacheKeyCreator的灵活性是优于spel表达式的,毕竟spel表达式不好写的太长,并且也不好复用逻辑。对于一些复杂的key的生成,还是需要使用cacheKeyCreator。

然而大部分情况我相信spel都足以满足key的生成。

这篇文章算是一篇番外篇了,起因也是在另一个系列的文章——基于Redis的日志缓存工具中使用了spel表达式,发现还挺不错的,后边就决定在Dawn的缓存模型中试了一下。发现还不错,没有改太多的代码。

END

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年07月13日 11:37:54

评论区#

还没有评论哦,期待您的评论!

关闭特效