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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)(上)

前言


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


这不得不介绍的SpEL的概念。Sp:Spring,EL:Expression Language。我们熟悉的还有比如JSP中的EL表达式、Struts中的OGNL等等。那那那既然有了它们,为何还要SpEL呢?


SpEL 创建的初衷是给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性基于 Spring 产品的需求而设计,这是它出现的一大特色。


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


image.png


从图中可以看出SpEL的重要,它在Spring家族中如同基石一般的存在。

SpEL是spring-expression这个jar提供给我们的功能,它从Spring3.x版本开始提供~


备注:SpEL并不依附于Spring容器,它也可以独立于容器解析。因此,我们在书写自己的逻辑、框架的时候,也可以借助SpEL定义支持一些高级表达式~

需注意一点若看到这么用:#{ systemProperties['user.dir'] },我们知道systemProperties是Spring容器就内置的,至于为何?之前在分析容器原理的的时候有介绍过~ 还有systemEnvironment等等等等都是可以直接使用的~


关于systemProperties和systemEnvironment具体取值可参考:【小家Java】Java环境变量(Env)和系统属性(Property)详解—工具文章


阅读前准备


需要说明是:本文着眼于SpEL原理、源码层面的剖析,因此阅读本文之前,我默认小伙伴已经是掌握和可以熟练使用SpEL的的,这里贴出两个文档型兄弟博文供以参考:

Spring学习总结(四)——表达式语言 Spring Expression Language

Spring Expression Language(SpEL) 4 学习笔记


SpEL的使用基本总结如下:


  • SpEL 字面量:

- 整数:#{8}

- 小数:#{8.8}

- 科学计数法:#{1e4}

- String:可以使用单引号或者双引号作为字符串的定界符号。

- Boolean:#{true}


  • SpEL引用bean , 属性和方法:

- 引用其他对象:#{car}

- 引用其他对象的属性:#{car.brand}

- 调用其它方法 , 还可以链式操作:#{car.toString()}

- 调用静态方法静态属性:#{T(java.lang.Math).PI}


  • SpEL支持的运算符号:

- 算术运算符:+,-,*,/,%,^(加号还可以用作字符串连接)

- 比较运算符:< , > , == , >= , <= , lt , gt , eg , le , ge

- 逻辑运算符:and , or , not , |

- if-else 运算符(类似三目运算符):?:(temary), ?:(Elvis)

- 正则表达式:#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}’}


基本原理


为了更好的叙述,以一个简单例子作为参照:


    public static void main(String[] args) {
        String expressionStr = "1 + 2";
        ExpressionParser parpser = new SpelExpressionParser(); //SpelExpressionParser是Spring内部对ExpressionParser的唯一最终实现类
        Expression exp = parpser.parseExpression(expressionStr); //把该表达式,解析成一个Expression对象:SpelExpression
        // 方式一:直接计算
        Object value = exp.getValue();
        System.out.println(value.toString()); //3
        // 若你在@Value中或者xml使用此表达式,请使用#{}包裹~~~~~~~~~~~~~~~~~
        System.out.println(parpser.parseExpression("T(System).getProperty('user.dir')").getValue()); //E:\work\remotegitcheckoutproject\myprojects\java\demo-war
        System.out.println(parpser.parseExpression("T(java.lang.Math).random() * 100.0").getValue()); //27.38227555400853
        // 方式二:定义环境变量,在环境内计算拿值
        // 环境变量可设置多个值:比如BeanFactoryResolver、PropertyAccessor、TypeLocator等~~~
        // 有环境变量,就有能力处理里面的占位符 ${}
        EvaluationContext context = new StandardEvaluationContext();
        System.out.println(exp.getValue(context)); //3
    }


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

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

关于SpEL的几个概念:


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


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


image.png


步骤解释:


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



相信从这个Demo可以了解到SpEL处理的一个过程逻辑,有处理流程有一个整体的认识了。那么接下来就是要拆分到各个核心组件的内部,一探究竟~


