Spring Framework 7.0.9 深度剖析-13-SpEL 表达式引擎

1 阅读44分钟

Spring Framework 7.0.9 深度剖析-13-SpEL 表达式引擎

13.1 本章导读

Spring Expression Language(SpEL,Spring 表达式语言)是 Spring 框架中一个独立的模块——spring-expression。它不依赖 IoC 容器就能工作:你可以在任何 Java 程序里 new SpelExpressionParser().parseExpression("1 + 2 * 3").getValue() 拿到结果;同时它又是整个 Spring 生态里"动态求值"的基石——@Value("#{...}") 的注入值、XML 配置里 <property value="#{...}">、AOP 切点里引用的 bean、Spring Security 的 @PreAuthorize("#..."),背后都是同一个引擎在跑。

本章要解决的问题,是把这个引擎从字符串到返回值的完整流水线拆开看清楚:

  • 词法层:Tokenizer 如何把 a.b?.c + 'hello' 这样的字符串切成一个个 Token,以及 7.0.9 里 TokenKind 枚举到底有多少种 token、分别对应什么词法形态。
  • 语法层:InternalSpelExpressionParser 如何用手写递归下降把 token 流拼成抽象语法树(AST),运算符优先级链 or → and → relational → sum → product → power → unary → primary 是怎样在代码里一层层"吃"出来的。
  • 求值层:SpelNodeImpl.getValueInternal 如何递归求值,EvaluationContext 背后那一组 SPI(PropertyAccessor、MethodResolver、ConstructorResolver、TypeLocator、TypeConverter、OperatorOverloader)如何协作,ReflectivePropertyAccessor 的缓存和 OptimalPropertyAccessor 又是什么。
  • 编译层:SpelCompiler 如何在解释执行若干次后,用 ASM 现场生成一个 CompiledExpression 子类的字节码,把"解释执行"升级成"直接跑字节码",以及 7.0.9 新增的 isCompilationSupported() 门控。
  • 集成层:@Value("#{...}") 从 AutowiredAnnotationBeanPostProcessor 一路走到 StandardBeanExpressionResolver,再回到 SpelExpressionParser 的完整链路;#{} 与 ${} 的本质区别。
  • 安全层:StandardEvaluationContext 为什么"绝不能跑不可信表达式",SimpleEvaluationContext 又把哪些能力砍掉了。

本章涉及的全部类都在 spring-expression/src/main/java/org/springframework/expression/ 下,集成类 StandardBeanExpressionResolver 位于 spring-context(注意:不是常见资料所说的 spring-beans,7.0.9 实际位置以仓库为准)。前置知识只需要 Java 反射、泛型与 ASM 字节码的基本概念。读完本章,你应当能对着任意一条 SpEL 表达式,说出它经过了哪些类、哪些方法、缓存了什么、为什么快或为什么慢。

13.2 核心概念:SpEL 的四大抽象与一次求值的角色分工

在钻进源码之前,先把 SpEL 的接口骨架立起来。SpEL 的全部能力可以收敛为四个顶层接口,它们分别位于 spring-expression/src/main/java/org/springframework/expression/:

接口模块相对路径一句话职责
ExpressionParserspring-expression/src/main/java/org/springframework/expression/ExpressionParser.java把字符串解析成可重复求值的 Expression
Expressionspring-expression/src/main/java/org/springframework/expression/Expression.java一次解析产物,封装 getValue/setValue/getValueType/isWritable 家族方法
EvaluationContextspring-expression/src/main/java/org/springframework/expression/EvaluationContext.java求值上下文:rootObject、一组 accessor/resolver、typeLocator、变量表
SpelNodespring-expression/src/main/java/org/springframework/expression/spel/SpelNode.javaAST 节点接口,SpelNodeImpl 是其抽象实现

13.2.1 ExpressionParser:两态解析

ExpressionParser 只有两个方法(见 ExpressionParser.java 第 42、57 行):

Expression parseExpression(String expressionString) throws ParseException;
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

第二态多出一个 ParserContext 参数,它的关键是 isTemplate():当返回 true 时,解析器不再把整个字符串当作一条表达式,而是当作"模板"——一段普通文本里嵌着若干 #{...} 片段。这个分流发生在 TemplateAwareExpressionParser.parseExpression(spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java 第 56-65 行):

@Override
public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
	if (context != null && context.isTemplate()) {
		Assert.notNull(expressionString, "'expressionString' must not be null");
		return parseTemplate(expressionString, context);
	}
	else {
		Assert.hasText(expressionString, "'expressionString' must not be null or blank");
		return doParseExpression(expressionString, context);
	}
}

SpelExpressionParser(spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java)继承 TemplateAwareExpressionParser,它本身极薄——构造时持有一个 SpelParserConfiguration,doParseExpression 只是 new 出一个 InternalSpelExpressionParser 并转发:

@Override
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
	return new InternalSpelExpressionParser(this.configuration).doParseExpression(expressionString, context);
}

这里有个重要的线程模型事实:SpelExpressionParser 的类注释明确写着"Instances are reusable and thread-safe",而 InternalSpelExpressionParser 的注释是"Instances are reusable but are not thread-safe"。所以解析器每次解析都 new 一个内部 parser 出来用完即弃,线程安全的外壳包住非线程安全的内核。

13.2.2 Expression:getValue 家族与缓存契约

Expression 接口(Expression.java)方法极多,但可以归为三组:

  1. 求值:getValue()、getValue(Class<T>)、getValue(Object rootObject)、getValue(EvaluationContext) 等八个重载,区别只在于"用哪个上下文、用哪个 rootObject、要不要做期望类型转换"。
  2. 类型与可写性:getValueType()、getValueTypeDescriptor()、isWritable()——回答"这条表达式最终能写成什么类型""能不能被 setValue"。
  3. 写回:setValue(...) 三个重载。

这个接口的类级 Javadoc(Expression.java 第 30-45 行)藏着一条关键的生命周期契约,7.0.9 依然严格遵守:一个解析出来的 Expression 内部会缓存它在某次求值时命中的 PropertyAccessor/MethodExecutor/ConstructorExecutor;因此你可以把同一个 Expression 在"同类型、同配置"的多个 EvaluationContext 间复用,但绝不能先在 StandardEvaluationContext 上求值、再拿到 SimpleEvaluationContext 上求值——否则宽松上下文里缓存的 accessor 会被带到受限上下文,安全边界就被绕过了。要在两种安全语义下用同一条表达式字符串,必须 parseExpression 两次、得到两个 Expression 实例。

13.2.3 EvaluationContext:求值时的"策略总线"

EvaluationContext(EvaluationContext.java)是一组策略的集合:

TypedValue getRootObject();
default List<PropertyAccessor> getPropertyAccessors() { return Collections.emptyList(); }
default List<IndexAccessor> getIndexAccessors() { return Collections.emptyList(); }   // 6.2 新增
default List<ConstructorResolver> getConstructorResolvers() { return Collections.emptyList(); }
default List<MethodResolver> getMethodResolvers() { return Collections.emptyList(); }
@Nullable BeanResolver getBeanResolver();
TypeLocator getTypeLocator();
TypeConverter getTypeConverter();
TypeComparator getTypeComparator();
OperatorOverloader getOperatorOverloader();
void setVariable(String name, @Nullable Object value);
@Nullable Object lookupVariable(String name);
default boolean isAssignmentEnabled() { return true; }        // 5.3.38 新增
default boolean isCompilationSupported() { return true; }     // 7.0.9 新增

注意后两个 default 方法的版本标注:isAssignmentEnabled 自 5.3.38 起可以关闭 =、++、--;isCompilationSupported 自 7.0.9 起新增——它让"上下文"这一侧也能一票否决编译,与 SpelParserConfiguration 里的编译器模式正交(注释原文:"Both must permit compilation for a given expression to be compiled")。这是 7.0.9 相对旧版资料的一个新差异,后文讲编译时会回到它。

EvaluationContext 有两个实现:功能全开的 StandardEvaluationContext(spel/support/)与受限子集的 SimpleEvaluationContext(同包)。它们的差异是安全模型的核心,13.7 节专门展开。

13.2.4 SpelNode:AST 节点

SpelNode 是语法树节点接口,SpelNodeImpl(spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java)是其抽象基类。它实现了 org.springframework.asm.Opcodes——这意味着每个 AST 节点除了能"解释求值"(getValueInternal),还能"生成字节码"(generateCode),这正是编译模式的入口。spel/ast/ 包下有三十多个具体节点:OpPlus、PropertyOrFieldReference、MethodReference、Indexer、TypeReference、BeanReference、Ternary、Elvis、InlineList、InlineMap……每一个语法构造对应一个节点类。

13.2.5 一次求值的角色分工

把四个抽象串起来,一次 expression.getValue() 的角色是:ExpressionParser 造 Expression;Expression 内部持着一棵 SpelNodeImpl AST(字段 ast);求值时 Expression 把 EvaluationContext 包进一个 ExpressionState(每次求值 new 一个、持有本次求值的临时状态);然后递归调 ast.getValueInternal(state);每个节点再向 state 背后的 EvaluationContext 要 accessor/resolver。下一节就从最底层的词法分析开始,顺着这条流水线走一遍。

13.3 源码剖析(一):词法分析——Tokenizer 如何把字符串切成 Token

词法分析的全部逻辑在 spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java。它是一个手写的、基于字符指针的扫描器,没有用任何 lex 工具。

13.3.1 哨兵字符与字符分类表

Tokenizer 构造函数做了一个关键设计(Tokenizer.java 第 80-85 行):

public Tokenizer(String inputData) {
	this.expressionString = inputData;
	this.charsToProcess = (inputData + "\0").toCharArray();
	this.max = this.charsToProcess.length;
	this.pos = 0;
}

它在输入末尾追加了一个 \0 哨兵。这样扫描循环里所有"读到头了吗"的判断都不需要边界检查——直接 this.charsToProcess[this.pos] 取字符,撞到 \0 就走 case 0 分支结束。这是经典词法扫描器省掉边界判断的手法。

字符分类用一张 256 字节的位表 FLAGS(第 50-66 行):

private static final byte[] FLAGS = new byte[256];
private static final byte IS_DIGIT = 0x01;
private static final byte IS_HEXDIGIT = 0x02;
static {
	for (int ch = '0'; ch <= '9'; ch++) { FLAGS[ch] |= IS_DIGIT | IS_HEXDIGIT; }
	for (int ch = 'A'; ch <= 'F'; ch++) { FLAGS[ch] |= IS_HEXDIGIT; }
	for (int ch = 'a'; ch <= 'f'; ch++) { FLAGS[ch] |= IS_HEXDIGIT; }
}

isDigit/isHexadecimalDigit 都先判 ch > 255 再查 FLAGS[ch]——非 ASCII 字符(比如中文)直接不被当作数字,这也是为什么 SpEL 标识符里的字母用的是 Character.isLetter(ch)(支持 Unicode 字母),而数字严格限定 ASCII。

13.3.2 process():一个超大 switch 的分发

process()(第 88-274 行)是主循环:遇到字母走 lexIdentifier(),遇到 _/$ 也走标识符;其余字符进 switch(ch)。这里逐案看几个有讲究的分支:

双字符 token 的抢先判断。以 ? 为例(第 198-211 行):

case '?':
	if (isTwoCharToken(TokenKind.SELECT)) {        // ?[  选择
		pushPairToken(TokenKind.SELECT);
	}
	else if (isTwoCharToken(TokenKind.ELVIS)) {    // ?:  elvis
		pushPairToken(TokenKind.ELVIS);
	}
	else if (isTwoCharToken(TokenKind.SAFE_NAVI)) { // ?.  安全导航
		pushPairToken(TokenKind.SAFE_NAVI);
	}
	else {
		pushCharToken(TokenKind.QMARK);             // ?   三元问号
	}
	break;

