Spring EL表达式 抽象到极致的艺术品

深入解析Spring EL表达式

Spring表达式语言(SpEL)是Spring框架提供的一种功能强大且灵活的表达式语言,支持运行时查询和操作对象图,类似于JSP的EL和OGNL,但功能更为丰富。SpEL广泛应用于Spring生态系统(如IoC容器、Spring Security等),支持属性访问、方法调用、算术逻辑运算、集合操作等特性,并能通过配置文件或注解灵活配置,为Spring应用开发提供了高效的数据处理和动态配置能力。

一、Literal Expressions(文字表达式)

基本介绍

文字表达式是SpEL中最简单的表达式,用于表示常量值,包括字符串、数字、布尔值和null。它们是SpEL的基础构建块,通常与其他复杂表达式结合使用。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello, SpEL!'");
String result = (String) exp.getValue();
System.out.println(result); // 输出: Hello, SpEL!

exp = parser.parseExpression("42");
Integer number = (Integer) exp.getValue();
System.out.println(number); // 输出: 42

exp = parser.parseExpression("true");
Boolean bool = (Boolean) exp.getValue();
System.out.println(bool); // 输出: true

注意事项

  1. 字符串需加引号:字符串必须用单引号或双引号括起来,否则会被解析为变量或属性名。
  2. 类型推断 :SpEL会根据字面值自动推断类型(如42Integer3.14Double)。
  3. 转义字符 :字符串中支持标准的Java转义字符,如\n\t等。

鲜为人知的点

  • SpEL支持Unicode转义字符(如\u0041表示字符A),这在国际化场景中非常有用。
  • 布尔值的字面量truefalse是大小写敏感的,写成TRUE会导致解析错误。

二、Accessing Properties, Arrays, Lists, and Maps(访问属性、数组、列表和映射)

基本介绍

SpEL支持通过点号(.)或方括号([])访问对象的属性、数组、列表和映射。这种灵活的访问方式是SpEL的核心功能之一。

代码示例

java 复制代码
public class User {
    private String name = "Alice";
    private List<String> hobbies = Arrays.asList("reading", "gaming");
    private Map<String, Integer> scores = new HashMap<>();
    public String getName() { return name; }
    public List<String> getHobbies() { return hobbies; }
    public Map<String, Integer> getScores() { return scores; }
}

User user = new User();
user.getScores().put("math", 90);

EvaluationContext context = new StandardEvaluationContext(user);
ExpressionParser parser = new SpelExpressionParser();

// 访问属性
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(context);
System.out.println(name); // 输出: Alice

// 访问列表
exp = parser.parseExpression("hobbies[0]");
String hobby = (String) exp.getValue(context);
System.out.println(hobby); // 输出: reading

// 访问映射
exp = parser.parseExpression("scores['math']");
Integer score = (Integer) exp.getValue(context);
System.out.println(score); // 输出: 90

注意事项

  1. 属性访问优先级:SpEL会优先尝试调用getter方法,若无getter,则直接访问字段(需为public)。
  2. 空指针安全 :访问不存在的属性或索引会导致异常,需配合安全导航运算符(?.)使用。
  3. 大小写敏感:属性名和键名严格区分大小写。

鲜为人知的点

  • SpEL支持嵌套属性访问(如user.address.city),但嵌套层次过深可能影响性能。
  • 对于数组或列表,SpEL支持负索引(如list[-1]表示最后一个元素),但需确保SpEL实现支持此特性(Spring 5.3+)。

三、Inline Lists(内联列表)

基本介绍

内联列表允许在表达式中直接定义一个列表,语法为{元素1, 元素2, ...},常用于初始化或临时数据处理。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("{'apple', 'banana', 'orange'}");
List<String> fruits = (List<String>) exp.getValue();
System.out.println(fruits); // 输出: [apple, banana, orange]

注意事项

  1. 类型推断:内联列表的元素类型由SpEL推断,需确保元素类型一致以避免运行时异常。
  2. 空列表 :空列表用{}表示,但不能直接赋值给需要具体类型的变量。

