spel基本原理

spel基本原理

spel简介

前言

Spring Expression Language(简称 SpEL)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言。它的语法类似于传统 EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。

在我们离不开Spring框架的同时,其实我们也已经离不开SpEL了,因为它太好用、太强大了。此处我贴出官网的这张图:

img

从图中可以看出SpEL的重要,它在Spring家族中如同基石一般的存在。 SpEL是spring-expression这个jar提供给我们的功能,它从Spring3.x版本开始提供

备注:SpEL并不依附于Spring容器,它也可以独立于容器解析。因此,我们在书写自己的逻辑、框架的时候,也可以借助SpEL定义支持一些高级表达式~ 需注意一点若看到这么用:#{ systemProperties['user.dir'] },我们知道systemProperties是Spring容器就内置的,还有systemEnvironment等等都是可以直接使用的

基本原理

image-20231117180846133

任何语言都需要有自己的语法,SpEL当然也不例外。所以我们应该能够想到,给一个字符串最终解析成一个值,这中间至少得经历:

字符串 -> 语法分析 -> 生成表达式对象 -> (添加执行上下文) -> 执行此表达式对象 -> 返回结果

关于SpEL的几个概念:

  1. 表达式(“干什么”)SpEL的核心,所以表达式语言都是围绕表达式进行的
  2. 解析器(“谁来干”):用于将字符串表达式解析为表达式对象
  3. 上下文(“在哪干”):表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等
  4. root根对象及活动上下文对象(“对谁干”):root根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象

这是对于解析一个语言表达式比较基本的一个处理步骤,为了更形象的表达出意思,绘制一幅图友好展示如下:

img

步骤解释:

  1. 按照SpEL支持的语法结构,写出一个expressionStr
  2. 准备一个表达式解析器ExpressionParser,调用方法parseExpression()对它进行解析。这一步至少完成了如下三件事:
    1. 使用一个专门的断词器Tokenizer,将给定的表达式字符串拆分为Spring可以认可的数据格式
    2. 根据断词器处理的操作结果生成相应的语法结构
    3. 在这处理过程之中就需要进行表达式的对错检查(语法格式不对要精准报错出来)
  3. 将已经处理好后的表达式定义到一个专门的对象Expression里,等待结果
  4. 由于表达式内可能存在占位符变量${},所以还不太适合马上直接getValue()(若不需要解析占位符那就直接getValue()也是可以拿到值的()。所以在计算之前还得设置一个表达式上下文对象EvaluationContext(这一步步不是必须的))
  5. 替换好占位符内容后,利用表达式对象计算出最终的结果

ExpressionParser:表达式解析器

将表达式字符串解析为可计算的已编译表达式。支持分析模板(Template)标准表达式字符串。 它是一个抽象,并没有要求具体的语法规则,Spring实现的语法规则是:SpEL语法。

1
2
3
4
5
6
7
// @since 3.0
public interface ExpressionParser {
// 他俩都是把字符串解析成一个Expression对象
//备注expressionString都是可以被repeated evaluation的
Expression parseExpression(String expressionString) throws ParseException;
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}

ExpressionParser的继承树如下

img

TemplateAwareExpressionParser

它是一个支持解析模版Template的解析器,抽象类。

SpelExpressionParser

SpEL parser该实例是可重用的和线程安全的

1
2
ExpressionParser parpser = new SpelExpressionParser();
Expression exp = parpser.parseExpression(expressionStr);
1
2
3
4
5
// 这里需要注意:因为是new的,所以每次都是一个新对象,所以它是线程安全的~
@Override
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
return new InternalSpelExpressionParser(this.configuration).doParseExpression(expressionString, context);
}

SpelParserConfiguration表示:顾名思义它表示SpEL的配置类。在构建SpelExpressionParser时我们可以给其传递一个SpelParserConfiguration对象以对SpelExpressionParser进行配置。其可以用于指定在遇到List或Array为null时是否自动new一个对应的实例(一般不建议修改此值~以保持语义统一)。StandardBeanExpressionResolver也使用到了它。

