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/:
| 接口 | 模块相对路径 | 一句话职责 |
|---|---|---|
ExpressionParser | spring-expression/src/main/java/org/springframework/expression/ExpressionParser.java | 把字符串解析成可重复求值的 Expression |
Expression | spring-expression/src/main/java/org/springframework/expression/Expression.java | 一次解析产物,封装 getValue/setValue/getValueType/isWritable 家族方法 |
EvaluationContext | spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java | 求值上下文:rootObject、一组 accessor/resolver、typeLocator、变量表 |
SpelNode | spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java | AST 节点接口,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)方法极多,但可以归为三组:
- 求值:
getValue()、getValue(Class<T>)、getValue(Object rootObject)、getValue(EvaluationContext)等八个重载,区别只在于"用哪个上下文、用哪个 rootObject、要不要做期望类型转换"。 - 类型与可写性:
getValueType()、getValueTypeDescriptor()、isWritable()——回答"这条表达式最终能写成什么类型""能不能被 setValue"。 - 写回:
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 行)是最长的词法子程序。分支顺序:
- 十六进制:
firstCharIsZero && 下一字符是 x/X,则连续吞十六进制数字,按后面有没有L/l决定产出LITERAL_HEXINT还是LITERAL_HEXLONG。 - 十进制整数部分:
do { pos++ } while (isDigit(...))。 - 小数点:若下一字符是
.,标记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、把.留给主循环作DOTtoken,这样3.toString()才能被解析成"整数字面量访问 toString 属性"。这是 SpEL 支持"基本类型字面量上直接调方法"(1.toString()、3.5.intValue())的词法基础。 - 后缀:
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 行)。流程:
- 取出目标类所有方法,按名字过滤掉不匹配的;
- 若该类型注册了
MethodFilter(StandardEvaluationContext.registerMethodFilter),先过滤; - 排序(第 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); }); - 桥接方法消解:
BridgeMethodResolver.findBridgedMethod把泛型桥接方法还原成真实方法,再用LinkedHashSet去重; - 逐个比对实参类型与形参类型,得到
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;
}
逐段解读:
- 编译快路径:
compiledAst != null且context.isCompilationSupported()(7.0.9 新门控)时,直接compiledAst.getValue(rootObject, context)。注意这里直接调编译产物、不再走ast.getValueInternal——已编译表达式完全绕开解释器。 - 编译产物抛异常的两种处理:MIXED 模式下回退到解释(
compiledAst=null、计数清零,下次再重新编译);IMMEDIATE 模式下把异常包成SpelEvaluationException抛给调用者。这正是SpelCompilerMode注释里说的"IMMEDIATE 下类型变了就直接报错"。 - 解释路径:new 一个
ExpressionState,递归ast.getValue(expressionState)。 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);
}
}
逐段:
- 两层缓存:
expressionCache(ConcurrentHashMap(256))把"值字符串 →Expression"缓存,避免重复 parse;evaluationCache(ConcurrentHashMap(8))把"BeanExpressionContext→StandardEvaluationContext"缓存。beanFactory 是单例,这两个缓存让#{...}在容器里几乎零重复解析成本。 - 模板上下文
beanExpressionParserContext(第 88-101 行):isTemplate()=true、前缀"#{"、后缀"}"。这就是TemplateAwareExpressionParser分流的来源——parseExpression(expr, context)走parseTemplate分支。 - 上下文装配: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 才编译
▼
返回最终值
这条时间线里有几个设计要点值得点明:
- 词法与语法严格分离。
Tokenizer只认字符、不理解语法;InternalSpelExpressionParser只认 token 流。两者通过List<Token>解耦,词法错误(如未闭合字符串)在process()就抛,语法错误(如MORE_INPUT)在eatExpression后抛。 - AST 不可变、状态可缓存。
SpelExpression的ast是 final 的,但节点内部的cachedReadAccessor/cachedExecutor/exitTypeDescriptor是可变的 volatile 字段——多次求值时节点"边跑边学",把第一次解析出的反射成员缓存下来。 - 解释与编译的平滑切换。
compiledAst一旦就绪,后续getValue完全绕开ast;编译产物运行失败(MIXED)又能一键回退解释。这个开关在getValue每次调用处都是一个 volatile 读,无锁。 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做数据绑定子集。
本章关键类清单
| 类名 | 模块与相对路径 | 职责一句话 |
|---|---|---|
SpelExpressionParser | spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java | 线程安全的解析器外壳,转发到内部 parser |
InternalSpelExpressionParser | spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java | 手写递归下降语法分析,优先级链与 AST 构造 |
Tokenizer | spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java | 词法扫描,字符流 → Token 流 |
TokenKind | spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java | 57 种 token 枚举,预存符号与载荷标记 |
SpelExpression | spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java | Expression 实现,持有 AST/编译产物/计数阈值 |
SpelNodeImpl | spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java | AST 节点抽象基类,getValueInternal + generateCode |
ExpressionState | spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java | 单次求值临时状态,活动对象栈/作用域栈/操作计数 |
OpPlus | spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java | 加法与字符串拼接,数值提升顺序 |
PropertyOrFieldReference | spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java | 属性/字段读取,accessor 缓存与 null-safe |
MethodReference | spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java | 方法调用,executor 缓存与重载执行 |
Indexer | spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java | 数组/List/Map/String/对象统一下标 |
TypeReference | spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java | T() 类型字面量,委托 TypeLocator |
BeanReference | spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java | @bean 引用,委托 BeanResolver |
Ternary / Elvis | spring-expression/src/main/java/org/springframework/expression/spel/ast/ | 三元短路 / elvis 默认值 |
StandardEvaluationContext | spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java | 功能全开的求值上下文,默认反射三件套 |
SimpleEvaluationContext | spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java | 受限子集上下文,无 T()/new/@bean |
ReflectivePropertyAccessor | spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java | 反射属性访问,四张缓存表 + OptimalPropertyAccessor |
ReflectiveMethodResolver | spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java | 方法候选加权排序与桥接方法消解 |
StandardTypeLocator | spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java | 类型定位,默认仅注册 java.lang |
SpelCompiler | spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java | ASM 生成 CompiledExpression 子类字节码 |
TemplateAwareExpressionParser | spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java | 模板模式 #{...} 切分与括号配对 |
StandardBeanExpressionResolver | spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java | 容器内 #{...} 求值,两层缓存 + 上下文装配 |
AbstractBeanFactory | spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java | evaluateBeanDefinitionString 统一入口 |
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();
}
}
...
}
三层细节:
- 每个元素压栈:把当前
Map.Entry包成TypedValue压为活动上下文对象,于是选择条件里的#this就是这个 entry;enterScope()同时压入scopeRootObjects,使条件里的无限定引用也指向当前元素。 - 条件必须返回布尔:不是
Boolean直接抛RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN——这是写 SpEL 选择条件最常踩的坑之一(条件写成#this.age忘了比较)。 - 三种 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+2*3):开销主要在解析。一旦Expression对象复用,求值就是 AST 递归 + 少量数学运算,纳秒级。容器里expressionCache让解析成本几乎归零。 - 反射属性/方法访问:第一次求值要遍历 accessor、
canRead查缓存、createOptimalAccessor生成专用 accessor;之后每次就是一次Method.invoke/Field.get,配合 JIT 内联后与直接反射调用相当。OptimalPropertyAccessor与CompiledExpression是性能台阶。 - 编译模式:ASM 生成的字节码直接
INVOKEVIRTUAL,绕过整个 AST 递归与 accessor 列表,比解释快数倍——但前提是类型稳定。
基准测试时的常见误区:把 parseExpression 算进单次求值成本(应分开计时);用类型漂移的根对象跑编译模式(触发 MIXED 回退反而更慢);以及在 JIT 预热前就下结论(反射访问要跑很多次 JIT 才内联)。
13.17.3 7.0.9 相对旧版的 SpEL 新行为汇总
本章写作时对照的 7.0.9 源码,相对网上常见的 5.x/6.x 资料,有几处需要特别标注:
EvaluationContext.isCompilationSupported()(7.0.9 新增,default true):上下文侧可一票否决编译;SimpleEvaluationContext.Builder.withCompilationSupported(...)暴露该开关。与SpelParserConfiguration的编译器模式正交。?.与?.[...]、?:、?.?[...]的 Optional 解包(7.0):null-safe 导航不再只对 null 短路,还会对 present 的Optional自动get()解包;但 Optional 分支暂不支持编译(exitTypeDescriptor=null)。PropertyOrFieldReference的findGetterForProperty第三档裸名访问器:Java record 组件、Kotlin data class 的name()形式属性可直接访问。AccessorUtils.getAccessorsToTry按类型亲和度重排(6.2 引入):精确类型 accessor 优先于通用 accessor。- 防护参数:
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→SpelNodeImplAST →SpelExpression; getValue→ExpressionState→ 递归getValueInternal→EvaluationContext的 accessor/resolver 策略链 → 缓存 accessor/executor;- 解释累计到阈值 →
SpelCompilerASM 生成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→求值→编译→集成 每一段都落到具体类的原因。