找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

428

积分

0

好友

62

主题
发表于 昨天 02:45 | 查看: 6| 回复: 0

你是否好奇 @Value 注解里的 #{user.name + '_' + T(System).currentTimeMillis()} 表达式是怎么被 Spring “读懂”的?为什么 SpEL 能直接调用类方法、访问属性?今天,我们通过扒开 Spring SpEL 的核心解析器——InternalSpelExpressionParser 的源码,从字符串到抽象语法树(AST)的完整流程,拆解 SpEL 解析的底层逻辑,彻底搞懂“Spring 是如何解析表达式”的。

一、SpEL 解析的入口:InternalSpelExpressionParser 的 parseExpression 方法

要理解 SpEL 解析,首先得找到入口。在 Spring 中,所有 SpEL 表达式的解析,最终都会落到 InternalSpelExpressionParserparseExpression 方法上。我们来看简化后的核心代码:

// InternalSpelExpressionParser.java 核心方法
@Override
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
    // 1. 预处理:处理 ParserContext(比如 #{...} 包裹的表达式)
    String expression = context.getExpressionString(expressionString);
    // 2. 核心解析逻辑
    return doParseExpression(expression, context);
}

protected SpelExpression doParseExpression(String expressionString, ParserContext context) throws ParseException {
    // 3. 创建 Tokenizer,将字符串拆分为 Token 流
    Tokenizer tokenizer = new Tokenizer(expressionString);
    List<Token> tokens = tokenizer.tokenize();

    // 4. 创建 SpelParser,处理 Token 流生成 AST
    SpelParser parser = new SpelParser(tokens, this.configuration);

    // 5. 解析 Token 流,生成 AST 根节点
    SpelNodeImpl astRoot = parser.parseExpression();

    // 6. 封装为 SpelExpression 返回
    return new SpelExpression(expressionString, astRoot, this.configuration);
}

这段代码勾勒出了 SpEL 解析的主流程骨架:从表达式字符串到 Token 流,再到 AST,最后封装为 SpelExpression 实例。其中有三个关键模块:Tokenizer(拆解 Token)、SpelParser(生成 AST)、SpelNodeImpl(AST 节点)。下面我们逐个拆解。

二、Tokenizer:将表达式字符串拆分为 Token 流

你可以把 Tokenizer 理解为“表达式的分词器”——它把像 #{user.name + 123} 这样的字符串,拆分成一个个有意义的“单词”(Token),比如 USER.NAME+123

1. Token 的类型与规则

Spring 定义了十多种 Token 类型(例如 IDENTIFIER(标识符,如 user)、DOT(点)、PLUS(加号)、NUMBER(数字)等)。Tokenizer 的核心逻辑是逐个字符扫描字符串,根据既定规则匹配 Token

我们来看 Tokenizertokenize 方法的简化代码:

// Tokenizer.java 核心分词逻辑
public List<Token> tokenize() {
    List<Token> tokens = new ArrayList<>();
    while (this.pos < this.expressionLength) {
        char ch = this.expression.charAt(this.pos);

        // 跳过空白字符
        if (Character.isWhitespace(ch)) {
            this.pos++;
            continue;
        }

        // 匹配标识符(如 user、name)
        if (Character.isLetter(ch) || ch == '_') {
            tokens.add(scanIdentifier());
            continue;
        }

        // 匹配数字(如 123、45.6)
        if (Character.isDigit(ch) || ch == '.') {
            tokens.add(scanNumber());
            continue;
        }

        // 匹配运算符(如 +、-、==)
        Token operatorToken = scanOperator();
        if (operatorToken != null) {
            tokens.add(operatorToken);
            continue;
        }

        // 其他字符(如括号、引号)
        tokens.add(scanOther());
    }
    tokens.add(Token.EOF); // 结束符
    return tokens;
}
2. 举个例子:user.name + 123 的分词结果

对于表达式 user.name + 123,Tokenizer 会输出以下 Token 列表:

图片

这些 Token 是后续解析的“原材料”——只有把字符串拆解成标准化的 Token 序列,SpelParser 才能准确理解表达式的语法结构。

三、SpelParser:从 Token 流到抽象语法树(AST)

SpelParser 是 SpEL 解析的核心大脑——它根据 Token 流,按照 SpEL 的语法规则(例如“标识符后面跟 DOT 表示访问属性”、“PLUS 表示加法运算”),递归构建出抽象语法树(AST)。