InternalSpelExpressionParser

上面知道SpelExpressionParser最终都是委托它里做的,并且configuration也交给它,然后调用doParseExpression方法处理。

SpEL对Template模式支持
1
2
3
4
5
6
7
8
9
10
11
public class _02_spel_template {
public static void main(String[] args) {
String greetingExp = "Hello, #{#user} ---> #{T(System).getProperty('user.home')}";
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", "username");

Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext());
System.out.println(expression.getValue(context, String.class));
}
}

这个功能就有点像加强版的字符串格式化了。它的执行步骤描述如下:

  1. 创建一个模板表达式,所谓模板就是带字面量和表达式的字符串。其中#{}表示表达式的起止。上面的#user是表达式字符串,表示引用一个变量(注意这个写法,有两个#号)
  2. 解析字符串。其实SpEL框架的抽象是与具体实现无关的,只是我们这里使用的都是SpelExpressionParser
  3. 通过evaluationContext.setVariable可以在上下文中设定变量。
  4. 使用Expression.getValue()获取表达式的值,这里传入了Evalution上下文,第二个参数是类型参数,表示返回值的类型。

只有Template模式的时候,才需要#{},不然SpEL就是里面的内容即可,如1+2就是一个SpEL 至于@Value为何需要#{spel表示是内容}这样包裹着,是因为它是这样的expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext);,也就是说它最终是parseTemplate()这个去解析的。 如果parse的时候传的context是null啥的,就不会解析外层#{}了

ParserContext:解析器上下文

ParserContext:提供给表达式分析器的输入,它可能影响表达式分析/编译例程。它会对我们解析表达式字符串的行为影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface ParserContext {

// 是否是模版表达式。 比如:#{3 + 4}
boolean isTemplate();
// 模版的前缀、后缀 子类是可以定制化的~~~
String getExpressionPrefix();
String getExpressionSuffix();

// 默认提供的实例支持:#{} 的形式 显然我们可以改变它但我们一般并不需要这么去做~
ParserContext TEMPLATE_EXPRESSION = new ParserContext() {
@Override
public boolean isTemplate() {
return true;
}
@Override
public String getExpressionPrefix() {
return "#{";
}
@Override
public String getExpressionSuffix() {
return "}";
}
};
}

它只有一个实现类:TemplateParserContext,另外ParserContext.TEMPLATE_EXPRESSION也是该接口的一个内部实现,两者实现细节一样。这两者都是属于org.springframework.expression包内的。在org.springframework.context.expression.StandardBeanExpressionResolver类中,还有一个成员变量实现了这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自定义参数解析器上下文
public class _01_custome_parse_context {

public static void main(String[] args) {
String greetingExp = "Hello, @< #user >";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext("@<", ">"));

EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", "xiaoxi");
final String value = expression.getValue(context, String.class);
System.out.println(value); //Hello, xiaoxi
}
}

Expression

表示的是表达式对象。能够根据上下文对象对自身进行计算的表达式。封装以前分析的表达式字符串的详细信息。

它的继承树如下:

img

SpelExpression

这个是我们核心,甚至也是目前SpEL的唯一实现。 表达式可以独立计算,也可以在指定的上下文中计算。在表达式计算期间,可能会要求上下文解析:对类型、bean、属性和方法的引用。

这个是我们最主要的一个Expression表达式,AST是它的心脏。

LiteralExpression

Literal:字面意义的 它没有计算的活,只是表示字面意思(字面量)。 so,它里面处理的类型:全部为String.class,并且和**EvaluationContext**无关

CompositeStringExpression

表示一个分为多个部分的模板表达式(它只处理Template模式)。每个部分都是表达式,但模板的纯文本部分将表示为LiteralExpression对象。显然它是一个聚合。这个表达式的计算中,和EvaluationContext这个上下文有莫大的关系,因此有必要看看它。

EvaluationContext:评估/计算的上下文