鲜为人知的点

  • 内联列表是只读的(java.util.Collections.unmodifiableList),尝试修改会抛出UnsupportedOperationException
  • 内联列表在SpEL中非常轻量,适合小规模数据,但不建议用于复杂数据结构。

四、Inline Maps(内联映射)

基本介绍

内联映射用于在表达式中定义键值对,语法为{key1:value1, key2:value2},适用于临时构造映射数据。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("{name:'Alice', age:25}");
Map<String, Object> map = (Map<String, Object>) exp.getValue();
System.out.println(map); // 输出: {name=Alice, age=25}

注意事项

  1. 键类型:键通常为字符串,但也可以是其他类型(如数字)。
  2. 值类型:值的类型由SpEL推断,需注意类型一致性。

鲜为人知的点

  • 内联映射同样是只读的,尝试修改会抛出异常。
  • 在复杂场景下,内联映射的性能可能不如Java代码直接构造映射,需权衡使用。

五、Array Construction(数组构造)

基本介绍

SpEL支持通过new关键字构造数组,语法为new 类型[长度],并可通过索引初始化元素。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new int[]{1, 2, 3}");
int[] numbers = (int[]) exp.getValue();
System.out.println(Arrays.toString(numbers)); // 输出: [1, 2, 3]

注意事项

  1. 类型限制 :仅支持基本类型和String类型的数组构造。
  2. 长度固定:构造后的数组长度不可变。

鲜为人知的点

  • 数组构造在SpEL中较为少用,因为内联列表更灵活,且列表可以动态扩展。
  • SpEL不支持多维数组的直接构造,需通过嵌套列表间接实现。

六、Relational Operators(关系运算符)

基本介绍

SpEL支持标准的关系运算符,包括==!=<><=>=,以及关键字形式(如eqnelt等),用于比较值。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("5 > 3");
Boolean result = (Boolean) exp.getValue();
System.out.println(result); // 输出: true

exp = parser.parseExpression("'abc' eq 'abc'");
result = (Boolean) exp.getValue();
System.out.println(result); // 输出: true

注意事项

  1. 类型安全:比较的双方需为兼容类型,否则会抛出异常。
  2. 关键字形式 :关键字(如eq)在XML配置中更友好,因为符号可能需要转义。

鲜为人知的点

  • SpEL的关系运算符支持null比较,null == null返回true
  • 对于字符串比较,SpEL使用equals方法,而非==,这与Java行为一致。

七、Regular Expressions(正则表达式)

基本介绍

SpEL支持正则表达式匹配,通过matches关键字实现,用于验证字符串是否符合指定模式。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'[email protected]' matches '[a-z]+@[a-z]+\\.com'");
Boolean result = (Boolean) exp.getValue();
System.out.println(result); // 输出: true

注意事项

  1. 性能开销:正则表达式匹配可能较慢,需避免在高频场景中滥用。
  2. 异常处理 :无效的正则表达式会导致PatternSyntaxException

鲜为人知的点

  • SpEL的正则表达式基于Java的java.util.regex,支持所有Java正则特性。
  • 正则表达式在SpEL中不支持动态构造,需硬编码或通过变量注入。

八、Logical Operators(逻辑运算符)

基本介绍

SpEL支持逻辑运算符andornot(或&&||!),用于组合布尔表达式。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("true and false");
Boolean result = (Boolean) exp.getValue();
System.out.println(result); // 输出: false

exp = parser.parseExpression("not true");
result = (Boolean) exp.getValue();
System.out.println(result); // 输出: false

注意事项

  1. 短路求值andor支持短路求值,优化性能。
  2. 优先级:逻辑运算符的优先级低于关系运算符,需注意括号的使用。

鲜为人知的点

  • SpEL的逻辑运算符在处理null值时较为宽松,null and true不会抛异常,而是返回false
  • 在复杂表达式中,建议显式使用括号以提高可读性和避免优先级错误。

九、String Operators(字符串运算符)

基本介绍

SpEL支持字符串连接(通过+运算符),以及字符串相关的操作(如toUpperCase等方法调用)。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello' + ' ' + 'World'");
String result = (String) exp.getValue();
System.out.println(result); // 输出: Hello World