1. AST 是什么?

AST 是表达式的结构化表示。比如 user.name + 123 对应的 AST 结构大致如下:

      PLUS
     /    \
   DOT   NUMBER
  /   \      \
USER  NAME    123

每个节点(如 PLUS、DOT、IDENTIFIER)都是 SpelNodeImpl 的子类,对应一种特定的语法元素。

2. SpelParser 的 parseExpression 方法

SpelParser 的核心是 parseExpression 方法,它会调用一系列“解析子方法”(例如 parsePrimaryExpression 解析基础表达式、parseUnaryExpression 解析一元表达式),从而递归地构建出 AST。我们来看简化后的核心逻辑:

// SpelParser.java 核心解析方法
public SpelNodeImpl parseExpression() {
    // 解析表达式(递归构建 AST)
    SpelNodeImpl expr = parseLogicalOrExpression();

    // 检查是否有剩余 Token(防止语法错误)
    if (this.currentToken != TokenKind.EOF) {
        throw new ParseException("Unexpected token: " + this.currentToken);
    }
    return expr;
}

// 解析逻辑或表达式(示例:a || b)
private SpelNodeImpl parseLogicalOrExpression() {
    SpelNodeImpl left = parseLogicalAndExpression();
    while (this.currentToken == TokenKind.OR) {
        Token operator = consumeToken();
        SpelNodeImpl right = parseLogicalAndExpression();
        left = new OpOr(left, right, operator.getStartPosition());
    }
    return left;
}

// 解析加法表达式(示例:a + b)
private SpelNodeImpl parseAdditiveExpression() {
    SpelNodeImpl left = parseMultiplicativeExpression();
    while (this.currentToken.isOneOf(TokenKind.PLUS, TokenKind.MINUS)) {
        Token operator = consumeToken();
        SpelNodeImpl right = parseMultiplicativeExpression();

        // 根据运算符创建对应的 AST 节点
        if (operator.getKind() == TokenKind.PLUS) {
            left = new OpPlus(left, right, operator.getStartPosition());
        } else {
            left = new OpMinus(left, right, operator.getStartPosition());
        }
    }
    return left;
}

这段代码的核心逻辑是递归下降解析:从最高优先级的表达式(比如逻辑或)开始,逐步分解为低优先级的表达式(比如加法、乘法),直到解析到最基础的标识符或数字。每个运算符对应一个 AST 节点(如 OpPlus 对应加号),通过递归最终构建出完整的 AST。

3. 举个例子:解析 user.name + 123 的过程
  1. parseExpression 调用 parseLogicalOrExpression,最终会走到 parseAdditiveExpression(加法表达式)。
  2. 解析左侧:user.name 会被 parsePrimaryExpression 解析为 PropertyAccess 节点(DOT 运算符)。
  3. 遇到 PLUS 运算符,解析右侧:123 被解析为 Literal 节点(数字)。
  4. 创建 OpPlus 节点,作为左侧 PropertyAccess 和右侧 Literal 的父节点,形成最终的 AST。

四、AST 的生成与 SpelExpression 的返回

SpelParser 生成 AST 根节点后,InternalSpelExpressionParser 会把它封装为 SpelExpression 实例返回。SpelExpression 是表达式的“执行器”——后续调用其 getValue() 方法时,会遍历 AST 节点,依次执行每个节点的逻辑(比如 OpPlus 节点会计算左右子节点的值并相加)。

五、SpEL 解析的完整流程:UML 时序图

为了更清晰地展示整个流程,我们用 UML 时序图总结 InternalSpelExpressionParser 的工作流程:

alt

结尾

看到这里,你应该已经搞懂了 SpEL 解析的底层逻辑:从 InternalSpelExpressionParserparseExpression 入口,到 Tokenizer 拆解 Token,再到 SpelParser 生成 AST,最后封装为 SpelExpression所有的 SpEL 魔法,本质上都是对 AST 的遍历与执行

其实,Spring 的很多功能(比如 @Value、@ConditionalOnExpression)都是基于这套解析流程实现的。你在使用 SpEL 时遇到过什么“诡异”的问题吗?比如表达式解析报错、结果不符合预期?欢迎在评论区留言探讨。




上一篇:基于AI与DNS日志分析的威胁网站检测方案设计与实战
下一篇:AFL模糊测试实战指南:从安装部署、并行Fuzz到覆盖率分析与高级技巧
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 01:43 , Processed in 0.073175 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表