表达式在计算上下文中执行。在表达式计算期间遇到引用时,正是在这种上下文中解析引用。它的默认实现为:StandardEvaluationContext

EvaluationContext可以理解为parser 在这个环境里执行parseExpression解析操作。 比如说我们现在往ctx(一个EvaluationContext )中放入一个 对象list (注:假设list里面已经有数据,即list[0]=true)

1
ctx.setVariable("list" , list); //可以理解为往ctx域 里放了一个list变量

接下来要想获取或设置list的值都要在ctx范围内才能找到:

1
2
parser.parseExpression("#list[0]").getValue(ctx);//在ctx这个环境里解析出list[0]的值
parser.parseExpression("#list[0]").setValue(ctx , "false");//在ctx这个环境中奖 list[0]设为false

假如我们又往ctx中放入一个person对象(假设person已经实例化并且person.name的值是fsx

1
ctx.setVariable("p", person);

那么取其中name属性要像下面这样:

1
parser.parseExpression("#p.name").getValue(ctx);//结果是 fsx

但若是将ctx的root设为person 取name的时候就可以省略root对象这个前缀("#")了

1
2
3
4
//StandardEvaluationContext是EvaluationContext的子类 提供了setRootObject方法
((StandardEvaluationContext)ctx2).setRootObject(person);
parser.parseExpression("name").getValue(ctx2); //访问rootobject即person的属性那么 结果:fsx// 这种方式同
parser.parseExpression("name").getValue(person); //它的意思是直接从root对象里找~~~~

这样获取name就会去root对象里直接找 而不用#p这样子了~~~~ 这就是root对象的用处~它在后面的属性访问器中用处更大

EvaluationContext的继承树如下:

img

主要有两个开箱即用的实现:SimpleEvaluationContextStandardEvaluationContext

SimpleEvaluationContext

公开仅支持部分的SpEL的支持。它有意限制的表达式类别~~ 旨在仅支持SpEL语言语法的一个子集,它不包括 Java类型引用,构造函数和bean引用等等。它还要求明确选择对表达式中属性和方法的支持级别。

StandardEvaluationContext

公开支持全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

使用 setRootObject 方法来设置根对象,使用 setVariable 方法来注册自定义变量,使用 registerFunction 来注册自定义函数等等(registerFunction 方法进行注册自定义函数,其实完全可以使用 setVariable 代替,两者其实本质是一样的)。

EvaluationContext可以包含多个对象,但只能有一个root对象

1
EvaluationContext`可以包含多个对象,**但只能有一个root对象**。 当表达式中包含变量时,`SpEL`就会根据`EvaluationContext`中变量的值对表达式进行计算。 往`EvaluationContext`里放入对象方法:`setVariable(String name,Object value)`;向`EvaluationContext`中放入`value`对象,该对象名为`name

需要注意的是EvaluationContext接口中并没有定义设置root对象的方法,所以我们可以在StandardEvaluationContext里来设置root对象:setRootObject(Object rootObject) 默认它用#root取得此root对象,在SpEL中访问root对象的属性时,可以省略#root对象前缀,比如#root.name 可以简写成 name(注意不是写成#name

setVariable()的使用

需要注意一点:setVariable()进去的取值时,是必须指定前缀的。

#root表达式的使用

这个是SpEL中比较重要的一点,因为这个隐藏的表达式在Spring中有比较多的使用,例如:

  • @EventListener注解中condtion属性:#root.event#root.args
  • @Cacheable等缓存相关注解:#root.method #root.target等等非常多

此处我也列出两个常用的场景下的参考类:

  • @EventListener可用属性值参考:org.springframework.context.event.EventExpressionRootObject
  • @Cacheable可用属性值参考:org.springframework.cache.interceptor.CacheExpressionRootObject

SpEL中的PropertyAccessor(重要)

TODO 这个有什么作用?不清楚

PropertyAccessor属性访问器是SpEL中一个非常重要的概念。

PropertyAccessor的继承树如下:

img


参考链接

【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)-CSDN博客