前言
我们首先简单了解一下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);
我们可以看到,主要涉及到的类,就是 SpelExpressionParser
、Expression
、 StandardEvaluationContext
,下面我们深入了解一下这几个类的作用:
- SpelExpressionParser: 这个类是 SpEL 表达式的解析器,它负责将 SpEL 表达式解析成内部的数据结构,然后进行求值。
SpelExpressionParser
可以解析 SpEL 表达式字符串,并生成一个Expression
对象,表示这个表达式。然后,我们可以使用这个Expression
对象对表达式进行求值。 - 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的。
总结
我自己经常陷入一种死循环,看了一个知识点,感觉好像很简单啊,但是过了两天,啥、啥、这是啥
。就基本都忘的干净了,看了很多,最后面发现,能记下的寥寥无几,然后进一步出现焦虑、烦躁、厌学、各种情绪接踵而来。后面发现,如果自己尝试去做一下,对于一个知识点的理解就会多那么一点。如果后面一直没有用到的话,可能还是会忘记,但是下一次看到的时候,至少不会出现 我啥时候看过
这种窘迫。 慢慢升级打怪,慢一点也没关系。