ExpressionParser:表达式解析器


将表达式字符串解析为可计算的已编译表达式。支持分析模板(Template)和标准表达式字符串。

它是一个抽象,并没有要求具体的语法规则,Spring实现的语法规则是:SpEL语法。


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


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


ParserContext

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也是该接口的一个内部实现,我们可以直接引用)


关于StandardBeanExpressionResolver的内部类实现,也是一个非常基础的实现。关于@Value的原理文章有提到


public class TemplateParserContext implements ParserContext {
  private final String expressionPrefix;
  private final String expressionSuffix;
  // 默认就是它了~~~
  public TemplateParserContext() {
    this("#{", "}");
  }
  @Override
  public final boolean isTemplate() {
    return true;
  }
  @Override
  public final String getExpressionPrefix() {
    return this.expressionPrefix;
  }
  @Override
  public final String getExpressionSuffix() {
    return this.expressionSuffix;
  }
}


~ExpressionParser的继承树如下


image.png


TemplateAwareExpressionParser

它是一个支持解析模版Template的解析器。


// @since 3.0  它是一个抽象类
public abstract class TemplateAwareExpressionParser implements ExpressionParser {
  @Override
  public Expression parseExpression(String expressionString) throws ParseException {
    return parseExpression(expressionString, null);
  }
  @Override
  public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
    // 若指定了上下文,并且是模版 就走parseTemplate
    if (context != null && context.isTemplate()) {
      return parseTemplate(expressionString, context);
    } else {
      // 抽象方法 子类去实现~~~
      return doParseExpression(expressionString, context);
    }
  }
  private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException {
    // 若解析字符串是空串~~~~~
    if (expressionString.isEmpty()) {
      return new LiteralExpression("");
    }
    // 若只有一个模版表达式,直接返回。否则会返回一个CompositeStringExpression,聚合起来的表达式~
    Expression[] expressions = parseExpressions(expressionString, context);
    if (expressions.length == 1) {
      return expressions[0];
    } else {
      return new CompositeStringExpression(expressionString, expressions);
    }
  }
  // ... parseExpressions的实现逻辑  还是稍显复杂的~ 因为支持的case太多了~~~
}


它的子类实现有:InternalSpelExpressionParserSpelExpressionParser


SpelExpressionParser


SpEL parser该实例是可重用的和线程安全的(原因?此处卖个关子,小伙伴可自行想想)

