Spring-Starter自动锁组件扩展--支持通过SpEL构建key

前言

我们首先简单了解一下SpEL是什么,来看一下GPT的回答:

SpEL(Spring Expression Language)是 Spring 框架提供的一种强大的表达式语言 ,用于在运行时访问 和操作对象的属性、方法,以及调用各种函数等。SpEL 可以在 Spring 应用中的 XML 配置文件、注解、Spring 表达式模板字符串等地方使用,它提供了一种灵活、简洁的方式来配置和处理应用程序的数据和行为。

从上面GPT的回答中我们可以知道,SpEL 是 一个 表达式语言、作用在 运行时,支持操作 对象的 属性、方法和调用函数。 知道这些大概就能明白这是啥了,他主要就是一个 模板,然后spring会在运行时通过一些机制,来把模板替换成实际的数据。(调用函数这个应该是更高级的用法了,现在咋们先了解一下简单的 模板替换 的操作)。

实现

我们直接上代码,看一下怎么用的:

java 复制代码
@Test
public void test(){
    String spEL = "'anoxia:lock:' + #userDto.id +':'+ #userDto.name";
    log.info("spEL:{}",spEL);
    UserDto userDto = new UserDto();
    userDto.setId(1000000L);
    userDto.setName("hello");
    ExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression(spEL);
    StandardEvaluationContext context = new StandardEvaluationContext();
    context.setVariable("userDto", userDto);
    String value = expression.getValue(context, String.class);
    log.info("测试结果:{}",value);
}

基本格式 #字段名#对象.字段名#对象.对象.字段名等。我们知道这几种基本就够用了。

上面的代码运行结果:

效果就是,对象具体的值替换了 #对象.字段名 的内容。

根据上面的代码我们可以把整个解析过程分解为三步:

1、创建解析器,把表达式传入解析器里面去(这里有点 正则解析 的味道)

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spEL);

2、创建内容上下文,就是被解析的目标对象,需要把这个对象设置进去。

java 复制代码
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("userDto", userDto);

3、解析 表达式,获取解析结果。

java 复制代码
String value = expression.getValue(context, String.class);

我们可以看到,主要涉及到的类,就是 SpelExpressionParserExpressionStandardEvaluationContext ,下面我们深入了解一下这几个类的作用:

  1. SpelExpressionParser: 这个类是 SpEL 表达式的解析器,它负责将 SpEL 表达式解析成内部的数据结构,然后进行求值。SpelExpressionParser 可以解析 SpEL 表达式字符串,并生成一个 Expression 对象,表示这个表达式。然后,我们可以使用这个 Expression 对象对表达式进行求值。
  2. StandardEvaluationContext: 这个类是 SpEL 的求值上下文,它提供了表达式求值时的环境信息。StandardEvaluationContext 中包含了一些变量、函数等信息,用于表达式的求值过程中。我们可以在这个上下文中设置变量、函数等信息,以供 SpEL 表达式求值时使用。

在实际使用 SpEL 的过程中,我们首先需要创建一个 StandardEvaluationContext 对象,然后可以使用 SpelExpressionParser 解析 SpEL 表达式,最后使用 Expression 对象对表达式进行求值。

上面的比较正式,可能理解起来还是有些复杂。我们可以这样理解, SpelExpressionParser == 工厂 、 Expression == 机器 、StandardEvaluationContext == 原料。

所以就是:工厂 里面 拿一个 机器 ,然后输入原料,最后面 获得 产品 == 最后解析就结果。 我自己是这样去理解的,这样的话,看起来就比较简单,对于我自己来说也更容易接受。大家视情况去理解。

使用

既然已经了解过实现的机制,那么这个趁热打铁,使用他做点什么。上一次做 2、Springboot-Starter造轮子之自动锁组件(lock-starter) - 掘金 的时候,有大佬提了一些建议,可以使用 SpEL 去处理下 key 构建的问题。

那我们就使用学到的知识,来实现一下这个功能。

AutoLock 注解的改动

添加一个参数,来接受 SpEL 表达式。

java 复制代码
/**
 * 锁的基本信息
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLock {
    ....
    /**
     * 锁的key ,支持 SpEL
     * @return
     */
    String key() default "";
    .....
}

SpringELUtils SpEL 解析工具类

这里里面的解析过程,个人感觉比较死,不知道有什么办法可以动态的获取到 目标对象,目前来说只是通过简单的分割匹配来获取到目标对象。(太菜了)