同一个 ? 开头,后续字符决定了它是四种完全不同的 token:选择 ?[、elvis ?:、安全导航 ?.、三元 ?。isTwoCharToken(第 511-515 行)就是简单地比对当前与下一字符是否等于该 TokenKind 预存的 tokenChars:

private boolean isTwoCharToken(TokenKind kind) {
	return (kind.tokenChars.length == 2 &&
			this.charsToProcess[this.pos] == kind.tokenChars[0] &&
			this.charsToProcess[this.pos + 1] == kind.tokenChars[1]);
}

同理:+ 可能是 PLUS 或 INC(++);- 是 MINUS 或 DEC;! 是 NOT、NE(!=)或 PROJECT(![);^ 是 POWER 或 SELECT_FIRST(^[);$ 是 SELECT_LAST($[)或标识符开头;& 是 FACTORY_BEAN_REF 或 SYMBOLIC_AND(&&);= 是 ASSIGN 或 EQ(==);>/< 分别对应 GT/GE、LT/LE。| 比较特殊:如果后面不是 |,直接抛 MISSING_CHARACTER——因为单 | 在 SpEL 里没有意义,不像 ! 还能当 NOT。

空白与注释。SpEL 没有 // 注释语法,空白(空格、\t、\r、\n)只是 this.pos++ 漂移过去(第 248-254 行)。反斜杠 \ 直接抛 UNEXPECTED_ESCAPE_CHAR——SpEL 字符串里不认可 \ 转义,转义靠双写(见下文)。

13.3.3 TokenKind 完整清单(从仓库枚举逐字统计)

TokenKind(spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java)按"操作数在前"的顺序排列,共 57 个枚举常量。不带字符串的是纯词法类(hasPayload=true,即 token 还携带原始文本数据),带字符串的是符号 token:

  • 字面量(7):LITERAL_INT、LITERAL_LONG、LITERAL_HEXINT、LITERAL_HEXLONG、LITERAL_STRING、LITERAL_REAL、LITERAL_REAL_FLOAT。
  • 括号与分隔符(10):LPAREN("(")、RPAREN(")")、COMMA(",")、IDENTIFIER、COLON(":")、HASH("#")、RSQUARE("]")、LSQUARE("[")、LCURLY("{")、RCURLY("}")。
  • 点与算术(5):DOT(".")、PLUS("+")、STAR("*")、MINUS("-")、DIV("/")、MOD("%")。
  • 集合操作(4):SELECT_FIRST("^[")、SELECT_LAST("$[")、QMARK("?")、PROJECT("![")、SELECT("?[")。
  • 关系与逻辑(11):GE(">=")、GT(">")、LE("<=")、LT("<")、EQ("==")、NE("!=")、NOT("!")、ASSIGN("=")、SYMBOLIC_OR("||")、SYMBOLIC_AND("&&")。
  • 关键字式操作符(3):INSTANCEOF("instanceof")、MATCHES("matches")、BETWEEN("between")。
  • 自增自减与幂(3):POWER("^")、INC("++")、DEC("--")。
  • 特殊语法糖(4):ELVIS("?:")、SAFE_NAVI("?.")。
  • bean 引用(2):BEAN_REF("@")、FACTORY_BEAN_REF("&")。

TokenKind 有两个关键设计:每个符号 token 预存了 tokenChars(toCharArray()),供 isTwoCharToken 比对;hasPayload 的计算反直觉——this.tokenChars.length == 0 才算"有载荷"(第 133 行):字面量与标识符没有固定符号,token 对象里额外存了原始文本(Token.data),而符号 token 的文本就是符号本身、不必另存。

13.3.4 字符串字面量:双写转义

lexQuotedStringLiteral()(第 278-299 行)处理单引号串:

private void lexQuotedStringLiteral() {
	int start = this.pos;
	boolean terminated = false;
	while (!terminated) {
		this.pos++;
		char ch = this.charsToProcess[this.pos];
		if (ch == '\'') {
			// may not be the end if the char after is also a '
			if (this.charsToProcess[this.pos + 1] == '\'') {
				this.pos++;  // skip over that too, and continue
			}
			else {
				terminated = true;
			}
		}
		if (isExhausted()) {
			raiseParseException(start, SpelMessage.NON_TERMINATING_QUOTED_STRING);
		}
	}
	this.pos++;
	this.tokens.add(new Token(TokenKind.LITERAL_STRING, subarray(start, this.pos), start, this.pos));
}

SpEL 字符串里要表达一个单引号,写法是两个单引号 ''(SQL 风格),而不是反斜杠转义。扫描时遇到 ',先看后一个字符是不是也是 '——是就双写吞掉继续,否则才认定串结束。双引号串 lexDoubleQuotedStringLiteral 逻辑完全一致。这解释了一个常见坑:SpEL 里写 'It''s mine' 而不是 'It\'s mine'。

13.3.5 数字字面量:十六进制、浮点、后缀与 3.toString() 陷阱

lexNumericLiteral(boolean firstCharIsZero)(第 341-446 行)是最长的词法子程序。分支顺序:

  1. 十六进制:firstCharIsZero && 下一字符是 x/X,则连续吞十六进制数字,按后面有没有 L/l 决定产出 LITERAL_HEXINT 还是 LITERAL_HEXLONG。
  2. 十进制整数部分:do { pos++ } while (isDigit(...))。
  3. 小数点:若下一字符是 .,标记 isReal=true,继续吞数字。但这里有个精妙处理(第 382-389 行):
    if (this.pos == dotpos + 1) {
        // the number is something like '3.'. It is really an int but may be
        // part of something like '3.toString()'. In this case process it as
        // an int and leave the dot as a separate token.
        this.pos = dotpos;
        pushIntToken(subarray(start, this.pos), false, start, this.pos);
        return;
    }
    
    也就是说 3.toString() 里的 . 不会被当作小数点吞掉——3 后面紧跟 . 但 . 后没有数字时,扫描器回退,把 3 作为 LITERAL_INT、把 . 留给主循环作 DOT token,这样 3.toString() 才能被解析成"整数字面量访问 toString 属性"。这是 SpEL 支持"基本类型字面量上直接调方法"(1.toString()、3.5.intValue())的词法基础。
  4. 后缀:L/l → long(但 3.4L 非法,抛 REAL_CANNOT_BE_LONG);e/E 指数 → real;f/F → LITERAL_REAL_FLOAT;d/D → LITERAL_REAL。

13.3.6 标识符与关键字操作符的文本别名

lexIdentifier()(第 448-467 行)吞掉所有 字母/数字/_/$ 字符,然后做一件重要的事:

private static final String[] ALTERNATIVE_OPERATOR_NAMES =
		{"DIV", "EQ", "GE", "GT", "LE", "LT", "MOD", "NE", "NOT"};
...
if (subarray.length == 2 || subarray.length == 3) {
	String asString = new String(subarray).toUpperCase(Locale.ROOT);
	int idx = Arrays.binarySearch(ALTERNATIVE_OPERATOR_NAMES, asString);
	if (idx >= 0) {
		pushOneCharOrTwoCharToken(TokenKind.valueOf(asString), start, subarray);
		return;
	}
}
this.tokens.add(new Token(TokenKind.IDENTIFIER, subarray, start, this.pos));

SpEL 允许用文本别名写操作符:a EQ b 等价于 a == b、a NE b 等价于 a != b、1 DIV 2 等价于 1 / 2、a NOT b……注意 ALTERNATIVE_OPERATOR_NAMES 是排好序的数组,配合 Arrays.binarySearch;而且只匹配 2~3 个字符的标识符(AND/OR 也是文本操作符,但故意不在这个数组里——注释说明它们留给 InternalSpelExpressionParser 在语法层处理,因为 and/or 要做短路求值,词法层只产出 IDENTIFIER)。

至此,process() 返回一个 List<Token>。Token 类(同包 Token.java)极简单:它持有 TokenKind kind、原始 char[] data(符号 token 为 null)、startPos/endPos。词法层的产物就是这样一串带位置的 token,接下来交给递归下降语法器。

13.4 源码剖析(二):语法分析——递归下降与运算符优先级链

语法分析器是 InternalSpelExpressionParser(spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java),一个约 1060 行的手写递归下降 parser。它的状态很简单:tokenStream、tokenStreamPointer,以及一个用于暂存构造节点的 Deque<SpelNodeImpl> constructedNodes(第 100 行)。

13.4.1 doParseExpression:词法入口与"不能有多余输入"

doParseExpression(第 130-155 行)是总入口:

@Override
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
		throws ParseException {
	checkExpressionLength(expressionString);
	try {
		this.expressionString = expressionString;
		Tokenizer tokenizer = new Tokenizer(expressionString);
		this.tokenStream = tokenizer.process();
		this.tokenStreamLength = this.tokenStream.size();
		this.tokenStreamPointer = 0;
		this.constructedNodes.clear();
		SpelNodeImpl ast = eatExpression();
		if (ast == null) {
			throw new SpelParseException(this.expressionString, 0, SpelMessage.OOD);
		}
		Token t = peekToken();
		if (t != null) {
			throw new SpelParseException(this.expressionString, t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
		}
		return new SpelExpression(expressionString, ast, this.configuration);
	}
	catch (InternalParseException ex) {
		throw ex.getCause();
	}
}

逐段看:先 checkExpressionLength 按 configuration.getMaximumExpressionLength() 限长(默认 DEFAULT_MAX_EXPRESSION_LENGTH,可由 spring.context.expression.maxLength 调整);然后跑词法器、置指针;调 eatExpression() 吃掉整条表达式;关键是吃完之后还要求 peekToken()==null——如果还剩 token,就抛 MORE_INPUT。这就是为什么 1 2、a b 这种"两个表达式并排"会报解析错误:递归下降默认只吃一个完整表达式,多出来的输入不被容忍。InternalParseException 是个异常壳,词法/语法层内部抛它,出口处 throw ex.getCause() 拆包成真正的 SpelParseException。

13.4.2 优先级链:八个 eat 方法层层嵌套

SpEL 的运算符优先级,完全由这一组 eatXxx 方法的嵌套调用深度决定——越靠外层优先级越低、越晚结合。链是(方法后注释是 ANTLR 风格文法):

eatExpression            // expression : logicalOrExpression ((ASSIGN|ELVIS|QMARK) ...)?
  └─ eatLogicalOrExpression     // : logicalAndExpression (OR logicalAndExpression)*
       └─ eatLogicalAndExpression   // : relationalExpression (AND relationalExpression)*
            └─ eatRelationalExpression // : sumExpression (relOp sumExpression)?
                 └─ eatSumExpression      // : productExpression ((PLUS|MINUS|INC) productExpression)*
                      └─ eatProductExpression  // : powerExpr ((STAR|DIV|MOD) powerExpr)*
                           └─ eatPowerIncDecExpression // : unaryExpression (POWER unaryExpression)? (INC|DEC)?
                                └─ eatUnaryExpression  // : (NOT|PLUS|MINUS|INC|DEC) unaryExpression | primary
                                     └─ eatPrimaryExpression  // : startNode (node)*

以加法为例(eatSumExpression,第 278-292 行):

private @Nullable SpelNodeImpl eatSumExpression() {
	SpelNodeImpl expr = eatProductExpression();
	while (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.INC)) {
		Token t = takeToken();  //consume PLUS or MINUS or INC
		SpelNodeImpl rhExpr = eatProductExpression();
		checkRightOperand(t, rhExpr);
		if (t.kind == TokenKind.PLUS) {
			expr = new OpPlus(t.startPos, t.endPos, expr, rhExpr);
		}
		else if (t.kind == TokenKind.MINUS) {
			expr = new OpMinus(t.startPos, t.endPos, expr, rhExpr);
		}
	}
	return expr;
}

这是经典的"左结合循环":先吃一个 product(乘除),然后只要后面还跟着 +/-/INC,就吃一个运算符、再吃一个 product,用 new OpPlus(...) 把左右包起来。因为循环里 expr = new OpPlus(...) 总是把已构造的 expr 放左孩子,所以 1-2-3 被构造成 OpPlus(OpPlus(1,2),3)——左结合。而 eatProductExpression 内部又调 eatPowerIncDecExpression,意味着 1+2*3 里 2*3 先被 product 层吃掉成为一个节点,再被 sum 层包成 OpPlus(1, OpMultiply(2,3))——乘除优先于加减,完全由"product 在 sum 内层"这一调用结构保证。

逻辑或/与的短路在 eatLogicalOrExpression/eatLogicalAndExpression(第 209-230 行)体现:它们既认符号 ||/&&,也认标识符 or/and(peekIdentifierToken("or")),循环左结合地拼 OpOr/OpAnd。

幂运算在 eatPowerIncDecExpression(第 316-332 行):注意它吃的是 eatUnaryExpression 而不是 eatPowerIncDecExpression 自身——也就是说 2^3^2 不会无限左结合,幂的右操作数是一元表达式;同时后缀 ++/-- 在这里处理(new OpInc(startPos, endPos, true/*post*/, expr))。

一元正负/非在 eatUnaryExpression(第 336-364 行):先看是不是 NOT/PLUS/MINUS 开头,是就吞掉再递归 eatUnaryExpression(支持 --a、!!!flag 这种连写);否则落到 eatPrimaryExpression。这里区分了前缀 ++a(OpInc(..., false, expr),第三参 false 表示 prefix)与后缀 a++。

13.4.3 关系操作符:符号与文本三兄弟

eatRelationalExpression(第 233-274 行)吃一个 sum,再用 maybeEatRelationalOperator()(第 909-930 行)判断后面是不是关系操作符:

private @Nullable Token maybeEatRelationalOperator() {
	Token t = peekToken();
	if (t == null) { return null; }
	if (t.isNumericRelationalOperator()) { return t; }
	if (t.isIdentifier()) {
		String idString = t.stringValue();
		if (idString.equalsIgnoreCase("instanceof")) { return t.asInstanceOfToken(); }
		if (idString.equalsIgnoreCase("matches")) { return t.asMatchesToken(); }
		if (idString.equalsIgnoreCase("between")) { return t.asBetweenToken(); }
	}
	return null;
}

数值关系操作符 GT/LT/LE/GE/EQ/NE 直接用 token kind;而 instanceof/matches/between 词法层是普通标识符,语法层在这里把它们"升级"成关键字 token(asXxxToken())。随后 eatRelationalExpression 按 kind 分派成 OpGT/OpLT/OperatorInstanceof/OperatorMatches/OperatorBetween 等节点。注意 matches 的构造带了一个共享正则缓存:new OperatorMatches(this.patternCache, ...)——patternCache 是 parser 字段里的 ConcurrentLruCache<String, Pattern>(第 103-104 行),避免每条表达式重复 Pattern.compile。

13.4.4 eatExpression 的顶层糖:赋值、Elvis、三元

最外层 eatExpression(第 171-206 行)在 logicalOr 之上再叠三种"顶层"语法:

SpelNodeImpl expr = eatLogicalOrExpression();
Token t = peekToken();
if (t != null) {
	if (t.kind == TokenKind.ASSIGN) {  // a=b
		... nextToken(); SpelNodeImpl assignedValue = eatLogicalOrExpression();
		return new Assign(t.startPos, t.endPos, expr, assignedValue);
	}
	if (t.kind == TokenKind.ELVIS) {   // a?:b
		... nextToken(); SpelNodeImpl valueIfNull = eatExpression();
		return new Elvis(t.startPos, t.endPos, expr, valueIfNull);
	}
	if (t.kind == TokenKind.QMARK) {   // a?b:c
		... nextToken(); SpelNodeImpl ifTrueExprValue = eatExpression();
		eatToken(TokenKind.COLON); SpelNodeImpl ifFalseExprValue = eatExpression();
		return new Ternary(t.startPos, t.endPos, expr, ifTrueExprValue, ifFalseExprValue);
	}
}
return expr;

这就是 = 赋值、?: elvis、? : 三元的解析点。注意 elvis 的右值递归调 eatExpression(可以再是 elvis,支持 a?:b?:c),三元则严格要求 QMARK ... COLON ... 三段。这些"顶层"语法放在 logicalOr 之外,正是因为它们的优先级最低——a==b? x:y 里 a==b 先被 relational 层算完,再作三元条件。

13.4.5 primary 与复合表达式:eatStartNode / eatNode

eatPrimaryExpression(第 367-384 行)吃一个"起始节点",然后循环吃 node(./?. 引导的属性/方法/索引,或裸 [...] 索引)。若吃了多于一段,就包成 CompoundExpression:

private @Nullable SpelNodeImpl eatPrimaryExpression() {
	SpelNodeImpl start = eatStartNode();  // always a start node
	List<SpelNodeImpl> nodes = null;
	SpelNodeImpl node = eatNode();
	while (node != null) {
		if (nodes == null) { nodes = new ArrayList<>(4); nodes.add(start); }
		nodes.add(node);
		node = eatNode();
	}
	if (start == null || nodes == null) { return start; }
	return new CompoundExpression(start.getStartPosition(), nodes.get(nodes.size() - 1).getEndPosition(),
			nodes.toArray(new SpelNodeImpl[0]));
}

eatStartNode(第 517-540 行)是个大 if-else 串:字面量 → 括号表达式 → 类型引用 T(...) → null → new ... 构造器 → 属性/方法 → #var/#fn() → @bean → 投影/选择/索引 → 内联 list/map {...}。eatDottedNode(第 410-424 行)在 ./?. 后依次尝试 maybeEatMethodOrProperty、maybeEatFunctionOrVar、maybeEatProjection、maybeEatSelection、maybeEatIndexer,谁先匹配上就 pop() 出节点。

几个有味道的细节:

  • #var 与 #fn() 的区分在 maybeEatFunctionOrVar(第 432-448 行):吞 # 和标识符后,若后面跟 ( 就是 FunctionReference(带参数),否则是 VariableReference。
  • T(...) 的歧义在 maybeEatTypeReference(第 575-602 行):先 peek 标识符是不是 T,但如果 T 后面紧跟 ],说明它其实是 map 的 key(map[T]),于是退化成普通 PropertyOrFieldReference;同理 new 后紧跟 ] 也是 map key。这是 SpEL 同时支持 T(java.lang.Integer) 和 ['T'] 两种用法时的消歧。
  • 内联 list/map在 maybeEatInlineListOrMap(第 634-691 行):{} 空 list、{:} 空 map、{a,b,c} list、{k:v,...} map,靠第一个逗号/冒号判断容器类型。
  • bean 引用在 maybeEatBeanReference(第 544-573 行):@beanName 或 @'bean.name'(带引号可含点号),&beanName 是 FactoryBean 引用。

语法层到此结束,产物是一棵 SpelNodeImpl 树。下一节进入这些节点的求值逻辑。

13.5 源码剖析(三):AST 节点的求值逻辑

13.5.1 SpelNodeImpl:所有节点的抽象基类

SpelNodeImpl(spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java)实现了 SpelNode 与 org.springframework.asm.Opcodes。它的字段很少:

private final int startPos;
private final int endPos;
protected SpelNodeImpl[] children = SpelNodeImpl.NO_CHILDREN;
private @Nullable SpelNodeImpl parent;
/** 编译期使用的退出类型描述符,形如 Ljava/lang/String、I、[I */
protected volatile @Nullable String exitTypeDescriptor;

构造函数(第 75-85 行)把子节点挂上去并反向设置 parent——这就是 nextChildIs(Class...)(第 91-110 行)能工作的原因:它通过 parent.children 找到自己后面的兄弟,判断下一个节点类型。PropertyOrFieldReference 的 autoGrow 逻辑就用它判断"后面跟着的是不是 Indexer/PropertyOrFieldReference"。

对外的求值入口是两个 final 方法(第 113-120 行):

@Override
public final @Nullable Object getValue(ExpressionState expressionState) throws EvaluationException {
	return getValueInternal(expressionState).getValue();
}
@Override
public final TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException {
	return getValueInternal(expressionState);
}

真正的求值是抽象方法 public abstract TypedValue getValueInternal(ExpressionState expressionState)(第 205 行)。getValue 只是丢掉 TypedValue 的类型描述符壳、取出裸值。这种"裸值/带类型值"双通道贯穿整个引擎:普通求值拿 Object,需要精确类型(如方法重载匹配、结果类型转换)时拿 TypedValue。

exitTypeDescriptor 是连接解释与编译的关键:第一次解释求值后,节点把自己的退出类型(字符串?int?)记下来;编译时 isCompilable() 要求这个描述符非空——所以编译一定发生在至少一次解释求值之后,因为只有跑过才知道动态类型。SpelExpression.checkCompile 的计数阈值正是为此服务。

13.5.2 ExpressionState:一次求值的临时状态

求值时 SpelExpression 会 new 一个 ExpressionState(spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java)包住 EvaluationContext。它的类注释点明了与 context 的分工:context 跨多次求值共享,state 只活在本次求值里。它持有两个栈:

  • Deque<TypedValue> contextObjects:当前"活动上下文对象"(getActiveContextObject)。复合表达式每进一层(如 a.b.c 里的 b),就把 a 压栈,使无限定引用默认对活动对象解析。
  • Deque<TypedValue> scopeRootObjects:进入新作用域(如投影/选择 list.?[#this>1] 里的 #this)时压入的"作用域根"。注释里的例子就是 #list1.?[#list2.contains(#this)]——进入选择后 #this 变成 list1 的元素。

它还做两件横切的事:

public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) {
	OperatorOverloader overloader = this.relatedContext.getOperatorOverloader();
	if (overloader.overridesOperation(op, left, right)) {
		trackOperation();
		Object returnValue = overloader.operate(op, left, right);
		return new TypedValue(returnValue);
	}
	else {
		... throw new SpelEvaluationException(SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES, op, leftType, rightType);
	}
}

public void trackOperation() {
	int maxOperations = this.configuration.getMaximumOperations();
	if (++this.operationsCount >= maxOperations) {
		throw new SpelEvaluationException(SpelMessage.MAX_OPERATIONS_EXCEEDED, maxOperations);
	}
}

operate 是所有"类型不支持的运算符"的兜底——先问 context 注册的 OperatorOverloader 能不能接管,不能就抛异常。trackOperation 是 6.2.19 引入的防 DoS 计数器:一次求值里"操作次数"超过 maximumOperations 就抛错,防止恶意/病态表达式死循环。

13.5.3 OpPlus:既要做加法又要拼字符串

OpPlus(spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java)的 getValueInternal(第 71-156 行)是运算符节点里最有代表性的一段。它先处理一元 +x(单操作数),再处理二元 a+b:

if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
	state.trackOperation();
	if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
		BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
		BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
		return new TypedValue(leftBigDecimal.add(rightBigDecimal));
	}
	else if (leftNumber instanceof Double || rightNumber instanceof Double) {
		this.exitTypeDescriptor = "D";
		return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue());
	}
	else if (leftNumber instanceof Float || rightNumber instanceof Float) { ... "F" ... }
	else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { ... }
	else if (leftNumber instanceof Long || rightNumber instanceof Long) { ... "J" ... }
	else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) {
		this.exitTypeDescriptor = "I";
		return new TypedValue(leftNumber.intValue() + rightNumber.intValue());
	}
	else {
		return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue());
	}
}