注意事项

  1. 性能 :字符串连接在循环中可能导致性能问题,建议使用StringBuilder替代。
  2. 类型转换 :非字符串值会自动调用toString方法。

鲜为人知的点

  • SpEL支持字符串的隐式方法调用,如'hello'.toUpperCase(),无需显式调用String类方法。
  • 字符串连接的结果总是新的String对象,需注意内存分配。

十、Mathematical Operators(数学运算符)

基本介绍

SpEL支持基本的数学运算符,包括+-*/%^(幂运算),适用于数字类型。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("2 + 3 * 4");
Integer result = (Integer) exp.getValue();
System.out.println(result); // 输出: 14

exp = parser.parseExpression("2 ^ 3");
result = (Integer) exp.getValue();
System.out.println(result); // 输出: 8

注意事项

  1. 类型转换 :运算结果的类型由操作数决定(如intdouble运算返回double)。
  2. 除零异常:除法和模运算需避免除以零。

鲜为人知的点

  • SpEL的幂运算(^)不支持负数底数,需通过其他方式处理。
  • 数学运算符的优先级与Java一致,但建议使用括号以提高可读性。

十一、Assignment(赋值)

基本介绍

SpEL支持通过=运算符为变量或属性赋值,通常在动态配置或上下文更新中使用。

代码示例

java 复制代码
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("myVar", 0);

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#myVar = 42");
Integer result = (Integer) exp.getValue(context);
System.out.println(result); // 输出: 42
System.out.println(context.lookupVariable("myVar")); // 输出: 42

注意事项

  1. 作用域 :赋值仅影响当前EvaluationContext,不会修改原始对象。
  2. 类型安全:赋值时需确保类型兼容。

鲜为人知的点

  • 赋值表达式本身会返回赋值后的值,可用于链式操作。
  • SpEL不支持直接修改final字段或不可变对象的属性。

十二、Type Expressions(类型表达式)

基本介绍

SpEL通过T()运算符访问Java类型,用于调用静态方法或构造实例。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("T(java.lang.Math).PI");
Double pi = (Double) exp.getValue();
System.out.println(pi); // 输出: 3.141592653589793

注意事项

  1. 类路径:需提供完整的类路径,SpEL不会自动导入。
  2. 性能 :频繁使用T()可能增加解析开销。

鲜为人知的点

  • T()可以结合new关键字构造实例,如T(java.util.Date).newInstance()
  • SpEL不支持泛型类型的直接解析,需通过具体类处理。

十三、Method Invocation(方法调用)

基本介绍

SpEL支持调用对象的方法,方法名需与JavaBean规范一致。

代码示例

java 复制代码
public class User {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

User user = new User();
EvaluationContext context = new StandardEvaluationContext(user);

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("greet('Alice')");
String result = (String) exp.getValue(context);
System.out.println(result); // 输出: Hello, Alice!

注意事项

  1. 方法重载:SpEL会根据参数类型选择最匹配的方法。
  2. 异常传播:方法抛出的异常会直接传播到调用者。

鲜为人知的点

  • SpEL支持链式方法调用(如obj.method1().method2()),但需确保中间结果非空。
  • 方法调用支持变长参数,但需谨慎处理参数类型。

十四、Constructor Invocation(构造函数调用)

基本介绍

SpEL通过new关键字调用构造函数,支持基本类型和自定义类的实例化。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new java.util.Date()");
Date date = (Date) exp.getValue();
System.out.println(date); // 输出: 当前时间

注意事项

  1. 参数匹配:需提供正确的参数类型和数量。
  2. 异常处理:构造函数抛出的异常需由调用者捕获。

鲜为人知的点

  • SpEL支持通过构造函数初始化复杂对象,但性能可能不如Java代码直接调用。
  • 构造函数调用不支持泛型参数推断,需显式指定类型。

十五、Variables(变量)

基本介绍

SpEL通过#variableName访问EvaluationContext中的变量,支持动态数据绑定。

代码示例

java 复制代码
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("username", "Alice");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#username");
String result = (String) exp.getValue(context);
System.out.println(result); // 输出: Alice

注意事项

