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的。

总结

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

相关推荐
Bonne journée6 分钟前
‌在Python中,print(f‘‘)是什么?
java·开发语言·python
潘多编程22 分钟前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_8575893627 分钟前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_8576226628 分钟前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
小懒编程日记33 分钟前
【数据结构与算法】B树
java·数据结构·b树·算法
Y_3_744 分钟前
【回溯数独】有效的数独(medium)& 解数独(hard)
java·数据结构·windows·算法·dfs·回溯
RangoLei_Lzs1 小时前
C++模版SFIANE应用踩的一个小坑
java·开发语言·ui
北极无雪1 小时前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
VXbishe1 小时前
(附源码)基于springboot的“我来找房”微信小程序的设计与实现-计算机毕设 23157
java·python·微信小程序·node.js·c#·php·课程设计
YONG823_API2 小时前
电商平台数据批量获取自动抓取的实现方法分享(API)
java·大数据·开发语言·数据库·爬虫·网络爬虫