数值提升顺序严格按 BigDecimal → Double → Float → BigInteger → Long → Integer——只要任一操作数是某一档,整个结果就提升到那档,与 Java 二元数值提升规则一致。每次命中都设置 exitTypeDescriptor(D/F/J/I),为编译做准备。

数值都不匹配时,退到字符串拼接分支(第 134-153 行):

if (leftOperand instanceof String leftString && rightOperand instanceof String rightString) {
	this.exitTypeDescriptor = "Ljava/lang/String";
	checkStringLength(leftString); checkStringLength(rightString);
	return concatenate(state, leftString, rightString);
}
if (leftOperand instanceof String leftString) {
	checkStringLength(leftString);
	String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
	...
}

两个细节:其一,a + null 里 null 一侧被转成字符串 "null"(与 Java 字符串拼接一致);其二,checkStringLength 有一个上限 MAX_CONCATENATED_STRING_LENGTH = 100_000(第 61 行,5.2.24 引入)——拼接结果超过 10 万字符直接抛 MAX_CONCATENATED_STRING_LENGTH_EXCEEDED,这也是防 DoS。两侧都不是数值也不是字符串时,最后 return state.operate(Operation.ADD, leftOperand, rightOperand),交给 OperatorOverloader。

它的 generateCode(第 242-275 行)展示了编译时字符串拼接怎么生成字节码:exitTypeDescriptor 是 Ljava/lang/String 时 new 一个 StringBuilder,递归 walk 左右操作数往 append 里塞,最后调 toString;否则按 IADD/LADD/FADD/DADD 生成数值加法。这解释了为什么编译模式下字符串拼接能快很多——直接生成了等效的 Java 字节码。

13.5.4 PropertyOrFieldReference:属性/字段解析与缓存

这是求值里最常走的节点(a.b、user.name 都是它)。PropertyOrFieldReference(spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java)持有两个缓存字段(第 75-77 行):

private volatile @Nullable PropertyAccessor cachedReadAccessor;
private volatile @Nullable PropertyAccessor cachedWriteAccessor;

核心是 readProperty(第 195-283 行)。先处理 null-safe(?.):

if (isNullSafe()) {
	if (target == null) {
		return TypedValue.NULL;
	}
	if (target instanceof Optional<?> optional) {
		if (optional.isPresent()) {
			target = optional.get();
			fallbackOptionalTarget = optional;
		}
		else {
			isEmptyOptional = true;
		}
	}
}

注意 7.0 的新行为:?. 不仅对 null 短路,还对 Optional 目标解包——user?.name 当 user 是 Optional<User> 且 present 时,自动取 optional.get() 再访问 name;empty Optional 则返回 null。这是 7.0 新增的 Optional 友好行为。

然后走缓存快路径(第 218-231 行):

PropertyAccessor accessorToUse = this.cachedReadAccessor;
if (accessorToUse != null) {
	if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
		try {
			state.trackOperation();
			return accessorToUse.read(evalContext, target, name);
		}
		catch (Exception ex) {
			// This is OK - it may have gone stale due to a class change,
			// let's try to get a new one and call it before giving up...
		}
	}
	this.cachedReadAccessor = null;
}

缓存的 accessor 只有在"当前 context 的 accessor 列表里还包含它"时才复用——这就是 Expression 接口 Javadoc 里"跨 context 复用要确认配置一致"的落点。缓存失效(上下文换了)或读抛异常,就清空缓存走慢路径。

慢路径(第 233-260 行)遍历候选 accessor:

List<PropertyAccessor> accessorsToTry =
		AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
try {
	for (PropertyAccessor accessor : accessorsToTry) {
		if (accessor.canRead(evalContext, target, name)) {
			if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
				accessor = reflectivePropertyAccessor.createOptimalAccessor(
						evalContext, target, name);
			}
			this.cachedReadAccessor = accessor;
			state.trackOperation();
			return accessor.read(evalContext, target, name);
		}
		...
	}
}

第一次命中后,若命中的是通用 ReflectivePropertyAccessor,就调用 createOptimalAccessor 把它换成一个"专为这个类这个属性"优化过的 OptimalPropertyAccessor(见 13.6.2),并缓存。下一次求值直接走缓存快路径,不再遍历 accessor 列表。

读不到时的错误信息也分情形(第 272-282 行):empty Optional 返回 null;原 target 是 null 抛 PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL;否则抛 PROPERTY_OR_FIELD_NOT_READABLE 并带上实际目标类名。

还有一个 autoGrow 分支(getValueInternal 第 126-167 行):当 configuration.isAutoGrowNullReferences() 开启、且读到 null、且后面还跟着索引/属性时,自动 new 一个空 List/Map 或用无参构造器 new 一个对象回填属性——这是为了支持"链式路径上自动补空对象"的宽松数据绑定。

13.6 源码剖析(四):求值 SPI——反射 accessor、方法解析器与类型系统

PropertyOrFieldReference 把"怎么读属性"委托给了 EvaluationContext 里的 PropertyAccessor 列表。StandardEvaluationContext 默认只注册一个:ReflectivePropertyAccessor。它是整个 SpEL 反射求值的心脏。

13.6.1 ReflectivePropertyAccessor:四张缓存表

ReflectivePropertyAccessor(spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java)维护四张缓存(第 96-102 行):

private final Map<PropertyCacheKey, InvokerPair> readerCache = new ConcurrentHashMap<>(64);
private final Map<PropertyCacheKey, Member> writerCache = new ConcurrentHashMap<>(64);
private final Map<PropertyCacheKey, TypeDescriptor> typeDescriptorCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, Method[]> sortedMethodsCache = new ConcurrentHashMap<>(64);

PropertyCacheKey 是个 record:(Class<?> clazz, String property, boolean targetIsClass)——注意第三维 targetIsClass,因为访问 Class 对象上的静态属性和访问实例上的同名属性是两回事,缓存必须区分。

canRead(第 133-170 行)先查 readerCache,命中即返回 true;否则找 getter:

Method method = findGetterForProperty(name, type, target);
if (method != null) {
	Property property = new Property(type, method, null);
	TypeDescriptor typeDescriptor = new TypeDescriptor(property);
	Method methodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, type);
	this.readerCache.put(cacheKey, new InvokerPair(methodToInvoke, typeDescriptor));
	this.typeDescriptorCache.put(cacheKey, typeDescriptor);
	return true;
}
else {
	Field field = findField(name, type, target);
	if (field != null) { ... readerCache.put(cacheKey, new InvokerPair(field, typeDescriptor)); }
}

read(第 174-237 行)真正执行:从缓存取 InvokerPair,是 Method 就 makeAccessible 后 invoke,是 Field 就 field.get。注意它返回的 TypedValue 用了 invoker.typeDescriptor.narrow(value)——按实际运行时窄化类型描述符。

13.6.2 getter 的三种命名约定与 record/Kotlin 数据类

findGetterForProperty(第 389-408 行)依次尝试三种命名:

Method method = findMethodForProperty(methodSuffixes, "get", clazz, mustBeStatic, 0, ANY_TYPES);
if (method == null) {
	method = findMethodForProperty(methodSuffixes, "is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
	if (method == null) {
		// Plain accessor method for a data class, for example, a Java
		// record component accessor or a Kotlin data class accessor such as name()
		method = findMethodForProperty(new String[] {propertyName}, "", clazz, mustBeStatic, 0, ANY_TYPES);
	}
}
if (method != null && method.getReturnType() == void.class) {
	method = null; // not a valid accessor method
}

顺序是:JavaBean 的 getX() → boolean 的 isX()(且返回类型必须是 Boolean/boolean)→ 裸名访问器 x()。第三种是 5.3+ 为 Java record 与 Kotlin data class 加的:record 组件的访问器方法名就是组件名本身(name() 而非 getName()),ReflectivePropertyAccessor 因此能把 record 的 user.name 解析到 User.name()。void 返回的一律不算访问器。

getPropertyMethodSuffixes(第 466-472 行)还处理了 xY 这种边界:属性名第二个字符大写时,只试 getXY() 一种(避免 getxY 的 JavaBean 约定歧义);否则两种都试。

13.6.3 OptimalPropertyAccessor:从"通用反射"到"专用反射"

createOptimalAccessor(第 533-579 行)是性能关键。通用 ReflectivePropertyAccessor.read 每次都要查缓存、判断 member 类型、再反射调用;而 OptimalPropertyAccessor(第 618-750 行)是个只认识"某类某属性"的专用 accessor,它持有固定的 Member(Method 或 Field),read 直接反射调用、不再查缓存:

private static class OptimalPropertyAccessor implements CompilablePropertyAccessor {
	private final Member member;
	private final TypeDescriptor typeDescriptor;
	...
	@Override
	public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
		if (this.member instanceof Method method) {
			ReflectionUtils.makeAccessible(method);
			Object value = method.invoke(target);
			return new TypedValue(value, this.typeDescriptor.narrow(value));
		}
		else {
			Field field = (Field) this.member;
			ReflectionUtils.makeAccessible(field);
			Object value = field.get(target);
			return new TypedValue(value, this.typeDescriptor.narrow(value));
		}
	}
	@Override
	public boolean isCompilable() {
		return (Modifier.isPublic(this.member.getModifiers()) &&
				Modifier.isPublic(this.member.getDeclaringClass().getModifiers()));
	}
}

它实现的是 CompilablePropertyAccessor(spel/CompilablePropertyAccessor.java)——除了 read,还有 generateCode(propertyName, mv, cf),能把"读属性"直接编译成 INVOKEVIRTUAL/GETFIELD 字节码。isCompilable 要求 member 与声明类都是 public——非 public 的类即使能反射读,也不能编译成字节码(否则生成的类访问不到)。这就是 PropertyOrFieldReference.isCompilable() 的判断依据:缓存的必须是个 CompilablePropertyAccessor 且 isCompilable() 为真。

13.6.4 ReflectiveMethodResolver:方法候选的加权排序

方法调用 MethodReference 把"选哪个重载"委托给 ReflectiveMethodResolver.resolve(spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java 第 116-190 行)。流程:

  1. 取出目标类所有方法,按名字过滤掉不匹配的;
  2. 若该类型注册了 MethodFilter(StandardEvaluationContext.registerMethodFilter),先过滤;
  3. 排序(第 134-151 行):参数个数少的排前,同参数个数时可变参数方法排后——因为固定参数签名是更精确的匹配:
    methods.sort((m1, m2) -> {
        int m1pl = m1.getParameterCount(); int m2pl = m2.getParameterCount();
        if (m1pl == m2pl) {
            if (!m1.isVarArgs() && m2.isVarArgs()) return -1;
            else if (m1.isVarArgs() && !m2.isVarArgs()) return 1;
            else return 0;
        }
        return Integer.compare(m1pl, m2pl);
    });
    
  4. 桥接方法消解:BridgeMethodResolver.findBridgedMethod 把泛型桥接方法还原成真实方法,再用 LinkedHashSet 去重;
  5. 逐个比对实参类型与形参类型,得到 ArgumentsMatchKind:exactMatch(精确匹配,立即返回 ReflectiveMethodExecutor)、closeMatch(子类型接近,记录最小 getTypeDifferenceWeight)、requiringConversion(需要 typeConverter 转换)。

三个匹配等级对应类注释里说的三种匹配:精确、子类型、可转换。useDistance 开启时,多个 closeMatch 之间用"类型差异权重"选最接近的——这和 Spring 容器在多个候选 bean 间按类型距离选优是同一套思想。

13.6.5 MethodReference 的执行与缓存

MethodReference.getValueInternal(spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java 第 119-232 行):先 getArguments(state) 求值实参(注意它临时把 scope root 压回活动上下文,第 244-246 行,使实参表达式里的无限定引用指向作用域根而非目标对象),然后:

MethodExecutor executorToUse = getCachedExecutor(evaluationContext, target, targetType, argumentTypes);
if (executorToUse != null) {
	try {
		state.trackOperation();
		return executorToUse.execute(evaluationContext, target, arguments);
	}
	catch (AccessException ex) {
		throwSimpleExceptionIfPossible(target, ex);
		this.cachedExecutor = null;   // 方法调用"变味"了,清缓存重试
		executorToUse = null;
	}
}
// 没缓存或缓存失效,走 resolve
MethodExecutorSearchResult searchResult = findMethodExecutor(argumentTypes, target, evaluationContext);
...
this.cachedExecutor = new CachedMethodExecutor(executorToUse, ..., targetType, argumentTypes);

CachedMethodExecutor 会记下解析时的 targetType 与 argumentTypes——下次调用时若目标类型/实参类型变了,缓存直接不命中,重新 resolve。异常处理里 throwSimpleExceptionIfPossible 会剥掉 InvocationTargetException,把方法内部真正抛出的业务异常原样抛给用户(这就是为什么 SpEL 里调 user.getName() 而 getName() 抛了 IllegalStateException,你看到的是 IllegalStateException 而不是反射包装异常)。

13.6.6 TypeLocator / TypeConverter / OperatorOverloader

  • StandardTypeLocator(spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java):T(Integer) 这种类型字面量靠它。构造函数里只注册了 java.lang 一个前缀(第 79 行 registerImport("java.lang"))——这是 7.0.9 的实际行为,网上常见说法(默认还含 java.util/java.net/java.io)在本仓库不成立。findType 先按全名 ClassUtils.forName,找不到再逐个拼前缀 prefix + "." + typeName 试;结果缓存在 typeCache(除非 classLoader 是 SmartClassLoader 且类可热重载)。
  • StandardTypeConverter(同包):委托给 Spring 核心 ConversionService;StandardEvaluationContext 默认 new StandardTypeConverter()(无 ConversionService,用默认转换),而 StandardBeanExpressionResolver 注入的是包装了 beanFactory 的 ConversionService 的版本。
  • StandardOperatorOverloader(同包):默认实现其实对标准类型不接管任何运算,ExpressionState.operate 走到它时基本抛 OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES——它存在的意义是允许用户注册自定义类型间的运算符语义(比如两个自定义金额对象相加)。

StandardEvaluationContext 的默认装配(initPropertyAccessors/initConstructorResolvers/initMethodResolvers,第 568-606 行)就是:ReflectivePropertyAccessor、ReflectiveConstructorResolver、ReflectiveMethodResolver 各一个;typeLocator/typeConverter 懒初始化。变量表 variables 是 ConcurrentHashMap,registerFunction(name, method) 本质就是 variables.put(name, method)——函数和变量共享同一个命名空间,这是类注释反复提醒的点。

13.7 源码剖析(五):其余关键 AST 节点

13.7.1 Indexer:数组/List/Map/String/对象的统一下标

Indexer(spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java)对应 expr[index]。它内部先把目标归类为 IndexedType 枚举的六种之一(第 89 行):

private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT, CUSTOM}

首次求值后缓存 indexedType,后续直接按缓存类型走对应分支:

  • ARRAY:Array.get(target, index);
  • LIST:((List<?>) target).get(index);
  • MAP:((Map<?,?>) target).get(index);
  • STRING:返回 String.valueOf(target.charAt(index))——即字符串第 n 个字符作为 String;
  • OBJECT:把 index 当属性名走 PropertyAccessor(map['key'] 与 map.key 在 SpEL 里等价);
  • CUSTOM:走用户注册的 IndexAccessor(6.2 新增的 SPI)。

它和 PropertyOrFieldReference 一样支持 null-safe(?.[0],6.2 起)与 7.0 的 Optional 解包(Optional<List> 上 ?.[0] 自动 optional.get().get(0))。AccessMode 枚举(READ/WRITE/READ_WRITE)则决定这个 indexer 节点是否支持 setValue——写回数组元素、List 元素、Map value 都走这里。

13.7.2 TypeReference:T() 类型字面量

TypeReference(spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java)对应 T(...)。getValueInternal(第 57-77 行):

String typeName = (String) this.children[0].getValueInternal(state).getValue();
state.trackOperation();
if (!typeName.contains(".") && Character.isLowerCase(typeName.charAt(0))) {
	TypeCode tc = TypeCode.valueOf(typeName.toUpperCase(Locale.ROOT));
	if (tc != TypeCode.OBJECT) {
		// It is a primitive type
		Class<?> clazz = makeArrayIfNecessary(tc.getType());
		this.exitTypeDescriptor = "Ljava/lang/Class";
		this.type = clazz;
		return new TypedValue(clazz);
	}
}
Class<?> clazz = state.findType(typeName);
clazz = makeArrayIfNecessary(clazz);
this.exitTypeDescriptor = "Ljava/lang/Class";
this.type = clazz;
return new TypedValue(clazz);

两层逻辑:短名且首字母小写的(int/boolean/void 等)走 TypeCode 枚举映射到基本类型 Class;否则委托 state.findType → TypeLocator.findType。dims(解析器从 T(int[][]) 里数出来的方括号数)通过 Array.newInstance 生成数组类——所以 T(String[]) 求值结果是 String[].class。所有 T(...) 节点的 exitTypeDescriptor 都是 Ljava/lang/Class,因为返回值永远是个 Class 对象。

13.7.3 BeanReference:@bean 与 BeanResolver

BeanReference(spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java)对应 @beanName/&beanName。求值极简(第 61-76 行):

public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
	BeanResolver beanResolver = state.getEvaluationContext().getBeanResolver();
	if (beanResolver == null) {
		throw new SpelEvaluationException(
				getStartPosition(), SpelMessage.NO_BEAN_RESOLVER_REGISTERED, this.beanName);
	}
	state.trackOperation();
	try {
		return new TypedValue(beanResolver.resolve(state.getEvaluationContext(), this.beanName));
	}
	catch (AccessException ex) { ... }
}

SpEL 引擎本身不认识 Spring 容器——它只知道 BeanResolver 这个 SPI。脱离容器裸跑 SpEL 时 context 没设 BeanResolver,@xxx 就抛 NO_BEAN_RESOLVER_REGISTERED。在 Spring 容器里,StandardBeanExpressionResolver 会 setBeanResolver(new BeanFactoryResolver(beanFactory)),@ 才真正变成"从 beanFactory 取 bean"。& 前缀的 FactoryBean 引用则把 beanName 带上 & 前缀(解析器层 maybeEatBeanReference 第 562-565 行处理),BeanFactoryResolver.resolve 内部按 & 前缀走 FactoryBean 协议。

13.7.4 Ternary 与 Elvis:短路求值

Ternary(Ternary.java 第 53-63 行):

Boolean condition = this.children[0].getValue(state, Boolean.class);
if (condition == null) {
	throw new SpelEvaluationException(getChild(0).getStartPosition(),
			SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean");
}
state.trackOperation();
TypedValue result = this.children[condition ? 1 : 2].getValueInternal(state);

条件必须能转成 boolean(null 不行),然后只求值选中的那一分支——这就是短路。它的 generateCode 生成 IFEQ 跳转字节码,与 Java 三元编译结果一致。

Elvis(Elvis.java 第 62-80 行)对应 a ?: b:

TypedValue leftHandTypedValue = this.children[0].getValueInternal(state);
Object leftHandValue = leftHandTypedValue.getValue();
if (leftHandValue instanceof Optional<?> optional) {
	this.exitTypeDescriptor = null;   // Optional 暂不支持编译
	if (optional.isPresent()) { return new TypedValue(optional.get()); }
	return this.children[1].getValueInternal(state);
}
if (leftHandValue != null && !"".equals(leftHandValue)) {
	return leftHandTypedValue;
}
return this.children[1].getValueInternal(state);

注意 elvis 的判定比"非 null"更宽:空字符串 "" 也会触发右值——name ?: 'anonymous' 在 name 为空串时也返回 'anonymous'。7.0 还加了 Optional 分支(present 取其值,empty 走右值),但这个分支暂不支持编译(exitTypeDescriptor=null,isCompilable 自然 false)。

13.7.5 VariableReference / FunctionReference:# 命名空间

  • VariableReference(#var):求值就是 state.lookupVariable(name) → context.lookupVariable(name),找不到返回 null。
  • FunctionReference(#fn(args)):从 context 取出注册的 Method/MethodHandle,反射调用。这就是 StandardEvaluationContext.registerFunction 的消费端——函数就是存在变量表里的 Method 对象。

13.7.6 InlineList / InlineMap 与选择/投影

  • InlineList({1,2,3})/InlineMap({a:1,b:2}):解析器层已把元素节点数组排好,求值时逐个 getValueInternal 收集成 List/Map。空 list {} 与空 map {:} 在解析器层就区分开了。
  • Selection(list.?[expr] 全选、^[ 选首个、$[ 选末个)与 Projection(list.![expr] 投影):它们进入时 state.enterScope(),把集合每个元素作为新的活动上下文压栈,使表达式里的 #this 指向当前元素;遍历完 exitScope()。这就是 ExpressionState.scopeRootObjects 栈的用途。
  • OperatorPower(a^b):Math.pow 语义;OpInc/OpDec:对数值变量自增自减(要求 context.isAssignmentEnabled())。

至此 AST 节点层的主要角色都已就位。下一节回到 SpelExpression,把"求值→可能触发编译"这条主线串起来。

13.8 源码剖析(六):SpelExpression 求值主线与编译模式

13.8.1 SpelExpression 的字段与编译阈值

SpelExpression(spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java)是 Expression 的实现。字段(第 66-90 行):

private static final int INTERPRETED_COUNT_THRESHOLD = 100;
private static final int FAILED_ATTEMPTS_THRESHOLD = 100;

private final String expression;
private final SpelNodeImpl ast;
private final SpelParserConfiguration configuration;
private @Nullable EvaluationContext evaluationContext;
private volatile @Nullable CompiledExpression compiledAst;
private final AtomicInteger interpretedCount = new AtomicInteger();
private final AtomicInteger failedAttempts = new AtomicInteger();
  • ast:解析好的语法树根,final,不可变;
  • compiledAst:编译产物,volatile——编译完成后对所有线程可见;
  • interpretedCount:解释执行次数,达到阈值触发编译;
  • failedAttempts:编译失败次数,超过 100 就放弃编译这条表达式。

13.8.2 getValue:先试编译产物,否则解释执行

以无参 getValue() 为例(第 131-155 行),八个重载结构相同:

@Override
public @Nullable Object getValue() throws EvaluationException {
	EvaluationContext context = getEvaluationContext();
	CompiledExpression compiledAst = this.compiledAst;
	if (compiledAst != null && context.isCompilationSupported()) {
		try {
			return compiledAst.getValue(context.getRootObject().getValue(), context);
		}
		catch (Throwable ex) {
			// If running in mixed mode, revert to interpreted
			if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
				this.compiledAst = null;
				this.interpretedCount.set(0);
			}
			else {
				// Running in SpelCompilerMode.immediate mode - propagate exception to caller
				throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
			}
		}
	}

	ExpressionState expressionState = new ExpressionState(context, this.configuration);
	Object result = this.ast.getValue(expressionState);
	checkCompile(expressionState);
	return result;
}

逐段解读:

  1. 编译快路径:compiledAst != null 且 context.isCompilationSupported()(7.0.9 新门控)时,直接 compiledAst.getValue(rootObject, context)。注意这里直接调编译产物、不再走 ast.getValueInternal——已编译表达式完全绕开解释器。
  2. 编译产物抛异常的两种处理:MIXED 模式下回退到解释(compiledAst=null、计数清零,下次再重新编译);IMMEDIATE 模式下把异常包成 SpelEvaluationException 抛给调用者。这正是 SpelCompilerMode 注释里说的"IMMEDIATE 下类型变了就直接报错"。
  3. 解释路径:new 一个 ExpressionState,递归 ast.getValue(expressionState)。
  4. checkCompile:每次解释求值后都检查是否该触发编译。

13.8.3 checkCompile 与 compileExpression:何时编译、失败怎么办

checkCompile(第 479-498 行):

private void checkCompile(ExpressionState expressionState) {
	this.interpretedCount.incrementAndGet();
	if (!expressionState.getEvaluationContext().isCompilationSupported()) {
		return;
	}
	SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode();
	if (compilerMode != SpelCompilerMode.OFF) {
		if (compilerMode == SpelCompilerMode.IMMEDIATE) {
			if (this.interpretedCount.get() > 1) {
				compileExpression();
			}
		}
		else {
			// compilerMode = SpelCompilerMode.MIXED
			if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) {
				compileExpression();
			}
		}
	}
}
  • 先 interpretedCount++;
  • context 不支持编译(isCompilationSupported()==false)直接返回——7.0.9 的门控在这里生效;
  • IMMEDIATE:解释超过 1 次(即第二次求值起)就编译;
  • MIXED:解释超过 100 次才编译——"先充分解释、确认类型稳定后再编译"。

compileExpression(第 506-552 行):

public boolean compileExpression() {
	CompiledExpression compiledAst = this.compiledAst;
	if (compiledAst != null) { return true; }
	if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) { return false; }

	synchronized (this) {
		if (this.compiledAst != null) { return true; }   // 别的线程已经编译好了
		try {
			SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
			compiledAst = compiler.compile(this.ast);
			if (compiledAst != null) {
				this.compiledAst = compiledAst;
				return true;
			}
			else {
				this.failedAttempts.incrementAndGet();
				return false;
			}
		}
		catch (Exception ex) {
			this.failedAttempts.incrementAndGet();
			if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
				this.compiledAst = null; this.interpretedCount.set(0);
				return false;
			}
			else {
				throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_COMPILING_EXPRESSION);
			}
		}
	}
}