  1. 变量未定义 :访问未定义的变量会导致SpelEvaluationException
  2. 作用域:变量仅在当前上下文有效。

鲜为人知的点

  • SpEL支持变量的动态更新,适合在循环或条件语句中处理临时状态。
  • 变量名支持下划线和数字,但建议遵循Java命名规范。

十六、User-defined Functions(用户定义函数)

基本介绍

SpEL允许通过EvaluationContext注册自定义函数,扩展表达式功能。

代码示例

java 复制代码
public class Utils {
    public static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}

EvaluationContext context = new StandardEvaluationContext();
context.registerFunction("reverse", Utils.class.getDeclaredMethod("reverse", String.class));

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#reverse('hello')");
String result = (String) exp.getValue(context);
System.out.println(result); // 输出: olleh

注意事项

  1. 方法签名:自定义函数需为静态方法,且需正确声明异常。
  2. 性能:函数调用会增加反射开销,需权衡使用。

鲜为人知的点

  • SpEL支持函数的链式调用,但需确保函数返回类型匹配。
  • 自定义函数在Spring Integration中常用于复杂消息处理。

十七、Bean References(Bean引用)

基本介绍

SpEL通过@beanName引用Spring容器中的Bean,常用于依赖注入或配置,Spring Security中常用。

代码示例

java 复制代码
ApplicationContext context = new GenericApplicationContext();
ConfigurableListableBeanFactory beanFactory = ((GenericApplicationContext) context).getBeanFactory();
beanFactory.registerSingleton("myBean", new User());

SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext evalContext = new StandardEvaluationContext();
evalContext.setBeanResolver(new BeanFactoryResolver(context));

Expression exp = parser.parseExpression("@myBean.name");
String result = (String) exp.getValue(evalContext);
System.out.println(result); // 输出: Alice

注意事项

  1. Bean解析器 :需配置BeanResolver,否则无法解析@符号。
  2. 性能:Bean引用可能涉及容器查找,需避免频繁调用。

鲜为人知的点

  • SpEL支持嵌套Bean引用(如@bean1.bean2),但需确保中间Bean非空。
  • 在Spring Boot中,Bean引用常用于@Value注解的动态配置。

十八、Ternary, Elvis, and Safe-navigation Operators(三元运算符、Elvis运算符和安全导航运算符)

基本介绍

  • 三元运算符condition ? value1 : value2,根据条件选择值。
  • Elvis运算符expr ?: default,当exprnull时返回默认值。
  • 安全导航运算符?.,避免空指针异常。

代码示例

java 复制代码
ExpressionParser parser = new SpelExpressionParser();

// 三元运算符
Expression exp = parser.parseExpression("true ? 'yes' : 'no'");
String result = (String) exp.getValue();
System.out.println(result); // 输出: yes

// Elvis运算符
exp = parser.parseExpression("null ?: 'default'");
result = (String) exp.getValue();
System.out.println(result); // 输出: default

// 安全导航运算符
EvaluationContext context = new StandardEvaluationContext(new User());
exp = parser.parseExpression("address?.city");
result = (String) exp.getValue(context);
System.out.println(result); // 输出: null

注意事项

  1. 优先级:三元运算符优先级较低,需注意括号使用。
  2. Elvis运算符局限 :仅适用于null检查,不支持其他条件。

鲜为人知的点

  • 安全导航运算符在处理复杂嵌套对象时非常实用,但不支持写操作。
  • Elvis运算符可以与方法调用结合,如obj?.method() ?: default

十九、Collection Projection(集合投影)

基本介绍

集合投影通过![]语法对集合中的每个元素应用表达式,生成新的集合。类似stream.map.collect

代码示例

java 复制代码
List<String> names = Arrays.asList("alice", "bob");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("names", names);

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#names.![toUpperCase()]");
List<String> result = (List<String>) exp.getValue(context);
System.out.println(result); // 输出: [ALICE, BOB]

注意事项