java 复制代码
/**
 * 解析 spEL 表达式
 * @author huangle
 * @date 2024/2/29 - 17:12
 */
public class SpringELUtils {

    public static final String SPEL_START_SYMBOL = "#";

    /**
     * #id
     * #obj.id
     * #obj.obj.id
     * @param spEL
     * @param tarObj
     * @return
     */
    public static String parseSpEL(String spEL, Object tarObj) {
        if (!spEL.startsWith(SPEL_START_SYMBOL)) {
            return null;
        }
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spEL);
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariable(extractValue(spEL), tarObj);
        return expression.getValue(context, String.class);
    }


    private static String extractValue(String expression) {
        String[] split = expression.split(":");
        if (split.length > 1) {
            expression = split[0];
        }
        expression = expression.substring(1); // 去掉 #
        int lastDotIndex = expression.lastIndexOf('.');
        if (lastDotIndex != -1) {
            return expression.substring(0, lastDotIndex);
        }
        return expression; // 没有点,只有一个对象只,直接返回
    }

}

AutoLockAspect 变动

这里的改动就只是判断使用了那种方式去构建key,没有把原来的方式删掉,只是把新的构建方式添加了上去。

java 复制代码
// 获取锁前缀
        String prefix = autoLock.prefix();
        String key = autoLock.key();
        StringBuilder lockKeyStr = new StringBuilder(prefix);
        if (!StringUtils.isEmpty(key)) {
            String keyValue = SpringELUtils.parseSpEL(key, joinPoint.getArgs()[0]);
            lockKeyStr.append(keyValue);
        } else {
            // 获取方法参数
            Parameter[] parameters = method.getParameters();
            Object[] args = joinPoint.getArgs();
            // 遍历参数
            int index = -1;
            LockField lockField;
            for (Parameter parameter : parameters) {
                Object arg = args[++index];
                lockField = parameter.getAnnotation(LockField.class);
                if (lockField == null) {
                    continue;
                }
                String[] fieldNames = lockField.fieldNames();
                String[] spELs = lockField.spELs();
                if (fieldNames == null || fieldNames.length == 0) {

                    lockKeyStr.append(SEPARATOR).append(arg);
                } else {
                    List<Object> filedValues = ReflectionUtil.getFiledValues(parameter.getType(), arg, fieldNames);
                    for (Object value : filedValues) {
                        lockKeyStr.append(SEPARATOR).append(value);
                    }
                }
            }
        }
        String lockKey = REDIS_LOCK_PREFIX + SEPARATOR + lockKeyStr;

测试

简单看一下我们的测试代码,直接使用 key = "#userDto.id + ':' + #userDto.name"。其实我在想如果有多个参数的时候,在切面哪里该怎么去处理。目前看来上面哪个解析类是处理不了的。(这里留一个bug吧,后面看有什么办法解决一下。)按道理正常情况肯定应该被解决的,奈何自己太菜,各位大佬有什么想法可以提一下,我会尝试看能不能去实现。

java 复制代码
@PostMapping("/userInfo/v2")
@AutoLock(lockTime = 1, timeUnit = TimeUnit.MINUTES, key = "#userDto.id + ':' + #userDto.name")
public String userInfoV2(@RequestBody UserDto userDto){
    return userDto.getId()+":"+userDto.getName();
}

目前看来,这个不那么完整的 SpEL 表达式处理是可以正常的构建这个key的。

总结

我自己经常陷入一种死循环,看了一个知识点,感觉好像很简单啊,但是过了两天,啥、啥、这是啥。就基本都忘的干净了,看了很多,最后面发现,能记下的寥寥无几,然后进一步出现焦虑、烦躁、厌学、各种情绪接踵而来。后面发现,如果自己尝试去做一下,对于一个知识点的理解就会多那么一点。如果后面一直没有用到的话,可能还是会忘记,但是下一次看到的时候,至少不会出现 我啥时候看过这种窘迫。 慢慢升级打怪,慢一点也没关系。

相关推荐
一只叫煤球的猫5 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9655 小时前
tcp/ip 中的多路复用
后端
bobz9655 小时前
tls ingress 简单记录
后端
皮皮林5516 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友6 小时前
什么是OpenSSL
后端·安全·程序员
bobz9657 小时前
mcp 直接操作浏览器
后端
前端小张同学9 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook9 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康10 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在10 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net