双重检查锁:外层先读 compiledAst,进 synchronized(this) 再读一次(别的线程可能已编译)。编译失败(compiler.compile 返回 null,通常因为某节点 isCompilable()==false——比如用了不支持编译的特性、或某属性类型还没确定)就 failedAttempts++。失败 100 次后 checkCompile 外层直接 return false,不再浪费 CPU。

13.8.4 SpelCompiler:用 ASM 现场生成一个 CompiledExpression 子类

SpelCompiler(spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java)。它是按 ClassLoader 缓存的(第 77 行 Map<ClassLoader, SpelCompiler> compilers),getCompiler(classLoader) 取/建。

compile(SpelNodeImpl)(第 101-122 行):

public @Nullable CompiledExpression compile(SpelNodeImpl expression) {
	if (expression.isCompilable()) {
		Class<? extends CompiledExpression> clazz = createExpressionClass(expression);
		if (clazz != null) {
			try {
				return ReflectionUtils.accessibleConstructor(clazz).newInstance();
			}
			catch (Throwable ex) { throw new IllegalStateException(...); }
		}
	}
	return null;
}

前提是根节点 isCompilable()——而这个方法会递归要求所有子节点 isCompilable() 且 exitTypeDescriptor != null。这就是"编译一定发生在解释若干次后"的根因:exitTypeDescriptor 只有解释求值过才填。

createExpressionClass(第 135-170 行)用 ASM 生成类:

String className = "org/springframework/expression/spel/generated/CompiledExpression" + getNextSuffix();
cw.visit(V17, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
// 默认构造函数 ...
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
		"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", ...);
CodeFlow cf = new CodeFlow(className, cw);
try {
	expressionToCompile.generateCode(mv, cf);
}
catch (IllegalStateException ex) {
	return null;   // 生成代码中途放弃编译
}

生成的类继承 CompiledExpression,重写 getValue(Object rootObject, EvaluationContext ctx),方法体由 AST 各节点的 generateCode 协作生成。类名带自增后缀 %05d,由编译器持有的 ChildClassLoader(一个 URLClassLoader 子类,第 288 行)defineClass 加载。CLASSES_DEFINED_LIMIT=100(第 71 行)是每个 child loader 最多定义的类数,超限换新 loader,避免类元数据膨胀。

13.8.5 三种编译器模式与适用场景

SpelCompilerMode(spel/SpelCompilerMode.java)三态:

  • OFF(默认):全程解释。最灵活,类型随时可变;
  • IMMEDIATE:第一次解释后尽快编译;编译产物运行时一旦因类型变化抛异常,直接冒泡给调用者。适合类型完全稳定、追求极致性能的场景;
  • MIXED:解释 100 次后才编译;编译产物运行抛异常则自动回退解释、清零计数重新观察。在性能与稳健之间取平衡。

7.0.9 还在 EvaluationContext 加了 isCompilationSupported()——与 SpelParserConfiguration 的编译器模式正交:两边都允许才编译。SimpleEvaluationContext.Builder 有个 withCompilationSupported() 开关,受限上下文可以显式禁止编译(因为编译产物绑定了某次解释的动态类型,在安全敏感场景下可能固化出意外行为)。

这也点出了编译模式的根本限制:编译基于"解释时观察到的类型",如果后续求值时根对象类型变了(比如 x.name 第一次 x 是 Person、第二次 x 是 String),编译产物会按旧类型生成字节码,运行就会抛异常——所以文档反复强调:编译只适合"类型信息在重复求值间不变"的表达式。

13.9 源码剖析(七):与 Spring 容器的集成——@Value("#{...}") 的求值链路

13.9.1 链路总览

@Value("#{...}") 的值不是在注解扫描时直接算好的,而是作为 bean 定义里的"值字符串",在 bean 创建、属性装配阶段交给 BeanExpressionResolver 求值。完整链路:

AutowiredAnnotationBeanPostProcessor (扫描 @Value)
  → 把注解里的字符串作为待解析值
  → AbstractBeanFactory.evaluateBeanDefinitionString(value, mbd)
      → beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope))
          → StandardBeanExpressionResolver.evaluate(...)
              → expressionCache.computeIfAbsent(value, parser.parseExpression(expr, templateContext))
              → evaluationCache.computeIfAbsent(bec, 构造 StandardEvaluationContext)
              → expr.getValue(evalContext)

13.9.2 AbstractBeanFactory.evaluateBeanDefinitionString

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java 第 1649-1662 行:

protected @Nullable Object evaluateBeanDefinitionString(@Nullable String value, @Nullable BeanDefinition beanDefinition) {
	if (this.beanExpressionResolver == null) {
		return value;
	}

	Scope scope = null;
	if (beanDefinition != null) {
		String scopeName = beanDefinition.getScope();
		if (scopeName != null) {
			scope = getRegisteredScope(scopeName);
		}
	}
	return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope));
}

它是 beanFactory 暴露给内部(属性值解析、构造器参数解析、甚至第 1606 行的 beanClassName 动态解析)的统一入口:若工厂没配 beanExpressionResolver(纯 spring-beans 裸场景),原样返回;否则按 bean 的 scope 构造一个 BeanExpressionContext(它把 beanFactory 与 scope 包在一起,作为求值时的"伪 rootObject"),交给 resolver。

13.9.3 StandardBeanExpressionResolver:缓存、模板与上下文装配

StandardBeanExpressionResolver 位于 spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java(注意不是 spring-beans,7.0.9 以仓库为准)。它的核心是 evaluate(第 159-187 行):

@Override
public @Nullable Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException {
	if (!StringUtils.hasLength(value)) {
		return value;
	}
	try {
		Expression expr = this.expressionCache.computeIfAbsent(value, expression ->
				this.expressionParser.parseExpression(expression, this.beanExpressionParserContext));
		EvaluationContext evalContext = this.evaluationCache.computeIfAbsent(beanExpressionContext, bec -> {
				ConfigurableBeanFactory beanFactory = bec.getBeanFactory();
				StandardEvaluationContext sec = new StandardEvaluationContext(bec);
				sec.addPropertyAccessor(new BeanExpressionContextAccessor());
				sec.addPropertyAccessor(new BeanFactoryAccessor());
				sec.addPropertyAccessor(new org.springframework.expression.spel.support.MapAccessor());
				sec.addPropertyAccessor(new EnvironmentAccessor());
				sec.setBeanResolver(new BeanFactoryResolver(beanFactory));
				sec.setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader()));
				sec.setTypeConverter(new StandardTypeConverter(() -> {
					ConversionService cs = beanFactory.getConversionService();
					return (cs != null ? cs : DefaultConversionService.getSharedInstance());
				}));
				customizeEvaluationContext(sec);
				return sec;
			});
		return expr.getValue(evalContext);
	}
	catch (Throwable ex) {
		throw new BeanExpressionException("Expression parsing failed", ex);
	}
}

逐段:

  1. 两层缓存:expressionCache(ConcurrentHashMap(256))把"值字符串 → Expression"缓存,避免重复 parse;evaluationCache(ConcurrentHashMap(8))把"BeanExpressionContext → StandardEvaluationContext"缓存。beanFactory 是单例,这两个缓存让 #{...} 在容器里几乎零重复解析成本。
  2. 模板上下文 beanExpressionParserContext(第 88-101 行):isTemplate()=true、前缀 "#{"、后缀 "}"。这就是 TemplateAwareExpressionParser 分流的来源——parseExpression(expr, context) 走 parseTemplate 分支。
  3. 上下文装配:rootObject 是 BeanExpressionContext;额外注册 BeanExpressionContextAccessor(把 environment/systemProperties/systemEnvironment 等预定义变量当属性读)、BeanFactoryAccessor、MapAccessor、EnvironmentAccessor;BeanResolver 设为 BeanFactoryResolver(让 @bean 能查容器);TypeLocator 用 beanClassLoader(保证能 load 应用类);TypeConverter 包装 beanFactory 的 ConversionService(让 SpEL 转换复用容器的类型转换)。

构造函数(第 120-124 行)里 SpelParserConfiguration(null, beanClassLoader, false, false, Integer.MAX_VALUE, ...)——编译模式 OFF(前两个 false 对应 compilerMode 与 compilerClassLoader 关闭),即容器场景下 SpEL 默认不编译,只用解释+缓存。

13.9.4 模板解析:TemplateAwareExpressionParser 怎么拆 "#{...}"

容器值往往是混合文本,如 user.name = #{user.name} 或 prefix-#{a}-#{b}。TemplateAwareExpressionParser.parseExpressions(第 100-143 行)按前缀 #{ 切分:找到前缀,前缀前的静态文本包成 LiteralExpression,#{ 与匹配 } 之间的内容递归 doParseExpression 成真正的 SpEL 表达式;多段则包成 CompositeStringExpression,求值时把各段结果拼接。

skipToCorrectEndSuffix(第 175-231 行)处理嵌套:它用栈配对 (){[]},并跳过单/双引号字符串字面量内部的括号——所以 #{name == 'a}b'} 里的 } 不会被误判为结束符。这就是 #{...} 能正确处理字符串内含 } 的关键。

13.9.5 #{} 与 ${}:求值时机的本质区别

这是最高频的混淆点,必须讲清:

  • ${...}:是属性占位符,由 PropertySourcesPlaceholderConfigurer 在 bean 定义加载阶段解析,查的是 Environment/属性源,不经过 SpEL 引擎。它在容器早期、bean 实例化之前就被替换成字面值。
  • #{...}:是 SpEL 表达式,由 StandardBeanExpressionResolver 在 bean 创建、属性装配阶段求值,可以引用 bean(@bean)、调方法、做运算、访问 environment。

两者可以嵌套:#{${some.prop}} 先由 placeholder 把 ${some.prop} 替换成真实字符串,再交给 SpEL 求值。时机上 ${} 在前(定义后处理)、#{} 在后(实例化时)。

13.9.6 边界澄清:AOP 切点表达式不是 SpEL