public class SpelExpressionParser extends TemplateAwareExpressionParser {
  private final SpelParserConfiguration configuration;
  public SpelExpressionParser() {
    this.configuration = new SpelParserConfiguration();
  }
  public SpelExpressionParser(SpelParserConfiguration configuration) {
    Assert.notNull(configuration, "SpelParserConfiguration must not be null");
    this.configuration = configuration;
  }
  // 最终都是委托给了Spring的内部使用的类:InternalSpelExpressionParser--> 内部的SpEL表达式解析器~~~
  public SpelExpression parseRaw(String expressionString) throws ParseException {
    return doParseExpression(expressionString, null);
  }
  // 这里需要注意:因为是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一个对应的实例(一般不建议修改此值~以保持语义统一)


// 它是个public类,因为`StandardBeanExpressionResolver`也使用到了它~~~
public class SpelParserConfiguration {
  // OFF IMMEDIATE(expressions are compiled as soon as possible) MIXED
  private static final SpelCompilerMode defaultCompilerMode; 
  static {
    // 它的值可由`spring.properties`里面的配置改变~~~~  所以你可以在你的类路径下放置一个文件,通过`spring.expression.compiler.mode=IMMEDIATE`来控制编译行为
    String compilerMode = SpringProperties.getProperty("spring.expression.compiler.mode");
    defaultCompilerMode = (compilerMode != null ?
        SpelCompilerMode.valueOf(compilerMode.toUpperCase()) : SpelCompilerMode.OFF);
  }
  // 调用者若没指定,会使用上面的默认的~
  private final SpelCompilerMode compilerMode;
  @Nullable
  private final ClassLoader compilerClassLoader;
  // 碰到为null的,是否给自动new一个对象,比如new String(),new ArrayList()等等~
  private final boolean autoGrowNullReferences;
  // 专门针对于集合是否new
  private final boolean autoGrowCollections;
  // 集合能够自动增长到的最大值~~~~
  private final int maximumAutoGrowSize;
  // 省略get/set方法~~~后面会给一个自定义配置的示例~~~
}


InternalSpelExpressionParser



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


// 它是Spring内部使用的类~
class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
  private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+");
  private final SpelParserConfiguration configuration; //SpEL的配置
  // 此处用一个双端队列  来保存表达式的每一个节点,每个节点都是一个SpelNode 该对象记录着位置、子节点、父节点等等~~~
  private final Deque<SpelNodeImpl> constructedNodes = new ArrayDeque<>();
  private String expressionString = ""; // 带解析的表达式字符串~
  // Token流:token保存着符号类型(如int(,]+=?>=等等各种符号 非常之多)  然后记录着它startPos和endPos
  private List<Token> tokenStream = Collections.emptyList();
  // length of a populated token stream
  private int tokenStreamLength;
  // Current location in the token stream when processing tokens
  private int tokenStreamPointer;
  // 唯一的一个构造函数~
  public InternalSpelExpressionParser(SpelParserConfiguration configuration) {
    this.configuration = configuration;
  }
  @Override
  protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
    try {
      this.expressionString = expressionString;
      // Tokenizer就是分词器。把待解析的表达式交给它分词~~~
      Tokenizer tokenizer = new Tokenizer(expressionString);
      // process处理,得到tokenStream  并且记录上它的总长度  并且标记当前处理点为0
      this.tokenStream = tokenizer.process();
      this.tokenStreamLength = this.tokenStream.size();
      this.tokenStreamPointer = 0;
      this.constructedNodes.clear(); // 显然把当前节点清空~~
      SpelNodeImpl ast = eatExpression();
      Assert.state(ast != null, "No node");
      Token t = peekToken();
      if (t != null) {
        throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
      }
      Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
      // 最终:每一个SpelNodeImpl  它就是一个SpelExpression表达式,但会出去。\
      // 此时:只是把我们的字符串解析成为一个SpelExpression,还没有参与赋值、计算哦~~~~
      return new SpelExpression(expressionString, ast, this.configuration);
    } catch (InternalParseException ex) {
      throw ex.getCause();
    }
  }
  ... // 解析表达式的逻辑非常的复杂,Spring团队老牛逼了,竟然支持到了这么多的功能~~~~
}


这么一来,我们的ExpressionParser就算解释完成了。绝大部分情况下我们最终都是使用了SpelExpressionParser去解析标准的语言表达式。


但是,但是,但是我们上面也说了,它还支持Template模式,下面以一个Demo加深了解:


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", "fsx");
        Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext());
        System.out.println(expression.getValue(context, String.class)); //Hello, fsx ---> C:\Users\fangshixiang
    }


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


  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啥的,就不会解析外层#{}了




相关文章
|
1月前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
31 0
|
3天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
33 14
|
4月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
4月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
24天前
|
Java 开发者 Spring
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
|
5月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
129 0
|
5月前
|
设计模式 监控 Java
解析Spring Cloud中的断路器模式原理
解析Spring Cloud中的断路器模式原理
|
2月前
|
Java Spring 容器
Spring底层原理大致脉络
Spring底层原理大致脉络
|
2月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
144 9
|
2月前
|
设计模式 Java Spring
Spring Boot监听器的底层实现原理
Spring Boot监听器的底层实现原理主要基于观察者模式(也称为发布-订阅模式),这是设计模式中用于实现对象之间一对多依赖的一种常见方式。在Spring Boot中,监听器的实现依赖于Spring框架提供的事件监听机制。
33 1