SpEL

写 Spring 的时候,经常会看到一些看起来像小型脚本的写法,比如 #{...}@Value("#{systemProperties['user.home']}")@PreAuthorize("hasRole('ADMIN')")。这些东西看起来不像普通 Java 代码,但又确实在项目里经常出现。

这些表达式背后用到的,就是 SpEL。它的全称是 Spring Expression Language,也就是 Spring 表达式语言。简单说,它是 Spring 提供的一套表达式机制,用来在运行时读取数据、调用方法、做条件判断。

为什么会有 SpEL

如果一个框架只支持写死配置,那很多动态场景都会很难处理。

比如下面这些需求:

  • 从配置、Bean 或上下文里动态取值
  • 根据条件决定某个 Bean 是否生效
  • 在方法执行前做权限判断
  • 在缓存、消息、调度等场景里写一些简单规则

这些场景如果全部都写成 Java 代码,当然不是不行,但很多时候会显得比较重。SpEL 的作用,就是给 Spring 提供一套统一的表达式能力,让这些动态逻辑可以用更轻量的方式写出来。

SpEL 是什么

按照 Spring Framework 官方文档的说法,SpEL 是一套支持在运行时查询和操作对象图的表达式语言。

如果把这句话说得直接一点,可以把它理解成 Spring 里的“表达式语法层”。它允许你在不写完整 Java 逻辑的情况下,完成下面这些事:

  • 访问对象属性
  • 调用对象方法
  • 使用逻辑运算符和条件表达式
  • 访问集合、Map、数组
  • 引用 Bean
  • 做集合筛选和投影

也就是说,SpEL 并不是一门独立语言,它更像 Spring 提供的一套运行时表达式能力。

SpEL 能做什么

读取属性和调用方法

这是最基础的用法。比如可以直接访问对象属性、Map 值,或者调用对象方法:

1
2
ExpressionParser parser = new SpelExpressionParser();
String value = parser.parseExpression("'hello'.toUpperCase()").getValue(String.class);

这一类表达式适合做一些简单值计算,不适合写太复杂的业务逻辑。

条件判断

SpEL 支持常见的逻辑判断和三元表达式,所以可以很方便地写条件选择:

1
2
@Value("#{systemProperties['user.region'] == 'CN' ? 'zh_CN' : 'en_US'}")
private String locale;

这种写法在配置或者注解参数里比较常见。

集合筛选和投影

这是 SpEL 比较有特点的地方。它支持对集合做筛选和投影。

比如:

  • 选择满足条件的元素
  • 从对象集合里取某个字段组成新集合

官方文档里把这部分称为 collection selection 和 collection projection。这个能力很方便,但表达式一长,可读性会下降得很快。

在 Spring 里常见的使用场景

SpEL 真正常见,不是在单独写 ExpressionParser 的时候,而是在各种 Spring 注解和配置场景里。

@Value

这是很多人最早接触 SpEL 的地方。除了读配置项,也可以直接写表达式:

1
2
@Value("#{8 * 60}")
private Integer timeout;

或者从系统属性、Bean 属性里取值:

1
2
@Value("#{systemProperties['java.version']}")
private String javaVersion;

权限表达式

在 Spring Security 里,很多权限注解背后也和表达式机制有关。例如:

1
2
3
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}

这类写法在项目里非常常见。虽然平时大家更习惯把它当成权限语法来看,但本质上它也是表达式求值的一种体现。

条件装配

Spring 里有些场景也会用表达式来控制 Bean 是否生效,比如 @ConditionalOnExpression

1
@ConditionalOnExpression("'${app.cache.type}' == 'redis'")

这种方式适合写简单条件,但如果条件本身已经比较复杂,就不太建议继续堆表达式了。

使用 SpEL 时要注意什么

SpEL 很方便,但也不是越多越好。

表达式太长会影响可读性

短表达式读起来很顺,长表达式就容易变成“配置里写业务逻辑”。一旦出现多层判断、嵌套调用、复杂集合操作,后面维护起来会比较痛苦。

适合写轻逻辑,不适合替代业务代码

SpEL 更适合做动态取值、简单条件、权限判断这类事情。如果已经开始写复杂流程判断,那通常应该回到 Java 代码本身。

不要把不可信输入直接当表达式执行

SpEL 本身有很强的表达能力,所以如果把外部输入直接交给表达式引擎处理,会带来明显的安全风险。正常项目里,一般不会让用户自己拼接和执行表达式。

小结

SpEL 可以理解成 Spring 里的表达式工具层。它的价值不在于替代 Java,而在于把一些运行时的小型动态逻辑写得更轻。

@Value、权限表达式、条件装配这些场景,用 SpEL 会很自然;但如果表达式已经开始承载太多业务逻辑,那通常就说明这部分代码应该换个地方写了。

参考资料