需要明确:Spring AOP 的切点表达式(execution(...)、@annotation(...)、bean(...))用的是 AspectJ 切点语言(Pointcut Language),由 spring-aop 的 PointcutParser 解析,与 spring-expression 的 SpEL 引擎完全无关。bean(xxx) 这种切点 designator 名字像 SpEL,但底层不是 SpEL。Spring Security 的 @PreAuthorize、Spring Data 的 @Query 里的表达式才是真正复用 SpEL(它们自建 EvaluationContext,把方法参数以 #paramName 暴露)。本章不展开 Security/Validation 细节,只确认机制存在、且走的是同一套 SpelExpressionParser + 自定义 EvaluationContext。

13.10 运行机制:从 parseExpression 到 getValue 的一次完整旅程

把前面所有类串成一条时间线。以表达式 person?.address?.city ?: 'unknown'、rootObject 为一个 Person 实例、用 StandardEvaluationContext 为例:

用户代码
  │  parser.parseExpression("person?.address?.city ?: 'unknown'")
  ▼
SpelExpressionParser.doParseExpression
  │  new InternalSpelExpressionParser(config).doParseExpression(...)
  ▼
InternalSpelExpressionParser
  │  1. new Tokenizer(expr).process()            ── 词法:产出 [IDENTIFIER(person), DOT, SAFE_NAVI(?),  │                                                 IDENTIFIER(address), DOT, SAFE_NAVI(?),  │                                                 IDENTIFIER(city), ELVIS(?:), IDENTIFIER(unknown)]
  │  2. eatExpression → eatLogicalOr → ... → eatPrimaryExpression
  │     eatStartNode: PropertyOrFieldReference(person)
  │     eatDottedNode: ?. → PropertyOrFieldReference(address, nullSafe=true)
  │     eatDottedNode: ?. → PropertyOrFieldReference(city, nullSafe=true)
  │     → CompoundExpression[person, ?.address, ?.city]
  │  3. 顶层 peekToken == ELVIS → new Elvis(compound, StringLiteral('unknown'))
  ▼
new SpelExpression(expr, ast=Elvis, config)       ── 返回给用户,可重复 getValue
  │
  │  expression.getValue(context)
  ▼
SpelExpression.getValue
  │  compiledAst==null(未编译)→ 走解释
  │  new ExpressionState(context, config)
  ▼
ast.getValue(expressionState)  即 Elvis.getValueInternal
  │  children[0]=CompoundExpression → 递归求值
  │    PropertyOrFieldReference(person).getValueInternal
  │      → readProperty: 遍历 accessor,命中 ReflectivePropertyAccessor.canRead
  │      → createOptimalAccessor → OptimalPropertyAccessor 缓存到 node.cachedReadAccessor
  │      → accessor.read → 反射调 getPerson()
  │    ?.address: nullSafe,target==null 则直接返回 NULL
  │      否则 readProperty(address) → 反射 getAddress()
  │    ?.city: readProperty(city) → 反射 getCity()
  │  回到 Elvis: leftValue==null 或 ==''?  → 是则取 children[1]='unknown';否则返回 left
  ▼
SpelExpression.checkCompile
  │  interpretedCount++ → OFF 模式下什么都不做;MIXED 下累计到 100 才编译
  ▼
返回最终值

这条时间线里有几个设计要点值得点明:

  1. 词法与语法严格分离。Tokenizer 只认字符、不理解语法;InternalSpelExpressionParser 只认 token 流。两者通过 List<Token> 解耦,词法错误(如未闭合字符串)在 process() 就抛,语法错误(如 MORE_INPUT)在 eatExpression 后抛。
  2. AST 不可变、状态可缓存。SpelExpression 的 ast 是 final 的,但节点内部的 cachedReadAccessor/cachedExecutor/exitTypeDescriptor 是可变的 volatile 字段——多次求值时节点"边跑边学",把第一次解析出的反射成员缓存下来。
  3. 解释与编译的平滑切换。compiledAst 一旦就绪,后续 getValue 完全绕开 ast;编译产物运行失败(MIXED)又能一键回退解释。这个开关在 getValue 每次调用处都是一个 volatile 读,无锁。
  4. trackOperation 贯穿每个节点。每次反射调用、每次运算符都计数,防病态表达式。

13.11 扩展用法与最佳实践

13.11.1 两种 EvaluationContext 的选择

StandardEvaluationContext 功能全开:属性、方法、构造器(new)、类型字面量(T())、bean 引用、变量/函数、赋值。它的类注释用全大写警告:绝不能用于求值来自不可信来源的表达式——因为 T(java.lang.Runtime).getRuntime().exec(...) 这类表达式能在你机器上跑任意命令。

SimpleEvaluationContext(spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java)是受限子集:它的 typeNotFoundTypeLocator(第 130-132 行)直接抛 TYPE_NOT_FOUND——没有 T()、没有 new、没有 bean 引用;属性访问默认用 DataBindingPropertyAccessor(不写)。三种工厂方法:

  • forReadOnlyDataBinding():只读数据绑定;
  • forReadWriteDataBinding():可读可写;
  • forPropertyAccessors(...):自定义 accessor。

Builder 还能 withAssignmentDisabled()(关闭 =/++/--)与 withCompilationSupported(...)(7.0.9 门控)。但要注意它的类注释同样强调:"best-effort"限制,不等于绝对安全——只读模式管的是"不能赋值",不管"读属性有没有副作用"(比如 file.delete() 形状上也是个无参 getter)。所以面向用户输入的表达式,正确做法是:表达式本身由你方写死、用户只提供数据,而不是让用户写表达式。

13.11.2 自定义 PropertyAccessor 与 registerFunction

  • 自定义 PropertyAccessor:实现 getSpecificTargetClasses()(返回 null 表示通用,或返回目标类型数组做快速过滤)、canRead/read/canWrite/write,然后 context.addPropertyAccessor(myAccessor)——它会排在默认 ReflectivePropertyAccessor 之前(addBeforeDefault,StandardEvaluationContext 第 608-610 行,插到列表倒数第二位)。想给某种特殊对象(比如自定义 JSON 对象树)提供属性访问,这是扩展点。
  • registerFunction:context.registerFunction("upper", SomeClass.getMethod("toUpper", String.class)),表达式里就能写 #upper('abc')。本质是把 Method 放进变量表,FunctionReference 求值时反射调用。
  • BeanResolver:脱离容器想让 @bean 可用,实现 BeanResolver.resolve(context, name) 并 context.setBeanResolver(...) 即可;Spring 容器里用 BeanFactoryResolver。

13.11.3 性能与线程安全

  • 解析复用:SpelExpressionParser 线程安全,全局单例即可;Expression 设计为"解析一次、多次求值",不要每次求值都 parseExpression。
  • 缓存前提:Expression 缓存了 accessor/executor,跨 context 复用要求 context 类型与配置一致;绝不能先 StandardEvaluationContext 后 SimpleEvaluationContext(安全边界被缓存击穿)。
  • 编译模式选择:默认 OFF 足够;热路径、类型稳定的高频表达式(如规则引擎)可上 MIXED;IMMEDIATE 只在你确定类型永不变时用,否则一次类型漂移就直接抛给调用方。
  • maximumOperations 与表达式长度:对外暴露求值能力时,用 SpelParserConfiguration 收紧这两个上限,防 DoS。

13.12 常见问题与坑

13.12.1 #{} 与 ${} 混淆

最经典的坑。@Value("${x}") 取的是属性源里的值(占位符,容器早期解析),@Value("#{x}") 才是 SpEL(实例化时求值、可引用 bean)。把 @Value("#{'${x}'}") 写在一起时,先 ${x} 被占位符替换成字面量、再包成 SpEL 字符串——这是"注入属性值并允许 SpEL 后处理"的惯用法。报错时先分清是 IllegalArgumentException: Could not resolve placeholder(占位符侧)还是 SpelEvaluationException(SpEL 侧)。

13.12.2 null 安全:?. 与 ?: 的分工

  • ?.(SAFE_NAVI):在属性/方法/索引链上"当前目标为 null 就整段返回 null",对应 PropertyOrFieldReference/MethodReference/Indexer 里 isNullSafe() 分支。
  • ?:(Elvis):"左边为 null 或空串就取右边默认值"。 两者可组合:person?.address?.city ?: 'unknown'——?. 负责链上 null 短路,?: 负责最终默认值。7.0 起两者还都对 Optional 自动解包(opt?.city、opt ?: fallback)。

13.12.3 字符串转义

SpEL 单引号串里要写单引号必须双写:'It''s';不支持 \'。双引号串同理 ""。模板里 #{...} 内含 } 时,只要它在字符串字面量内,skipToCorrectEndSuffix 的栈配对能正确处理;但跨层嵌套括号仍要配平。

13.12.4 类型转换失败

getValue(desiredResultType) 失败时,底层是 StandardTypeConverter.convertValue → 容器 ConversionService 抛 ConverterNotFoundException。常见于 SpEL 算出 String 而你要 Integer、或 bean 没注册 ConversionService。排查时先去掉期望类型、看 getValue() 裸结果是什么类型。

13.12.5 编译模式下的动态类型限制

开了 MIXED/IMMEDIATE 后,某条表达式第一次用 Person 求值(exitTypeDescriptor 被钉成 Person),第二次换成 String 根对象,编译产物按 Person 生成的字节码运行就会抛异常。MIXED 会回退解释并清零计数重新观察;IMMEDIATE 直接把异常抛给调用方。症状:间歇性 SpelEvaluationException: exception running compiled expression。对策:根对象类型会变的表达式保持 OFF;或确保同一 Expression 实例只喂同类型根对象。

13.12.6 T() 与类加载器

T(java.util.List) 能解析是因为 StandardTypeLocator 默认只注册 java.lang——所以 T(List) 找不到、必须写全限定名 T(java.util.List),或手动 typeLocator.registerImport("java.util")。Web 容器场景下类加载器层级复杂,务必给 StandardTypeLocator 传正确的 ClassLoader(StandardBeanExpressionResolver 传的就是 beanFactory.getBeanClassLoader()),否则 T(com.you.App) 抛 TYPE_NOT_FOUND。

13.12.7 表达式注入(SpEL Injection)安全

让用户直接写 SpEL 表达式是高危操作:T(java.lang.Runtime).getRuntime().exec('rm -rf /')、T(java.lang.System).exit(1) 都能跑。缓解手段:用 SimpleEvaluationContext(砍掉 T()/new/@bean)、withAssignmentDisabled()、限制 maximumOperations 与最大表达式长度;但如前所述,这是"best-effort",真正的防线是不让用户写表达式,只让用户提供被表达式引用的数据。

13.12.8 方法重载解析的意外

ReflectiveMethodResolver 按参数个数排序、可变参数排后、按类型差异权重选 close match。当目标类有多个同名重载、且实参类型介于其间时,可能选中你不期望的那个(尤其涉及自动装箱/转换)。StandardEvaluationContext.registerMethodFilter(type, filter) 可以在候选集上做一次自定义过滤/排序,是精确控制重载选择的手段。

13.13 本章小结

本章沿"词法 → 语法 → AST → 求值 → 编译 → 容器集成"的完整流水线剖析了 Spring Framework 7.0.9 的 SpEL 引擎:

  • 词法层 Tokenizer 用字符指针 + 哨兵 + 256 位字符表,把输入切成 57 种 TokenKind;双字符 token 抢先判断、字符串双写转义、3.toString() 的小数点回退、EQ/NE 等文本别名,都是这一层的精巧设计。
  • 语法层 InternalSpelExpressionParser 用八个嵌套 eatXxx 方法实现优先级链,or→and→relational→sum→product→power→unary→primary,左结合循环建 AST;顶层叠 =/?:/?: 三种语法糖。
  • 求值层 SpelNodeImpl.getValueInternal 递归求值,PropertyOrFieldReference/MethodReference 把解析结果缓存到节点字段,ReflectivePropertyAccessor 四张缓存表 + OptimalPropertyAccessor 专用反射 accessor,把重复求值成本压到一次反射调用。
  • 编译层 SpelCompiler 在解释 100 次(MIXED)或 1 次(IMMEDIATE)后用 ASM 生成 CompiledExpression 子类字节码;7.0.9 新增 EvaluationContext.isCompilationSupported() 与 SpelParserConfiguration 正交门控。
  • 集成层 @Value("#{...}") 经 AbstractBeanFactory.evaluateBeanDefinitionString → StandardBeanExpressionResolver(spring-context)→ SpelExpressionParser 模板模式,两层缓存避免重复解析;#{} 与 ${} 时机不同、引擎不同。
  • 安全层 StandardEvaluationContext 功能全开但禁跑不可信输入,SimpleEvaluationContext 砍掉 T()/new/@bean 做数据绑定子集。

本章关键类清单

类名模块与相对路径职责一句话
SpelExpressionParserspring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java线程安全的解析器外壳,转发到内部 parser
InternalSpelExpressionParserspring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java手写递归下降语法分析,优先级链与 AST 构造
Tokenizerspring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java词法扫描,字符流 → Token 流
TokenKindspring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java57 种 token 枚举,预存符号与载荷标记
SpelExpressionspring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.javaExpression 实现,持有 AST/编译产物/计数阈值
SpelNodeImplspring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.javaAST 节点抽象基类,getValueInternal + generateCode
ExpressionStatespring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java单次求值临时状态,活动对象栈/作用域栈/操作计数
OpPlusspring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java加法与字符串拼接,数值提升顺序
PropertyOrFieldReferencespring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java属性/字段读取,accessor 缓存与 null-safe
MethodReferencespring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java方法调用,executor 缓存与重载执行
Indexerspring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java数组/List/Map/String/对象统一下标
TypeReferencespring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.javaT() 类型字面量,委托 TypeLocator
BeanReferencespring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java@bean 引用,委托 BeanResolver
Ternary / Elvisspring-expression/src/main/java/org/springframework/expression/spel/ast/三元短路 / elvis 默认值
StandardEvaluationContextspring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java功能全开的求值上下文,默认反射三件套
SimpleEvaluationContextspring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java受限子集上下文,无 T()/new/@bean
ReflectivePropertyAccessorspring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java反射属性访问,四张缓存表 + OptimalPropertyAccessor
ReflectiveMethodResolverspring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java方法候选加权排序与桥接方法消解
StandardTypeLocatorspring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java类型定位,默认仅注册 java.lang
SpelCompilerspring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.javaASM 生成 CompiledExpression 子类字节码
TemplateAwareExpressionParserspring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java模板模式 #{...} 切分与括号配对
StandardBeanExpressionResolverspring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java容器内 #{...} 求值,两层缓存 + 上下文装配
AbstractBeanFactoryspring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.javaevaluateBeanDefinitionString 统一入口

13.14 源码剖析(补):复合表达式、短路逻辑与集合选择的作用域机制

前面几节按"节点类型"拆开讲了求值,这一节把三个容易被一笔带过、却最能体现 SpEL 设计巧思的机制讲透:复合表达式如何逐层切换活动对象、布尔逻辑如何短路、集合选择如何用作用域栈把 #this 绑到当前元素。

13.14.1 CompoundExpression:逐层 push/pop 活动上下文对象

a.b.c 在语法层被包成 CompoundExpression,三个孩子依次是 PropertyOrFieldReference(a)、PropertyOrFieldReference(b)、PropertyOrFieldReference(c)。它的 getValueRef(CompoundExpression.java 第 53-86 行)展示了链式求值的核心循环:

SpelNodeImpl nextNode = this.children[0];
try {
	TypedValue result = nextNode.getValueInternal(state);
	int cc = getChildCount();
	for (int i = 1; i < cc - 1; i++) {
		try {
			state.pushActiveContextObject(result);
			nextNode = this.children[i];
			result = nextNode.getValueInternal(state);
		}
		finally {
			state.popActiveContextObject();
		}
	}
	try {
		state.pushActiveContextObject(result);
		nextNode = this.children[cc - 1];
		return nextNode.getValueRef(state);
	}
	finally {
		state.popActiveContextObject();
	}
}
catch (SpelEvaluationException ex) {
	ex.setPosition(nextNode.getStartPosition());
	throw ex;
}

关键在 state.pushActiveContextObject(result):求值完 a 拿到结果后,把它压入 ExpressionState.contextObjects 栈,再求值 b——于是 b 节点的 state.getActiveContextObject() 返回的是 a 的结果,而不是原始 rootObject。这就是 a.b 里 b 是"对 a 的结果求属性"的实现方式。每一层处理完 finally popActiveContextObject,保证作用域不乱套。错误时 ex.setPosition(nextNode.getStartPosition()) 把异常位置修正到出错的那一段——这就是 SpEL 报错信息能精确指向 a.b.c 中 b 的源码位置的原因。

PropertyOrFieldReference.getValueInternal 取的正是 state.getActiveContextObject()(第 111 行),所以它天然知道"自己该对哪个对象读属性"。整个引擎里"无限定引用对谁解析"这个问题,答案永远是:当前 contextObjects 栈顶。

13.14.2 OpAnd / OpOr:短路与布尔断言

OpAnd.getValueInternal(OpAnd.java 第 48-56 行):

public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
	state.trackOperation();
	if (!getBooleanValue(state, getLeftOperand())) {
		// no need to evaluate right operand
		return BooleanTypedValue.FALSE;
	}
	return BooleanTypedValue.forValue(getBooleanValue(state, getRightOperand()));
}

