0%

使用Spel表达式

Spring SpEL 表达式全解析:从基础语法到实战场景

Spring 表达式语言(Spring Expression Language,简称 SpEL)是 Spring 框架提供的一种强大的表达式语言,支持在运行时解析和计算表达式,功能覆盖 “变量引用、方法调用、属性访问、逻辑运算” 等场景。它不仅可用于 XML / 注解配置中的占位符解析(如 ${user.name}),还能在代码中动态处理复杂表达式(如解析注解中的动态逻辑)。从 “核心概念→基础语法→实战场景→高级特性” 四个维度,系统讲解 SpEL 的使用方法与最佳实践。

SpEL 核心价值与应用场景

SpEL 的核心是 “在运行时动态解析表达式”,解决传统硬编码无法应对的动态逻辑问题,典型应用场景包括:

应用场景 示例 核心价值
配置文件占位符解析 XML 中 ${db.url}、注解中 @Value("${user.name}") 实现配置与代码分离,支持外部化配置
注解动态逻辑 自定义注解中 @Log(template = "操作 ${#userId}") 注解参数支持动态变量,提升注解灵活性
动态数据访问 解析对象属性 user.name、调用方法 user.getAge() 无需硬编码 getter/setter,动态操作对象
复杂逻辑计算 表达式 #price * (1 - #discount) 计算折扣价 支持算术 / 逻辑运算,简化动态计算代码
集合操作 过滤集合 #users.?[age > 18]、获取首元素 #users[^1] 简化集合筛选、排序、投影等操作

SpEL 基础:核心组件与执行流程

在使用 SpEL 前,需先理解其核心组件与执行流程,这是后续实战的基础。

核心组件

SpEL 的执行依赖三个核心组件:

组件 作用 核心类 / 接口
表达式解析器 将字符串表达式解析为 Expression 对象 ExpressionParser(常用实现 SpelExpressionParser
解析上下文 定义表达式的语法规则(如模板分隔符) ParserContext(常用实现 TemplateParserContext
计算上下文 提供表达式执行所需的变量、函数、类型等 EvaluationContext(常用实现 StandardEvaluationContext
表达式对象 已解析的表达式,可执行计算 Expression(通过 parser.parseExpression() 获取)

执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 核心代码(带流程标注)
public String parseTemplateExpression(String template, Map<String, String> variables) {
// 1. 创建表达式解析器(负责将字符串表达式转为Expression对象)
ExpressionParser parser = new SpelExpressionParser();

// 2. 创建模板解析上下文(定义模板表达式的分隔符:${ 开头,} 结尾)
ParserContext parserContext = new TemplateParserContext("${", "}");

// 3. 创建计算上下文(注入表达式所需的变量、函数等)
EvaluationContext context = new StandardEvaluationContext();
// 注入自定义变量(如 mediaName → "视频1")
for (Map.Entry<String, String> entry : variables.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}

// 4. 解析表达式 + 执行计算(替换模板中的变量)
// 表达式模板:"媒体名称 ${#mediaName}" → 解析后为 "媒体名称 视频1"
Expression expression = parser.parseExpression(template, parserContext);
return expression.getValue(context, String.class); // 返回解析结果
}

流程拆解

  1. 解析器初始化SpelExpressionParser 是 SpEL 的默认解析器,支持所有 SpEL 语法;
  2. 模板上下文配置TemplateParserContext("${", "}") 定义 “模板表达式” 的格式(区别于普通表达式),你的场景中 {#mediaName} 会被识别为模板变量;
  3. 变量注入:通过 EvaluationContext.setVariable() 注入动态变量,表达式中需用 #变量名 引用(如 #mediaName);
  4. 表达式执行expression.getValue() 会先解析模板(替换变量),再返回计算结果(你的场景中是字符串替换,复杂场景会执行运算)。

SpEL 基础语法:从简单到复杂

SpEL 语法灵活且功能强大,掌握基础语法是应对复杂场景的前提。以下按 “变量引用→属性访问→方法调用→逻辑运算→集合操作” 分类讲解。

1. 变量引用(你的场景核心)

  • 语法:通过 #变量名 引用 EvaluationContext 中注入的变量;

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 1. 注入变量
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("userId", 1001);
    context.setVariable("userName", "张三");

    // 2. 解析表达式
    ExpressionParser parser = new SpelExpressionParser();
    // 表达式:引用两个变量,拼接字符串
    Expression expr = parser.parseExpression("用户ID:#userId,姓名:#userName");
    String result = expr.getValue(context, String.class);
    System.out.println(result); // 输出:用户ID:1001,姓名:张三

2. 属性访问(对象属性与静态属性)

支持访问对象的实例属性、静态属性,无需硬编码 getter 方法。

(1)实例属性访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义用户类
class User {
private String name;
private int age;
// 无需 getter,SpEL 可通过反射访问私有属性(需注意安全管理器限制)
public User(String name, int age) {
this.name = name;
this.age = age;
}
}

// SpEL 访问实例属性
User user = new User("李四", 25);
EvaluationContext context = new StandardEvaluationContext(user); // 传入根对象(可省略 # 直接访问属性)

ExpressionParser parser = new SpelExpressionParser();
// 访问根对象属性(无需 #)
String name = parser.parseExpression("name").getValue(context, String.class); // 结果:李四
int age = parser.parseExpression("age").getValue(context, int.class); // 结果:25

// 若根对象未传入上下文,需通过变量引用
context.setVariable("user", user);
String name2 = parser.parseExpression("#user.name").getValue(context, String.class); // 结果:李四
(2)静态属性与方法调用

通过 T(全类名) 引用类,进而访问静态属性或调用静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ExpressionParser parser = new SpelExpressionParser();

// 1. 访问静态属性(如 Math.PI)
double pi = parser.parseExpression("T(java.lang.Math).PI").getValue(double.class);
System.out.println(pi); // 输出:3.141592653589793

// 2. 调用静态方法(如 Math.max(10, 20))
int max = parser.parseExpression("T(java.lang.Math).max(10, 20)").getValue(int.class);
System.out.println(max); // 输出:20

// 3. 自定义类的静态方法
class StringUtils {
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
}
String reversed = parser.parseExpression("T(com.example.StringUtils).reverse('hello')").getValue(String.class);
System.out.println(reversed); // 输出:olleh

3. 方法调用(实例方法与链式调用)

支持调用对象的实例方法,甚至链式调用(方法返回值继续调用方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
User user = new User("张三", 20);
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", user);

ExpressionParser parser = new SpelExpressionParser();

// 1. 调用实例方法(如 String 的 length() 方法)
int nameLength = parser.parseExpression("#user.name.length()").getValue(context, int.class);
System.out.println(nameLength); // 输出:2("张三" 的长度)

// 2. 链式调用(先获取 name,再转大写,再取子串)
String subStr = parser.parseExpression("#user.name.toUpperCase().substring(1)").getValue(context, String.class);
System.out.println(subStr); // 输出:三("张三" → "张三" → 截取索引1后的字符)

4. 算术与逻辑运算

支持常见的算术运算(+、-、*、/、%)、比较运算(>、<、==)、逻辑运算(&&、||、!):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("price", 100.0);
context.setVariable("discount", 0.8); // 8折
context.setVariable("minPrice", 50.0);

ExpressionParser parser = new SpelExpressionParser();

// 1. 算术运算:计算折扣价(price * (1 - discount))
double finalPrice = parser.parseExpression("#price * (1 - #discount)").getValue(context, double.class);
System.out.println(finalPrice); // 输出:20.0

// 2. 逻辑运算:判断折扣价是否大于最小价格(finalPrice > minPrice)
boolean isQualified = parser.parseExpression("#price * (1 - #discount) > #minPrice").getValue(context, boolean.class);
System.out.println(isQualified); // 输出:false(20 < 50)

5. 集合操作(筛选、排序、投影)

SpEL 对集合的支持极为强大,无需循环即可完成筛选、排序、投影等操作,语法简洁高效:

(1)集合筛选(?[]

筛选集合中满足条件的元素,返回新集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 准备集合数据
List<User> users = Arrays.asList(
new User("张三", 18),
new User("李四", 25),
new User("王五", 30)
);

EvaluationContext context = new StandardEvaluationContext();
context.setVariable("users", users);

ExpressionParser parser = new SpelExpressionParser();

// 筛选年龄 > 20 的用户(?[条件])
List<User> adultUsers = parser.parseExpression("#users.?[age > 20]").getValue(context, new TypeReference<List<User>>() {});
System.out.println(adultUsers.size()); // 输出:2(李四、王五)
(2)集合投影(![]

提取集合中元素的某个属性,形成新的 “属性集合”(类似 Stream 的 map 操作):

1
2
3
// 投影:提取所有用户的姓名,形成字符串集合(![])
List<String> userNames = parser.parseExpression("#users.!['姓名:' + name]").getValue(context, new TypeReference<List<String>>() {});
System.out.println(userNames); // 输出:[姓名:张三, 姓名:李四, 姓名:王五]
(3)集合首 / 尾元素(^[]/$[]

快速获取集合的第一个或最后一个元素:

1
2
3
// 获取年龄最大的用户(先排序,再取最后一个)
User oldestUser = parser.parseExpression("#users.^[age > 0].sort(age).$[0]").getValue(context, User.class);
System.out.println(oldestUser.getName()); // 输出:王五(30岁)

实战场景深度解析

扩展两个高频实战场景,覆盖 “注解动态逻辑解析” 和 “配置文件复杂表达式”。

场景 1:解析注解中的动态模板(你的需求延伸)

假设你需要自定义一个 @Log 注解,其 content 属性支持 SpEL 模板表达式,用于动态生成日志内容(如包含用户 ID、操作类型):

步骤 1:定义自定义注解
1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
// 日志模板,支持 SpEL 表达式(如 "用户 #userId 执行 #operation")
String content();
}
步骤 2:AOP 切面解析注解表达式

通过 AOP 拦截标注 @Log 的方法,解析表达式并生成日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.StandardEvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.TemplateParserContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Aspect
@Component
public class LogAspect {
// 1. 表达式解析器(线程安全,可全局复用)
private final ExpressionParser parser = new SpelExpressionParser();
// 2. 模板解析上下文(固定分隔符 ${})
private final ParserContext templateContext = new TemplateParserContext("${", "}");

// 切入点:匹配标注 @Log 的方法
@Pointcut("@annotation(com.example.Log)")
public void logPointcut() {}

// 后置通知:方法执行后解析表达式并生成日志
@AfterReturning("logPointcut()")
public void afterReturning(JoinPoint joinPoint) {
// 1. 获取目标方法与 @Log 注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
String logTemplate = logAnnotation.content(); // 获取注解中的模板表达式

// 2. 准备表达式变量(如从请求上下文获取 userId,从方法参数获取 operation)
Map<String, Object> variables = new HashMap<>();
// 模拟从请求上下文获取 userId
variables.put("userId", 1001);
// 模拟从方法参数获取 operation(假设方法第一个参数是操作类型)
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
variables.put("operation", args[0]);
}

// 3. 解析 SpEL 模板表达式
StandardEvaluationContext context = new StandardEvaluationContext();
variables.forEach(context::setVariable); // 注入变量

String logContent = parser.parseExpression(logTemplate, templateContext)
.getValue(context, String.class);

// 4. 输出日志(实际项目中可接入日志框架)
System.out.println("[自动日志] " + logContent);
}
}
步骤 3:使用注解并测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.stereotype.Service;

@Service
public class UserService {
// 注解中使用 SpEL 模板:动态引用 #userId 和 #operation
@Log(content = "用户 ${#userId} 执行 ${#operation} 操作成功")
public void updateUser(String operation, Long userId) {
// 业务逻辑:更新用户信息
System.out.println("更新用户 " + userId + " 的操作:" + operation);
}
}

// 测试代码
public class LogTest {
public static void main(String[] args) {
UserService userService = new UserService();
// 模拟 AOP 拦截后,日志输出:[自动日志] 用户 1001 执行 update 操作成功
userService.updateUser("update", 1001L);
}
}

场景 2:配置文件中的复杂表达式解析

除了代码中的动态解析,SpEL 还常用于 XML / 注解配置中的复杂表达式,例如在 @Value 中计算动态值:

(1)注解配置中使用 SpEL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppConfig {
// 1. 引用系统属性(user.home)
@Value("#{systemProperties['user.home']}")
private String userHome;

// 2. 引用环境变量(PATH)
@Value("#{environment['PATH']}")
private String systemPath;

// 3. 复杂表达式:计算 100 * 0.8(折扣价)
@Value("#{100 * 0.8}")
private double discountPrice;

// 4. 引用其他 Bean 的属性(假设存在 userBean,其有 name 属性)
@Value("#{userBean.name}")
private String userName;

// Getter 方法
public String getUserHome() { return userHome; }
public double getDiscountPrice() { return discountPrice; }
}
(2)XML 配置中使用 SpEL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 1. 配置 Bean,属性值通过 SpEL 解析 -->
<bean id="orderBean" class="com.example.Order">
<!-- 计算折扣价:#price * (1 - #discount) -->
<property name="finalPrice" value="#{100 * (1 - 0.2)}"/>
<!-- 引用其他 Bean 的属性:userBean.name -->
<property name="userName" value="#{userBean.name}"/>
</bean>

<!-- 2. 配置 userBean -->
<bean id="userBean" class="com.example.User">
<property name="name" value="张三"/>
</bean>
</beans>

SpEL 高级特性:类型转换与自定义函数

1. 自动类型转换

SpEL 支持自动将表达式结果转换为目标类型,无需手动强转,例如将字符串 true 转为布尔值、将数字字符串转为整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ExpressionParser parser = new SpelExpressionParser();

// 1. 字符串 → 布尔值
boolean isTrue = parser.parseExpression("'true'").getValue(boolean.class);
System.out.println(isTrue); // 输出:true

// 2. 字符串 → 整数
int num = parser.parseExpression("'123'").getValue(int.class);
System.out.println(num); // 输出:123

// 3. 表达式结果 → 自定义对象(需提供类型转换器)
// 自定义类型转换器(实现 Converter 接口)
class StringToUserConverter implements Converter<String, User> {
@Override
public User convert(String source) {
String[] parts = source.split(",");
return new User(parts[0], Integer.parseInt(parts[1]));
}
}

// 注册转换器并解析
StandardEvaluationContext context = new StandardEvaluationContext();
context.setTypeConverter(new DefaultConversionService() {{
addConverter(new StringToUserConverter());
}});
User user = parser.parseExpression("'李四,25'").getValue(context, User.class);
System.out.println(user.getName()); // 输出:李四

2. 自定义函数

通过 EvaluationContext 注册自定义函数,让 SpEL 支持业务特定的复杂逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

public class CustomFunctionTest {
// 自定义函数:计算两个数的平均值
public static double average(double a, double b) {
return (a + b) / 2;
}

public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

// 注册自定义函数:将 average 方法注册为 SpEL 中的 avg 函数
context.registerFunction("avg", CustomFunctionTest.class.getMethod("average", double.class, double.class));

// 调用自定义函数:avg(10, 20)
double avg = parser.parseExpression("#avg(10, 20)").getValue(context, double.class);
System.out.println(avg); // 输出:15.0
}
}

注意事项与避坑指南

  1. 线程安全ExpressionParser(如 SpelExpressionParser)是线程安全的,可全局复用;EvaluationContext 是非线程安全的,需为每个线程创建新实例(避免变量污染);
  2. 模板表达式 vs 普通表达式:
    • 模板表达式:需通过 TemplateParserContext 定义分隔符(如 ${}),适用于 “静态文本 + 动态变量” 场景(如你的日志模板);
    • 普通表达式:无需分隔符,直接解析表达式(如 #user.name + #user.age);
  3. 变量引用语法:
    • 引用 EvaluationContext 中的变量:#变量名
    • 引用根对象(StandardEvaluationContext 构造时传入的对象)的属性:直接写属性名(无需 #);
  4. 性能优化:频繁解析相同表达式时,可缓存 Expression 对象(解析过程耗时,执行过程高效);
  5. 安全风险:SpEL 支持反射访问私有属性和方法,若表达式来自外部输入(如用户提交的字符串),需过滤危险表达式(如 T(java.lang.Runtime).getRuntime().exec('rm -rf /')),避免代码注入攻击。

总结

SpEL 是 Spring 框架中极具灵活性的特性,不仅能解决 “配置占位符解析”“注解动态逻辑” 等基础场景,还能应对 “复杂计算”“集合操作” 等高级需求。其核心优势在于:

  1. 动态性:运行时解析表达式,应对硬编码无法覆盖的动态逻辑;
  2. 简洁性:一行表达式替代多行文法计算、集合筛选代码;
  3. 集成性:无缝融入 Spring 配置与代码,支持变量、对象、函数的灵活组合

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10