  1. 空集合:投影空集合返回空集合,不会抛异常。
  2. 性能:投影操作涉及迭代,需避免在大数据集上频繁使用。

鲜为人知的点

  • 投影支持嵌套操作,如list.![innerList.![#this]]
  • 投影结果的类型由元素类型决定,需确保类型一致。

二十、Collection Selection(集合选择)

基本介绍

集合选择通过.?[]语法过滤集合元素,仅保留满足条件的元素。类似stream.filter.collect

除了返回所有选定元素外,您还可以仅检索第一个或最后一个元素。

  • 要获取与选择表达式匹配的第一个元素,语法为 .^[selectionExpression]类似stream.findFirst
  • 要获取与选择表达式匹配的最后一个元素,语法为 .$[selectionExpression]

代码示例

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("numbers", numbers);

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#numbers.?[#this > 2]");
List<Integer> result = (List<Integer>) exp.getValue(context);
System.out.println(result); // 输出: [3, 4]

注意事项

  1. 条件表达式:选择条件必须返回布尔值。
  2. 性能:选择操作涉及全量迭代,需注意集合大小。

鲜为人知的点

  • SpEL支持多条件选择,如list.?[#this > 0 and #this < 10]
  • 选择操作可以与投影结合,形成强大的数据处理链。

二十一、Templated Expressions(模板表达式)

基本介绍

模板表达式通过#{}嵌入SpEL代码,结合普通文本生成动态内容,常用于配置或消息格式化。

代码示例

java 复制代码
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "Alice");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("Hello, #name!", new TemplateParserContext());
String result = (String) exp.getValue(context);
System.out.println(result); // 输出: Hello, Alice!

注意事项

  1. 解析器配置 :需使用TemplateParserContext解析模板表达式。
  2. 安全性:模板表达式可能引入注入风险,需验证输入。

鲜为人知的点

  • 模板表达式支持多段SpEL代码,如#{expr1}text#{expr2}
  • 在Spring Boot的@Value注解中,模板表达式常用于复杂属性注入。

总结与实践建议

SpEL作为Spring框架的强大工具,其功能远超简单的属性访问。从文字表达式到集合操作,再到模板表达式,SpEL提供了灵活的动态计算能力,适用于配置、验证、消息处理等多个场景。然而,SpEL的强大也伴随着复杂性,以下是一些实践建议:

  1. 性能优化:避免在高频场景中使用复杂表达式(如正则或投影),必要时缓存解析结果。
  2. 安全性:对用户输入的表达式进行严格验证,防止注入攻击。
  3. 可读性:在复杂表达式中善用括号和注释,提高代码可维护性。
  4. 调试技巧 :使用SpelExpression.getAST()查看表达式抽象语法树,便于排查问题。

理解SpEL的每个特性,并结合实际场景灵活运用,可以显著提升Spring应用的动态性和可扩展性。

相关推荐
fjkxyl4 分钟前
Spring的启动流程
java·后端·spring
东锋1.314 分钟前
Spring AI 发布了它的 1.0.0 版本的第七个里程碑(M7)
java·人工智能·spring
AronTing4 小时前
07-云原生安全深度剖析:从 Kubernetes 集群防护到微服务安全加固
spring·微服务·架构
乐予吕5 小时前
手写一个微型 Spring 框架:从端口监听到依赖注入
java·后端·spring
qq_447663057 小时前
Spring-注解编程
java·后端·spring
Warren987 小时前
Springboot项目正常启动,访问资源却出现404错误如何解决?
java·spring boot·spring
小丁爱养花17 小时前
驾驭 Linux 云: JavaWeb 项目安全部署
java·linux·运维·服务器·spring boot·后端·spring
经年小栈18 小时前
性能优化-Spring参数配置、数据库连接参数配置、JVM调优
数据库·spring·性能优化
eternal__day18 小时前
第二期:[特殊字符] 深入理解MyBatis[特殊字符]MyBatis基础CRUD操作详解[特殊字符]
java·spring·java-ee·maven·mybatis
魔道不误砍柴功19 小时前
SpringCloud Alibaba 之分布式全局事务 Seata 原理分析
分布式·spring·spring cloud