左操作数为 false 就直接返回 FALSE,右操作数根本不求值——短路。getBooleanValue 把操作数强制转成 Boolean.class,转出来是 null 就抛 TYPE_CONVERSION_ERROR(null 不能当布尔用)。OpOr 对称:左为 true 直接返回 TRUE。构造函数里 this.exitTypeDescriptor = "Z"(第 44 行)写死——逻辑运算结果永远是 boolean,编译期可确定。这也是为什么逻辑节点是最早一批可编译的节点。

13.14.3 Selection:进入元素作用域,让 #this 指向当前元素

list.?[#this.age > 18] 的语义是"筛出 age>18 的元素"。Selection.getValueRef(Selection.java 第 110 行起)对 Map 分支的处理最能说明问题:

SpelNodeImpl selectionCriteria = this.children[0];
if (operand instanceof Map<?, ?> mapdata) {
	Map<Object, Object> result = new HashMap<>();
	Object lastKey = null;
	for (Map.Entry<?, ?> entry : mapdata.entrySet()) {
		try {
			TypedValue kvPair = new TypedValue(entry);
			state.pushActiveContextObject(kvPair);
			state.enterScope();
			Object val = selectionCriteria.getValueInternal(state).getValue();
			if (val instanceof Boolean b) {
				if (b) {
					result.put(entry.getKey(), entry.getValue());
					if (this.variant == FIRST) {   // ^[  命中即返回
						return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this);
					}
					lastKey = entry.getKey();
				}
			}
			else {
				throw new SpelEvaluationException(..., SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN);
			}
		}
		finally {
			state.popActiveContextObject();
			state.exitScope();
		}
	}
	...
}

三层细节:

  1. 每个元素压栈:把当前 Map.Entry 包成 TypedValue 压为活动上下文对象,于是选择条件里的 #this 就是这个 entry;enterScope() 同时压入 scopeRootObjects,使条件里的无限定引用也指向当前元素。
  2. 条件必须返回布尔:不是 Boolean 直接抛 RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN——这是写 SpEL 选择条件最常踩的坑之一(条件写成 #this.age 忘了比较)。
  3. 三种 variant:ALL(?[,全收集)、FIRST(^[,命中第一个就 return)、LAST($[,最后单独处理只保留 lastKey)。Iterable/数组分支(第 178 行起)结构一致。null-safe 时 operand 为 null 或 empty Optional 返回 NullValueRef.INSTANCE。

Projection(list.![expr])结构几乎一样,区别只是把每个元素求值结果收集成新 List,不做布尔过滤。

13.14.4 SpelParserConfiguration:防护参数全家桶

SpelParserConfiguration(spel/SpelParserConfiguration.java)除了编译器模式,还承载一组防 DoS 防护参数(第 43-60 行常量):

public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000;
public static final int DEFAULT_MAX_OPERATIONS = 10_000;
public static final int DEFAULT_MAX_BIG_POWER_BITS = 1_000_000;
  • maximumExpressionLength:doParseExpression 开头 checkExpressionLength 拦截超长输入,默认 1 万字符;StandardBeanExpressionResolver 还能用 spring.context.expression.maxLength 系统属性覆盖。
  • maximumOperations:ExpressionState.trackOperation 每次反射/运算自增,超过 1 万抛 MAX_OPERATIONS_EXCEEDED——针对病态表达式(如超长选择/投影)的计算量防护。
  • maximumBigPowerBits:BigInteger 幂运算 a^b 的结果位上限,防止 2^99999999 这种超大数撑爆内存。

另有 autoGrowNullReferences/autoGrowCollections/maximumAutoGrowSize(第 117-121 行):开启后链式路径上的 null 引用自动 new 空集合/对象(PropertyOrFieldReference 第 126-167 行的 autoGrow 分支消费它),是宽松数据绑定的便利开关。这些字段都是 final、构造时确定,解析/求值过程中不可变。

13.15 源码剖析(补):编译产物契约、accessor 排序与构造器解析

13.15.1 CompiledExpression:编译产物的唯一契约

SpelCompiler 生成的每个类都继承自 CompiledExpression(spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java),它只有一个抽象方法:

public abstract class CompiledExpression {
	public abstract Object getValue(@Nullable Object target, @Nullable EvaluationContext context)
			throws EvaluationException;
}

这就是 13.8.2 里 compiledAst.getValue(rootObject, context) 的落点。类注释明说"intended for SpelCompiler 生成,不打算让用户子类化"——它是 SpEL 引擎与 ASM 生成代码之间的固定 ABI:编译期把 AST 的 generateCode 输出填进这个 getValue 方法体,运行期 SpelExpression 拿到实例直接调。解释器里那套"遍历 AST、push/pop 活动对象栈、逐个节点反射"在编译产物里全部消失,被等价的线性字节码替代。

13.15.2 AccessorUtils.getAccessorsToTry:精确匹配优先于通用 accessor

PropertyOrFieldReference 慢路径调 AccessorUtils.getAccessorsToTry(target, accessors)(spring-expression/src/main/java/org/springframework/expression/spel/ast/AccessorUtils.java 第 49-74 行)。它不是简单地把 context 注册的 accessor 列表原样返回,而是重排:

注册的 accessor 按序排列;返回列表里,对目标类型精确匹配的 accessor 排最前,其次是匹配目标类型父类的 accessor,最后才是"通用 accessor"(getSpecificTargetClasses() 返回 null 的)。

这有两个后果:其一,用户 addPropertyAccessor 注册的针对特定类型的 accessor,会自动排在通用 ReflectivePropertyAccessor 前面——你想给某种特殊对象定制访问,不用手动插到最前,框架按类型亲和度帮你排序;其二,通用 accessor 兜底放最后。StandardEvaluationContext.addPropertyAccessor 的 addBeforeDefault(插到列表倒数第二位)与 AccessorUtils 的重排配合,保证自定义 accessor 既在默认 accessor 之前被尝试,又不会越位抢掉更精确的 accessor。

13.15.3 ReflectiveConstructorResolver:new 表达式的构造器选优

new java.util.Date() 走 ConstructorReference → ReflectiveConstructorResolver.resolve(spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java 第 60 行起)。结构与方法解析几乎同构:

Class<?> type = context.getTypeLocator().findType(typeName);
Constructor<?>[] ctors = type.getConstructors();
Arrays.sort(ctors, Comparator.comparingInt(Constructor::getParameterCount));
Constructor<?> closeMatch = null;
Constructor<?> matchRequiringConversion = null;
for (Constructor<?> ctor : ctors) {
	...
	if (ctor.isVarArgs() && argumentTypes.size() >= paramCount - 1) {
		matchKind = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter);
	}
	else if (paramCount == argumentTypes.size()) {
		matchKind = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter);
	}
	...
}

构造器按参数个数升序排(无参构造优先),再用与方法解析同一套 ReflectionHelper.compareArguments 判定精确/接近/需转换三档。注释里那句 "sigh complicated" 是作者对可变构造参数(Object...)匹配复杂度的自嘲——可变参数分支要把实参打包成数组,边界情况很多。最终选中的构造器包成 ReflectiveConstructorExecutor 返回。

13.15.4 模板表达式的两个具体实现

TemplateAwareExpressionParser.parseTemplate 把混合文本拆成多段后,静态文本段包成 LiteralExpression、整体多段包成 CompositeStringExpression(都在 spring-expression/src/main/java/org/springframework/expression/common/):

  • LiteralExpression:getValue 直接返回构造时传入的静态字符串,不做任何求值——它代表模板里"不是表达式的那部分原文"。
  • CompositeStringExpression:持有一组 Expression,getValue 把每段 getValue 后拼接成最终字符串。这就是 "#{a}-#{b}" 最终被拼成 "值a-值b" 的地方。

TemplateParserContext(同包)是个预设模板上下文:前缀 "$"、后缀 "{"、isTemplate()=true——给想用 ${...} 风格模板的用户直接用。而容器里用的是 StandardBeanExpressionResolver 内部那个匿名 ParserContext(前缀 "#{"、后缀 "}")。两种前缀风格由 ParserContext 解耦,同一套 TemplateAwareExpressionParser 通吃。

13.15.5 OperatorPower 与 BigInteger 防护

a^b 对应 OperatorPower。它对 Number 做 Math.pow(double 情形),对 BigInteger 走 BigInteger.pow——而 SpelParserConfiguration.maximumBigPowerBits(默认 100 万位)在这里兜底:结果位数超限直接抛错,防止 T(java.math.BigInteger).valueOf(2)^999999 这种表达式把堆打爆。这是 SpEL 把"幂运算"这个数学上无界的操作收进安全边界的典型设计。

13.15.6 InlineList / InlineMap 求值

{1,2,3} 在语法层已把三个元素节点排进 children,求值时 InlineList.getValueInternal 遍历孩子、逐个 getValueInternal 收集成 java.util.ArrayList;InlineMap 按"key 孩子、value 孩子"成对收集成 LinkedHashMap(保持插入序)。它们都是叶子容器节点,不涉及 accessor——#list[0] 取到的就是这种 ArrayList。

13.16 源码剖析(补):关系运算节点与运算符族的求值细节

13.16.1 Token:词法层的最小产物

词法层 process() 返回的 List<Token>,每个 Token(spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java)持有四样东西:TokenKind kind、char[] data(原始文本切片,符号 token 为 null)、startPos、endPos。它的 stringValue() 对符号 token 返回空串、对载荷 token 返回 new String(data);isNumericRelationalOperator() 判断是否属于 GT/LT/LE/GE/EQ/NE 这一组,被 maybeEatRelationalOperator 使用;asInstanceOfToken() 等方法把普通标识符 token"变身"成关键字 token kind。语法器的所有报错位置定位(SpelParseException/SpelEvaluationException 带的 startPos)都来自 token 里记录的 pos——这就是 SpEL 报错能精确到字符列的原因。

13.16.2 OpMultiply / OpDivide / OpModulus:数值放大与取模

与 OpPlus 同源,这三个节点都继承 Operator,getValueInternal 先判断两侧是否都是 Number:是则按与 OpPlus 相同的提升档位(BigDecimal→Double→Float→BigInteger→Long→Integer)做对应运算;不是数值则 state.operate(Operation.MULTIPLY/DIVIDE/MODULO, ...) 交给 OperatorOverloader。一个 SpEL 特有的语义在 OpMultiply:当一侧是 String、另一侧是 Number 时,它做字符串重复——'ab' * 3 得到 'ababab'。这不是标准 Java 语义,是 SpEL 独有的语法糖,实现就在 OpMultiply.getValueInternal 里数值分支之后的字符串分支。

13.16.3 OpEQ / OpNE / OpGT 等:比较统一走 TypeComparator

六个数值关系节点(OpEQ/OpNE/OpGT/OpGE/OpLT/OpLE)结构高度一致:先求左右操作数,再用 state.getTypeComparator()(即 StandardTypeComparator.INSTANCE)比较。StandardTypeComparator(spel/support/StandardTypeComparator.java)能处理 Comparable、数字跨类型比较、字符串字典序、null(null 小于非 null)等;比较结果包成 BooleanTypedValue。这就是为什么 SpEL 里 1 < 2L、"a" < "b"、null < 1 都能直接比较。

13.16.4 OperatorBetween / Instanceof / Matches

  • OperatorBetween:a between (b,c),求值为 a>=b && a<=c。注意语法上 between 后面跟的是个括号表达式(InlineList 或括号内表达式),节点内部取它的两个元素做区间判断。
  • OperatorInstanceof:a instanceof T(b),typeName 经 TypeLocator 解析后 clazz.isInstance(a)。与 Java instanceof 语义一致,但类型名是 SpEL 的 T() 风格。
  • OperatorMatches:a matches 'regex',用 parser 级共享缓存 patternCache(ConcurrentLruCache,容量 OperatorMatches.MAX_PATTERN_CACHE_SIZE)编译正则——同一条表达式重复求值不会反复 Pattern.compile。

13.16.5 OpMinus 与 OpInc / OpDec

  • OpMinus:二元是减法(与 OpPlus 同档位提升),一元是取负(单操作数时对 Number 取负)。
  • OpInc/OpDec:自增自减。它们是少数会触发写回的节点——a++ 等价于读 a、加一、再写回 a。写回要求 context.isAssignmentEnabled()(默认 true,SimpleEvaluationContext.Builder.withAssignmentDisabled() 可关)。解析层用第三个布尔参数区分前缀 ++a(先加后用)与后缀 a++(先用后加)。

13.16.6 一个具体例子:1 + 2 * 3 的 AST 与求值

把前面所有机制落到一个最小例子。表达式 1 + 2 * 3:

词法层产出 token:LITERAL_INT(1)、PLUS、LITERAL_INT(2)、STAR、LITERAL_INT(3)。

语法层:

  • eatExpression → eatLogicalOr → ... → eatSumExpression:先 eatProductExpression 吃掉 1(product 里 power/unary/primary 层层落到 maybeEatLiteral 造出 IntLiteral(1));
  • 回到 eatSumExpression 循环,peek 到 PLUS,take,再 eatProductExpression:product 层先 eatPower... 吃 2,peek 到 STAR,take,再 eatPower... 吃 3,构造 OpMultiply(2,3) 返回;
  • eatSumExpression 构造 OpPlus(IntLiteral(1), OpMultiply(IntLiteral(2), IntLiteral(3)))。

求值:OpPlus.getValueInternal 先求左 1(IntLiteral 直接返回 TypedValue(1)),再求右 OpMultiply(内部 2*3=6);两侧都是 Number、都走 Integer 档,exitTypeDescriptor="I",返回 7。整个过程没有任何反射、没有 context accessor——纯字面量节点的数学求值,trackOperation 计数 +3。

这个例子展示了优先级链如何纯粹靠"嵌套调用深度"表达:2*3 先结合,是因为 OpMultiply 在 OpPlus 的右孩子位置、由更内层的 eatProductExpression 构造。

13.17 源码剖析(补):容器内的特殊 accessor 族与性能基准

13.17.1 StandardBeanExpressionResolver 装配的四个 accessor

回到 13.9.3,StandardBeanExpressionResolver.evaluate 在构造 StandardEvaluationContext 时往 accessor 列表里塞了四个特殊成员,它们都在 spring-context 模块的 org.springframework.context.expression 包下:

  • BeanExpressionContextAccessor:rootObject 本身就是 BeanExpressionContext(beanFactory+scope 的包装)。这个 accessor 让 #{environment}、#{systemProperties}、#{systemEnvironment}、#{beanName} 这些预定义名字直接当属性读——它识别 BeanExpressionContext 上的特殊属性名,映射到 beanFactory/Environment。
  • BeanFactoryAccessor:让表达式里能 @beanFactory 直接拿到 beanFactory 本身。
  • org.springframework.expression.spel.support.MapAccessor:通用 Map 访问,让 rootObject 是 Map 时 #['key'] 与 .key 等价。
  • EnvironmentAccessor:把 Environment 的属性源当属性读(#{environment['spring.profiles.active']})。

加上 BeanFactoryResolver(让 @bean 走容器)与包装 ConversionService 的 StandardTypeConverter,容器内 #{...} 才具备了"能引用 bean、能读环境、能复用容器类型转换"的完整能力。这些 accessor 都排在默认 ReflectivePropertyAccessor 之前,意味着表达式优先匹配容器语义、再落到普通 JavaBean 反射。

13.17.2 性能基准的三个层次

SpEL 的性能不能笼统说"快"或"慢",要分场景:

  1. 纯字面量/算术表达式(如 1+2*3):开销主要在解析。一旦 Expression 对象复用,求值就是 AST 递归 + 少量数学运算,纳秒级。容器里 expressionCache 让解析成本几乎归零。
  2. 反射属性/方法访问:第一次求值要遍历 accessor、canRead 查缓存、createOptimalAccessor 生成专用 accessor;之后每次就是一次 Method.invoke/Field.get,配合 JIT 内联后与直接反射调用相当。OptimalPropertyAccessor 与 CompiledExpression 是性能台阶。
  3. 编译模式:ASM 生成的字节码直接 INVOKEVIRTUAL,绕过整个 AST 递归与 accessor 列表,比解释快数倍——但前提是类型稳定。

基准测试时的常见误区:把 parseExpression 算进单次求值成本(应分开计时);用类型漂移的根对象跑编译模式(触发 MIXED 回退反而更慢);以及在 JIT 预热前就下结论(反射访问要跑很多次 JIT 才内联)。

13.17.3 7.0.9 相对旧版的 SpEL 新行为汇总

本章写作时对照的 7.0.9 源码,相对网上常见的 5.x/6.x 资料,有几处需要特别标注:

  1. EvaluationContext.isCompilationSupported()(7.0.9 新增,default true):上下文侧可一票否决编译;SimpleEvaluationContext.Builder.withCompilationSupported(...) 暴露该开关。与 SpelParserConfiguration 的编译器模式正交。
  2. ?. 与 ?.[...]、?:、?.?[...] 的 Optional 解包(7.0):null-safe 导航不再只对 null 短路,还会对 present 的 Optional 自动 get() 解包;但 Optional 分支暂不支持编译(exitTypeDescriptor=null)。
  3. PropertyOrFieldReference 的 findGetterForProperty 第三档裸名访问器:Java record 组件、Kotlin data class 的 name() 形式属性可直接访问。
  4. AccessorUtils.getAccessorsToTry 按类型亲和度重排(6.2 引入):精确类型 accessor 优先于通用 accessor。
  5. 防护参数:maximumOperations(1 万次)、maximumExpressionLength(1 万字符)、maximumBigPowerBits(100 万位)、OpPlus 拼接上限(10 万字符)——都是防 DoS 的边界。

13.17.4 与其他 Spring 模块表达式的边界

最后再划一次边界,避免混淆:

  • Spring AOP 切点表达式(execution/args/this/target/within/@annotation):AspectJ 语言,spring-aop 的 PointcutParser,不是 SpEL。
  • Spring Security @PreAuthorize/@PostFilter、Spring Data @Query、缓存注解 @Cacheable(key="#..."):是 SpEL,各自自建 EvaluationContext(暴露方法参数 #p0/#a0/#root.methodName 等),但引擎仍是本章的 SpelExpressionParser。
  • ${...} 占位符:PropertySourcesPlaceholderConfigurer,不是 SpEL。

三者共用"表达式"这个词,但解析引擎、求值时机、安全模型完全不同。本章讲的 SpEL 引擎,只覆盖后两类真正跑在 spring-expression 上的场景。

13.18 源码剖析(补):类型推导、可写性与结果类型转换

13.18.1 getValue 家族的类型转换闭环

Expression 接口里 getValue(Object, Class<T>) 这一组重载,承诺"求值并把结果转成期望类型"。在 SpelExpression 里,这个转换发生在 getValueInternal(SpelExpression.java 第 262-312 行):

private @Nullable Object getValueInternal(@Nullable Object rootObject, @Nullable TypeDescriptor expectedResultType)
		throws EvaluationException {
	EvaluationContext context = getEvaluationContext();
	context.setRootObject(rootObject);
	CompiledExpression compiledAst = this.compiledAst;
	if (compiledAst != null && context.isCompilationSupported()) {
		...
		Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
		if (expectedResultType != null) {
			return context.getTypeConverter().convertValue(
					result, TypeDescriptor.forObject(result), expectedResultType);
		}
		return result;
	}
	...
	TypedValue typedResultValue = this.ast.getTypedValue(expressionState);
	if (expectedResultType != null) {
		// The expected result type is not explicitly set on the typed result value
		// -> perform the conversion here.
		return expressionState.convertValue(typedResultValue, expectedResultType);
	}
	return typedResultValue.getValue();
}

关键在最后几行:编译路径与解释路径都做同一件事——若调用方传了 expectedResultType(如 getValue(Integer.class)),就拿 context.getTypeConverter() 把结果从"实际类型"转成"期望类型"。这个 typeConverter 在容器场景下就是包装了 ConversionService 的 StandardTypeConverter,于是 @Value("#{someStr}") 目标是 Integer 时,字符串到数字的转换走的是容器的 ConversionService,而不是 SpEL 自己的一套。

ExpressionState.convertValue(ExpressionState.java)再包一层 trackOperation()——类型转换也算一次操作,计入 maximumOperations 上限。

13.18.2 getValueType 与 isWritable:不实际取值的探测

getValueType() 与 isWritable() 同样在 SpelExpression 里实现。它们不真正求值,而是问 AST:ast.getValueRef(state) 返回一个 ValueRef,ValueRef 自带 getTypeDescriptor()(读反射拿到 getter 返回类型/字段类型)与 isWritable()(是否有对应 setter/可写索引)。CompoundExpression.getValueRef(13.14.1 那段)最后 nextNode.getValueRef(state) 就是把写回/类型探测的责任交给最右孩子——a.b.c 的可写性取决于 c 这个属性有没有 setter,而不是 a 或 b。

setValue(rootObject, newValue) 走同一条 getValueRef(state).setValue(newValue):PropertyOrFieldReference 慢路径里若 canWrite 命中 writerCache 里的 setter,就反射写回;Indexer 则写回数组/List/Map 元素。整条"读-改-写"链路(如 a++)就是 getValueRef + getValue + 计算 + setValue 的组合。

13.18.3 TypeCode:基本类型字面量的映射

T(int) 这种基本类型字面量,除了 TypeCode 枚举映射,还有数组维度处理(TypeReference.makeArrayIfNecessary)。TypeCode 把 void/int/long/.../boolean 这些短名映射到对应的 Class<?>(包括 int.class 这种基本类型字面量),TypeReference 求值后 exitTypeDescriptor="Ljava/lang/Class",返回的是基本类型的 Class 对象而非包装类——所以 T(int)==int.class 为真,T(Integer)==Integer.class 也为真,两者不相等。这是 SpEL 与 Java 反射里基本类型/包装类区分的完整对齐。

13.18.4 DataBindingPropertyAccessor:SimpleEvaluationContext 的只读 accessor

SimpleEvaluationContext 默认不装 ReflectivePropertyAccessor(它能读 public 字段、能调 setter,能力太宽),而是装 DataBindingPropertyAccessor(spel/support/DataBindingPropertyAccessor.java)。它只认 JavaBean getter(getX/isX),不访问 public 字段、不调任意方法——这是"数据绑定"语义:表达式只能读 bean 属性,不能借 T() 或字段访问搞侧信道。配合 typeNotFoundTypeLocator 抛异常(无 T()/new),SimpleEvaluationContext 才在数据绑定场景下做到"只能读你允许的属性"。

13.18.5 小结:本章流水线的完整闭环

至此,本章从词法到编译、从解释到容器集成、从安全到性能的全部环节都已落到 7.0.9 真实源码:

  • 字符串 → Tokenizer → List<Token> → InternalSpelExpressionParser → SpelNodeImpl AST → SpelExpression;
  • getValue → ExpressionState → 递归 getValueInternal → EvaluationContext 的 accessor/resolver 策略链 → 缓存 accessor/executor;
  • 解释累计到阈值 → SpelCompiler ASM 生成 CompiledExpression 子类 → 后续求值绕开 AST;
  • 容器里 #{...} 经 StandardBeanExpressionResolver(spring-context)装配容器语义 accessor,两层缓存零重复解析;
  • StandardEvaluationContext 全开但禁不可信输入,SimpleEvaluationContext 做受限数据绑定子集。

理解这条流水线后,任何一条 SpEL 表达式的性能、报错位置、安全边界都能定位到具体类与方法。

13.19 源码剖析(补):错误定位、ValueRef 写回与线程安全细节

13.19.1 错误消息的源码位置定位

SpEL 报错(SpelParseException/SpelEvaluationException)都带 position 字段——字符偏移。这个数字从哪来?词法层 Token 记录了每个 token 的 startPos/endPos;语法层构造节点时把 token 位置透传给节点(new OpPlus(t.startPos, t.endPos, ...));求值层异常时,CompoundExpression.getValueRef 的 catch 里 ex.setPosition(nextNode.getStartPosition())、OpAnd.getBooleanValue 的 catch 里 ex.setPosition(operand.getStartPosition())——这些"修正位置"代码就是为了把底层异常(如 IllegalStateException)重新定位到表达式树里出错的那一段。所以用户看到的 EL1004E:(3,5): Method calls cannot be performed on this type 里 (3,5) 这种列号,是节点位置一路透传出来的。

13.19.2 ValueRef:读与写的统一抽象

ExpressionState 与各节点返回的 ValueRef 接口(spel/ValueRef.java)有三个方法:getValue()、setValue()、getTypeDescriptor()、isWritable()。它把"一个可被读/写的值位置"抽象出来——无论是变量槽、bean 属性、Map 条目,都用同一个接口。Indexer 写回数组元素、PropertyOrFieldReference 写回 setter、VariableReference 写回 context.variables,最终都落到某个 ValueRef 实现上。SpEL 的 a = b(赋值节点 Assign)本质就是 left.getValueRef(state).setValue(b.getValue(state))。

13.19.3 线程安全:parser 复用、Expression 缓存的代价

把线程模型做个收束:

  • SpelExpressionParser:线程安全(类注释明示),全局单例;它每次解析 new 一个 InternalSpelExpressionParser(非线程安全但短生命)。
  • Expression(SpelExpression):可跨线程共享——compiledAst 是 volatile、缓存 accessor 是 volatile、interpretedCount/failedAttempts 是 AtomicInteger、compileExpression 有双重检查锁。这些并发原语就是 13.8 看到的那些字段。
  • EvaluationContext:不是线程安全共享的。StandardEvaluationContext 的 variables 是 ConcurrentHashMap(多线程同时 put 安全),但 accessor/resolver 列表、rootObject 都假定单线程一次求值。容器里 evaluationCache 缓存的 StandardEvaluationContext 对每个 BeanExpressionContext 一份,而 BeanExpressionContext 绑定单例 beanFactory——容器自身保证 bean 创建的串行化,所以那个 context 在并发下实际不会被多个线程同时写。
  • 红线:别把同一个 Expression 先喂 StandardEvaluationContext 再喂 SimpleEvaluationContext——accessor 缓存跨安全语义泄露。

13.19.4 收尾:为什么 SpEL 值得读

SpEL 是 Spring 框架里少有的"从零手写、自成体系"的子系统:手写词法、手写递归下降、手写 ASM 字节码生成、手写反射缓存。它没有借助 ANTLR、没有依赖 Javassist/cglib,全部依赖都是 JDK 反射 + spring-core 的 ConversionService/ReflectionUtils。读懂它,等于读了一遍"一个微型表达式语言引擎"该怎么从字符流做到 JIT 编译,又能作为 Spring 生态的 @Value 基础设施。这就是本章把 Tokenizer→Parser→AST→求值→编译→集成 每一段都落到具体类的原因。