SpEL 表达式引擎 - ExpressionParser、EvaluationContext 与实战应用
1. SpEL 概述
Spring Expression Language(SpEL)是 Spring 框架提供的一种强大的表达式语言,支持在运行时查询和操作对象图。SpEL 的语法类似于 Unified EL,但提供了更强大的功能,包括方法调用、集合操作、正则表达式匹配等。
SpEL 可以独立于 Spring 容器使用,也可以集成在 Spring 的各种组件中(如 @Value、@Cacheable、@PreAuthorize 等)。
核心 API 包含三个主要接口:
ExpressionParser— 解析表达式字符串为Expression对象Expression— 代表一个已解析的表达式,可在上下文中求值EvaluationContext— 为表达式求值提供上下文环境(变量、函数、Bean 引用等)
2. SpEL 核心 API
2.1 ExpressionParser 与 Expression
ExpressionParser 负责将表达式字符串解析为 Expression 对象,Expression 对象可在后续多次求值中复用。
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpelBasicExample {
public static void main(String[] args) {
// 创建解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析字符串字面量
Expression expr = parser.parseExpression("'Hello SpEL'");
String result = (String) expr.getValue();
System.out.println(result); // Hello SpEL
// 解析数字
int number = parser.parseExpression("42").getValue(Integer.class);
System.out.println(number); // 42
// 解析数学运算
int sum = parser.parseExpression("10 + 20 * 3").getValue(Integer.class);
System.out.println(sum); // 70
}
}2.2 EvaluationContext
EvaluationContext 提供了表达式求值所需的上下文信息,包括变量定义、函数注册、Bean 引用解析以及类型转换器等。
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class EvaluationContextExample {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
// 创建 EvaluationContext,绑定根对象
User user = new User("Alice", 25);
EvaluationContext context = new StandardEvaluationContext(user);
// 访问根对象的属性
String name = parser.parseExpression("name").getValue(context, String.class);
int age = parser.parseExpression("age").getValue(context, Integer.class);
System.out.println(name + " is " + age + " years old."); // Alice is 25 years old.
}
static class User {
public String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
}3. SpEL 表达式类型
3.1 字面量表达式
ExpressionParser parser = new SpelExpressionParser();
// 字符串字面量
String str = parser.parseExpression("'Hello World'").getValue(String.class);
// 数字字面量
int intVal = parser.parseExpression("1024").getValue(Integer.class);
double doubleVal = parser.parseExpression("3.14").getValue(Double.class);
// 布尔字面量
boolean boolVal = parser.parseExpression("true").getValue(Boolean.class);
// Null 字面量
Object nullVal = parser.parseExpression("null").getValue();3.2 布尔表达式
ExpressionParser parser = new SpelExpressionParser();
// 比较运算
boolean b1 = parser.parseExpression("10 > 5").getValue(Boolean.class); // true
boolean b2 = parser.parseExpression("10 == 10").getValue(Boolean.class); // true
boolean b3 = parser.parseExpression("'abc' instanceof T(String)").getValue(Boolean.class); // true
// 逻辑运算
boolean b4 = parser.parseExpression("true && false").getValue(Boolean.class); // false
boolean b5 = parser.parseExpression("true || false").getValue(Boolean.class); // true
boolean b6 = parser.parseExpression("!true").getValue(Boolean.class); // false
// 正则匹配
boolean b7 = parser.parseExpression("'abc123' matches '\\\\d+'").getValue(Boolean.class); // false
boolean b8 = parser.parseExpression("'123' matches '\\\\d+'").getValue(Boolean.class); // true3.3 方法调用
EvaluationContext context = new StandardEvaluationContext(new User("Bob", 30));
// 调用根对象的方法
String result = parser.parseExpression("greet('Hello')").getValue(context, String.class);
System.out.println(result); // Hello, I'm Bob
// 调用 String 方法
int length = parser.parseExpression("'Hello SpEL'.length()").getValue(Integer.class); // 10
String sub = parser.parseExpression("'Hello SpEL'.substring(6)").getValue(String.class); // SpEL3.4 属性访问
User user = new User("Charlie", 28);
user.setAddress(new Address("123 Main St", "Beijing"));
EvaluationContext context = new StandardEvaluationContext(user);
// 直接访问属性
String name = parser.parseExpression("name").getValue(context, String.class);
// 嵌套属性访问
String city = parser.parseExpression("address.city").getValue(context, String.class);
System.out.println(city); // Beijing
// 安全导航运算符(避免 NPE)
String zip = parser.parseExpression("address?.zipCode?.toUpperCase()")
.getValue(context, String.class);
System.out.println(zip); // null,不会抛 NPE3.5 集合操作
// 构造列表
List<Integer> numbers = (List<Integer>)
parser.parseExpression("{1, 2, 3, 4, 5}").getValue();
// 构造数组
int[] array = (int[])
parser.parseExpression("new int[]{10, 20, 30}").getValue();
// 构造 Map
Map<String, String> map = (Map<String, String>)
parser.parseExpression("{'name': 'Alice', 'age': '25'}").getValue();
// 集合选择(筛选)
List<Integer> evenNumbers = (List<Integer>)
parser.parseExpression("{1, 2, 3, 4, 5}.?[#this % 2 == 0]").getValue();
System.out.println(evenNumbers); // [2, 4]
// 集合投影(映射)
List<Integer> squares = (List<Integer>)
parser.parseExpression("{1, 2, 3}.![#this * #this]").getValue();
System.out.println(squares); // [1, 4, 9]3.6 三元运算与 Elvis 运算符
// 三元运算符
int max = parser.parseExpression("10 > 5 ? 10 : 5").getValue(Integer.class);
// Elvis 运算符(?:)— 简化三元,对象非 null 则用自身,否则用默认值
String name = parser.parseExpression("null ?: 'defaultName'").getValue(String.class);
System.out.println(name); // defaultName
// Elvis 结合根对象
User user = new User(null, 20);
EvaluationContext context = new StandardEvaluationContext(user);
String displayName = parser.parseExpression("name ?: 'Anonymous'")
.getValue(context, String.class);
System.out.println(displayName); // Anonymous3.7 赋值表达式
User user = new User("OldName", 30);
StandardEvaluationContext context = new StandardEvaluationContext(user);
// 赋值并返回新值
String newName = parser.parseExpression("name = 'NewName'")
.getValue(context, String.class);
System.out.println(newName); // NewName
System.out.println(user.getName()); // NewName3.8 类型、new 对象与变量
// T() — 获取类型
Class<?> clazz = parser.parseExpression("T(java.lang.Math)").getValue(Class.class);
// 调用静态方法
double random = parser.parseExpression("T(java.lang.Math).random()")
.getValue(Double.class);
double pi = parser.parseExpression("T(java.lang.Math).PI")
.getValue(Double.class);
// new 对象
String reversed = parser.parseExpression(
"new java.lang.StringBuilder('SpEL').reverse().toString()"
).getValue(String.class);
System.out.println(reversed); // LEpS
// #variable — 变量引用
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("greeting", "Hello SpEL");
String msg = parser.parseExpression("#greeting + ' is powerful'")
.getValue(ctx, String.class);
System.out.println(msg); // Hello SpEL is powerful3.9 #root 与 #this 引用
// #root 引用根对象
StandardEvaluationContext ctx = new StandardEvaluationContext("RootObject");
String rootVal = parser.parseExpression("#root").getValue(ctx, String.class);
System.out.println(rootVal); // RootObject
// #this 引用当前求值对象(集合选择/投影中常用)
// 选出大于 10 的元素
List<Integer> result = (List<Integer>)
parser.parseExpression("{5, 15, 8, 25, 3}.?[#this > 10]").getValue();
System.out.println(result); // [15, 25]
// #root 结合集合选择
List<Order> orders = Arrays.asList(
new Order("A", 100), new Order("B", 200), new Order("C", 50)
);
StandardEvaluationContext orderCtx = new StandardEvaluationContext(orders);
List<Order> expensiveOrders = (List<Order>)
parser.parseExpression("#root.?[amount > 80]").getValue(orderCtx);
// 等价写法(省略 #root,因为根对象就是 List)
expensiveOrders = (List<Order>)
parser.parseExpression("?[amount > 80]").getValue(orderCtx);3.10 类型转换 T()
T() 运算符用于获取 Java 类型的 Class 对象,从而访问静态字段或调用静态方法。
// 获取类型
Class<?> mathClass = parser.parseExpression("T(java.lang.Math)").getValue(Class.class);
// 调用静态方法
long max = parser.parseExpression("T(java.lang.Long).parseLong('1024')")
.getValue(Long.class);
// 访问静态常量
int maxInt = parser.parseExpression("T(Integer).MAX_VALUE")
.getValue(Integer.class);
// 类型判断
boolean isString = parser.parseExpression("'abc' instanceof T(String)")
.getValue(Boolean.class); // true
// 类型转换
Integer converted = parser.parseExpression("T(Integer).valueOf('42')")
.getValue(Integer.class);4. EvaluationContext 详解
4.1 StandardEvaluationContext
StandardEvaluationContext 是功能最完整的 EvaluationContext 实现,支持变量设置、函数注册、Bean 引用、类型转换器和属性访问控制。
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class StandardContextExample {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
// 创建上下文,设置根对象
User user = new User("Alice", 25);
StandardEvaluationContext context = new StandardEvaluationContext(user);
// 设置变量
context.setVariable("discount", 0.8);
context.setVariable("bonus", 100);
// 表达式中引用 #variable
double price = parser.parseExpression("100 * #discount + #bonus")
.getValue(context, Double.class);
System.out.println(price); // 180.0
// 注册自定义函数
try {
context.registerFunction("toUpperCase",
User.class.getDeclaredMethod("staticToUpper", String.class));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
String upper = parser.parseExpression("#toUpperCase('hello')")
.getValue(context, String.class);
System.out.println(upper); // HELLO
}
}4.2 SimpleEvaluationContext
SimpleEvaluationContext 是 Spring 4.1 引入的精简版上下文,仅支持部分 SpEL 功能,默认禁用了不安全的功能(如类类型访问 T()、new 对象、赋值等),适合用于用户输入的表达式场景,提高了安全性。
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class SimpleContextExample {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
User user = new User("Bob", 30);
// SimpleEvaluationContext 使用 Builder 构建
SimpleEvaluationContext context = SimpleEvaluationContext
.forReadOnlyDataBinding() // 只读模式
.withRootObject(user) // 设置根对象
.build();
// 可读属性
String name = parser.parseExpression("name")
.getValue(context, String.class);
System.out.println(name); // Bob
// 以下操作会抛出异常(SimpleEvaluationContext 不支持)
try {
// T() 类型访问被禁止
parser.parseExpression("T(java.lang.Math).random()").getValue(context);
// 赋值被禁止(只读上下文)
parser.parseExpression("name = 'NewName'").getValue(context);
// new 对象被禁止
parser.parseExpression("new java.util.Date()").getValue(context);
} catch (Exception e) {
System.out.println("SimpleEvaluationContext 限制了此操作: " + e.getMessage());
}
}
}4.3 StandardEvaluationContext vs SimpleEvaluationContext
| 特性 | StandardEvaluationContext | SimpleEvaluationContext |
|---|---|---|
| 属性访问 | 支持 | 支持 |
| 方法调用 | 支持 | 部分支持 |
| 变量(#variable) | 支持 | 不支持 |
| 自定义函数 | 支持 | 不支持 |
| Bean 引用(@beanRef) | 支持 | 不支持 |
| 类型表达式 T() | 支持 | ❌ 禁止 |
| new 对象 | 支持 | ❌ 禁止 |
| 赋值操作 | 支持 | ❌ 禁止(只读版本) |
| 使用场景 | 内部可靠表达式 | 用户输入的表达式 |
安全建议:当表达式来自不可信来源(如用户配置、外部输入)时,优先使用 SimpleEvaluationContext 并配合 setEvaluationContextCompilerAutomaticGrowth 等限制措施。
5. SpEL 在 Spring 框架中的应用
5.1 @Value 注解中的 SpEL
使用 #{…} 语法在 @Value 中注入 SpEL 表达式结果。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class SpelConfigBean {
// 字面量
@Value("#{42}")
private int answer;
// 调用静态方法
@Value("#{T(java.lang.Math).random() * 100.0}")
private double randomValue;
// 引用其他 Bean 的属性
@Value("#{otherBean.someProperty}")
private String otherBeanProperty;
// 三元运算
@Value("#{systemProperties['user.timezone'] ?: 'Asia/Shanghai'}")
private String timezone;
// 逻辑运算
@Value("#{2 > 1 && 3 < 5}")
private boolean logicalResult;
// 集合选择
@Value("#{orderService.orders.?[amount > 100]}")
private List<Order> largeOrders;
// 方法调用
@Value("#{beanUtil.toUpperCase('hello')}")
private String upperCaseValue;
}5.2 @Cacheable / #result 引用
Spring Cache 注解支持使用 SpEL 表达式定义缓存 key 和条件,#result 表示方法返回值。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// #id 引用方法参数
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟数据库查询
return userRepository.findById(id);
}
// 多参数组合 key
@Cacheable(value = "users", key = "#firstName + '-' + #lastName")
public User findByName(String firstName, String lastName) {
return userRepository.findByFirstNameAndLastName(firstName, lastName);
}
// 条件缓存:仅当 id > 1000 时缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 1000")
public User getUserConditionally(Long id) {
return userRepository.findById(id);
}
// #result 引用方法返回值(用于 @CachePut 和 @CacheEvict)
@CachePut(value = "users", key = "#result.id")
public User createUser(User user) {
return userRepository.save(user);
}
// #result 用于条件判断
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserOrNull(Long id) {
return userRepository.findById(id).orElse(null);
}
}5.3 XML 配置中的 SpEL
<?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">
<!-- 定义 Bean -->
<bean id="otherBean" class="com.example.OtherBean">
<property name="someProperty" value="Hello from OtherBean"/>
</bean>
<!-- SpEL 注入属性 -->
<bean id="myBean" class="com.example.MyBean">
<!-- 字面量 -->
<property name="count" value="#{42}"/>
<!-- 引用其他 Bean -->
<property name="name" value="#{otherBean.someProperty}"/>
<!-- 方法调用 -->
<property name="upperName" value="#{otherBean.someProperty.toUpperCase()}"/>
<!-- 三元运算 -->
<property name="status" value="#{systemProperties['env'] == 'prod' ? 'production' : 'development'}"/>
<!-- 布尔运算 -->
<property name="enabled" value="#{2 > 1}"/>
</bean>
</beans>6. @Value("#{…}") 与 @Value("${…}") 的区别
| 特性 | @Value("#{…}") SpEL 表达式 | @Value("${…}") 属性占位符 |
|---|---|---|
| 处理器 | SpEL 表达式引擎 | PropertySourcesPlaceholderConfigurer |
| 语法 | #{expression} | ${property.key:defaultValue} |
| 能力 | 方法调用、运算、Bean 引用等完整 SpEL | 仅读取属性值 |
| 数据源 | 任意 Java 对象、Bean、静态方法 | Environment、PropertySource |
| 默认值 | ?: Elvis 运算符 | ${key:defaultValue} |
@Component
public class ConfigComparison {
// 属性占位符 — 从配置文件读取
@Value("${app.name}")
private String appName;
// 属性占位符带默认值
@Value("${app.timeout:5000}")
private int timeout;
// SpEL 表达式
@Value("#{systemProperties['user.home']}")
private String userHome;
// SpEL 引用其他 Bean
@Value("#{dataSource.url}")
private String datasourceUrl;
// 二者结合:${…} 获取属性值,再用 #{…} 做进一步运算
@Value("#{'${app.version}'.toUpperCase()}")
private String versionUpper;
// SpEL 条件逻辑
@Value("#{${app.cache.enabled:false} ? T(java.lang.Integer).MAX_VALUE : 0}")
private int cacheSize;
}推荐用法
- 单纯读取配置值 →
@Value("${…}"),更简洁高效 - 需要运算、方法调用、Bean 引用 →
@Value("#{…}") - 两者可嵌套使用:
@Value("#{'${key}'.toUpperCase()}")
7. 安全表达式
7.1 SecurityExpressionRoot
Spring Security 使用 SpEL 作为方法级别安全控制的基础。SecurityExpressionRoot 提供了安全表达式求值的根对象,包含 hasRole、hasAuthority、permitAll、denyAll 等内置方法。
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
// SecurityExpressionRoot 提供的核心方法(部分)
public class SecurityExpressionRootSample {
// hasRole — 判断当前用户是否拥有指定角色
// boolean hasRole(String role)
// hasAuthority — 判断当前用户是否拥有指定权限
// boolean hasAuthority(String authority)
// permitAll — 始终允许
// boolean permitAll()
// denyAll — 始终拒绝
// boolean denyAll()
// isAnonymous — 是否匿名用户
// boolean isAnonymous()
// isAuthenticated — 是否已认证
// boolean isAuthenticated()
// isRememberMe — 是否通过 "记住我" 认证
// boolean isRememberMe()
// authentication — 获取当前 Authentication 对象
// Authentication getAuthentication()
// hasPermission — 权限评估(需要 PermissionEvaluator 支持)
// boolean hasPermission(Object target, Object permission)
// boolean hasPermission(Object targetId, String targetType, Object permission)
}7.2 @PreAuthorize、@PostAuthorize 的 SpEL
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class SecureOrderService {
// 基于角色控制
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long orderId) {
// 仅 ADMIN 可删除
}
// 基于权限控制
@PreAuthorize("hasAuthority('order:write')")
public Order createOrder(Order order) {
return orderRepository.save(order);
}
// 方法参数参与表达式
@PreAuthorize("#order.owner == authentication.name")
public void updateOrder(Order order) {
// 仅订单所有者可修改
}
// 多条件组合
@PreAuthorize("hasRole('ADMIN') or (#order.owner == authentication.name)")
public Order getOrder(Order order) {
return order;
}
// 引用 Bean 的方法做自定义验证
@PreAuthorize("@orderSecurity.canAccess(#orderId, authentication)")
public Order findOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
// @PostAuthorize — 方法执行后检查返回值
@PostAuthorize("returnObject.owner == authentication.name")
public Order getOrderById(Long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
// 过滤集合(已废弃,推荐 Java 8 Stream)
@PreAuthorize("permitAll()")
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
}7.3 permitAll / denyAll
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
// 在 Web 安全配置中使用 SpEL
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/public/**").access("permitAll()")
.antMatchers("/api/admin/**").access("hasRole('ADMIN')")
.antMatchers("/api/user/**").access(
"hasRole('USER') and isAuthenticated()")
.antMatchers("/api/operator/**").access(
"hasRole('ADMIN') or hasRole('OPERATOR')")
.anyRequest().access("denyAll()"); // 默认拒绝所有
}
}8. 自定义函数与 Bean 引用
8.1 registerFunction — 注册自定义函数
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class CustomFunctionExample {
public static void main(String[] args) throws Exception {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// 注册自定义函数
context.registerFunction("isEmpty",
CustomFunctionExample.class.getDeclaredMethod("isEmpty", String.class));
context.registerFunction("maskPhone",
CustomFunctionExample.class.getDeclaredMethod("maskPhone", String.class));
// 使用注册的函数
boolean empty = parser.parseExpression("#isEmpty('')")
.getValue(context, Boolean.class);
System.out.println(empty); // true
String masked = parser.parseExpression("#maskPhone('13812345678')")
.getValue(context, String.class);
System.out.println(masked); // 138****5678
}
public static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
}8.2 @beanRef — 在 SpEL 中引用 Spring Bean
在 Spring 容器环境中,可以使用 @beanName 语法引用 Spring Bean。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class BeanRefExample {
// 引用名为 "orderService" 的 Bean
@Value("#{@orderService}")
private OrderService orderService;
// 调用 Bean 的方法
@Value("#{@orderService.getOrderCount()}")
private int orderCount;
// 调用 Bean 的方法并传参
@Value("#{@stringUtil.concat('Hello', ' SpEL')}")
private String concatResult;
// 链式调用
@Value("#{@userRepository.findById(1L).name.toUpperCase()}")
private String userNameUpper;
}在非 Spring 容器环境中(如独立运行的规则引擎),通过 StandardEvaluationContext 的 setBeanResolver 实现 Bean 解析:
import org.springframework.context.ApplicationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
StandardEvaluationContext context = new StandardEvaluationContext();
// 设置 Bean 解析器,关联 ApplicationContext
ApplicationContext appContext = obtainApplicationContext();
context.setBeanResolver(new BeanFactoryResolver(appContext));
// 表达式中引用 Bean
double rate = parser.parseExpression("@exchangeRateService.getRate('USD', 'CNY')")
.getValue(context, Double.class);9. 性能优化
9.1 编译模式(SpelCompilerMode)
SpEL 支持三种求值模式,通过 SpelCompilerMode 控制:
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpelPerformanceExample {
public static void main(String[] args) {
// 配置 SpEL 解析器 — 立即编译模式
SpelParserConfiguration config = new SpelParserConfiguration(
SpelCompilerMode.IMMEDIATE, // 编译模式
SpelPerformanceExample.class.getClassLoader() // 指定类加载器
);
ExpressionParser parser = new SpelExpressionParser(config);
// 表达式首次求值会编译成字节码
Expression expr = parser.parseExpression("'Hello ' + name");
User user = new User("World", 1);
StandardEvaluationContext context = new StandardEvaluationContext(user);
// 第一次调用:解释执行并触发编译
String result1 = expr.getValue(context, String.class);
System.out.println(result1); // Hello World
// 后续调用:直接执行编译后的字节码
String result2 = expr.getValue(context, String.class);
System.out.println(result2); // Hello World
}
}| 编译模式 | 说明 | 适用场景 |
|---|---|---|
OFF | 不编译,始终解释执行 | 一次性求值或低频调用 |
IMMEDIATE | 首次求值后立即编译 | 高频调用的表达式 |
MIXED | 解释执行与编译混合,失败时回退解释 | 需要兼顾启动速度和生产性能 |
9.2 表达式缓存
在实际应用中,表达式解析是一个相对昂贵的操作,应尽可能缓存 Expression 对象,避免重复解析。
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ExpressionCache {
private final ExpressionParser parser = new SpelExpressionParser();
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
public <T> T evaluate(String expression, Object root, Class<T> returnType) {
// 先从缓存获取 Expression 对象
Expression expr = cache.computeIfAbsent(expression, parser::parseExpression);
return expr.getValue(root, returnType);
}
public void evict(String expression) {
cache.remove(expression);
}
public void clear() {
cache.clear();
}
// 使用示例
public static void main(String[] args) {
ExpressionCache cache = new ExpressionCache();
User user = new User("CachedUser", 28);
// 多次求值,仅第一次解析表达式
for (int i = 0; i < 1000; i++) {
String name = cache.evaluate("name.toUpperCase()", user, String.class);
}
}
}9.3 性能对比与建议
public class SpelBenchmark {
private static final int WARMUP = 5000;
private static final int ITERATIONS = 100000;
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
Expression expr = parser.parseExpression("name.length() + age");
User user = new User("Benchmark", 30);
// 预热
for (int i = 0; i < WARMUP; i++) {
expr.getValue(user);
}
// 测试
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
expr.getValue(user);
}
long elapsed = System.nanoTime() - start;
System.out.printf("执行 %d 次,平均耗时: %.2f μs%n",
ITERATIONS, elapsed / 1_000_000.0 / ITERATIONS * 1000);
}
}性能优化建议:
- 缓存 Expression 对象 — 避免重复解析,是最重要的优化手段
- 使用编译模式
IMMEDIATE— 高频表达式开启编译,可提升 5~10 倍性能 - SimpleEvaluationContext — 轻量上下文减少安全检查开销
- 避免复杂表达式 — 嵌套过深或集合选择/投影操作会带来额外开销
- 静态类型 — 指定
getValue(Class)返回类型可避免类型转换开销
10. 实战案例:电商风控规则引擎
使用 SpEL 实现一个可配置的风控规则引擎,支持运营人员动态配置风控规则。
10.1 风控规则模型
import java.util.function.BiFunction;
// 风控规则
public class RiskRule {
private String name; // 规则名称
private String condition; // SpEL 条件表达式,返回 boolean
private String action; // 规则命中后的动作:REJECT / REVIEW / PASS
private int priority; // 优先级
private boolean enabled; // 是否启用
// getters/setters 略
public RiskRule(String name, String condition, String action, int priority) {
this.name = name;
this.condition = condition;
this.action = action;
this.priority = priority;
this.enabled = true;
}
public String getName() { return name; }
public String getCondition() { return condition; }
public String getAction() { return action; }
public int getPriority() { return priority; }
public boolean isEnabled() { return enabled; }
}10.2 风控上下文
import java.time.LocalDateTime;
import java.util.Map;
// 风控请求上下文,作为表达式根对象
public class RiskContext {
private String userId; // 用户 ID
private String userLevel; // 用户等级
private double orderAmount; // 订单金额
private String ipAddress; // IP 地址
private String deviceId; // 设备 ID
private int orderCountLastHour; // 最近一小时订单数
private String paymentMethod; // 支付方式
private String shippingAddress; // 收货地址
private Map<String, Object> extra; // 扩展属性
// getters/setters
public String getUserId() { return userId; }
public String getUserLevel() { return userLevel; }
public double getOrderAmount() { return orderAmount; }
public String getIpAddress() { return ipAddress; }
public String getDeviceId() { return deviceId; }
public int getOrderCountLastHour() { return orderCountLastHour; }
public String getPaymentMethod() { return paymentMethod; }
public String getShippingAddress() { return shippingAddress; }
public Map<String, Object> getExtra() { return extra; }
public static RiskContextBuilder builder() {
return new RiskContextBuilder();
}
public static class RiskContextBuilder {
private RiskContext ctx = new RiskContext();
public RiskContextBuilder userId(String userId) { ctx.userId = userId; return this; }
public RiskContextBuilder userLevel(String level) { ctx.userLevel = level; return this; }
public RiskContextBuilder orderAmount(double amount) { ctx.orderAmount = amount; return this; }
public RiskContextBuilder ipAddress(String ip) { ctx.ipAddress = ip; return this; }
public RiskContextBuilder deviceId(String deviceId) { ctx.deviceId = deviceId; return this; }
public RiskContextBuilder orderCountLastHour(int count) { ctx.orderCountLastHour = count; return this; }
public RiskContextBuilder paymentMethod(String method) { ctx.paymentMethod = method; return this; }
public RiskContextBuilder shippingAddress(String addr) { ctx.shippingAddress = addr; return this; }
public RiskContextBuilder extra(Map<String, Object> extra) { ctx.extra = extra; return this; }
public RiskContext build() { return ctx; }
}
}10.3 风控规则引擎
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class RiskRuleEngine {
private final ExpressionParser parser;
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
private final List<RiskRule> rules;
public RiskRuleEngine(List<RiskRule> rules) {
// 使用 SimpleEvaluationContext 增强安全性
SpelParserConfiguration config = new SpelParserConfiguration();
this.parser = new SpelExpressionParser(config);
this.rules = new ArrayList<>(rules);
// 按优先级排序
this.rules.sort(Comparator.comparingInt(RiskRule::getPriority));
}
/**
* 执行风控检查
* @param context 风控上下文
* @return 风控结果
*/
public RiskResult evaluate(RiskContext context) {
// 创建只读的 EvaluationContext,将 RiskContext 作为根对象
SimpleEvaluationContext evalContext = SimpleEvaluationContext
.forReadOnlyDataBinding()
.withRootObject(context)
.build();
for (RiskRule rule : rules) {
if (!rule.isEnabled()) continue;
try {
// 从缓存获取已编译的表达式
Expression condition = expressionCache.computeIfAbsent(
rule.getCondition(), parser::parseExpression);
// 执行条件判断
boolean matched = condition.getValue(evalContext, Boolean.class);
if (matched) {
return new RiskResult(rule.getName(), rule.getAction(),
"规则命中: " + rule.getCondition());
}
} catch (Exception e) {
// 规则解析或执行异常,记录日志但不影响其他规则
System.err.println("规则 [" + rule.getName() + "] 执行异常: " + e.getMessage());
}
}
return RiskResult.pass();
}
/**
* 动态添加/更新规则
*/
public void addRule(RiskRule rule) {
rules.add(rule);
rules.sort(Comparator.comparingInt(RiskRule::getPriority));
// 清除缓存,确保新规则生效
expressionCache.remove(rule.getCondition());
}
// 风控结果
public static class RiskResult {
private final String ruleName;
private final String action; // REJECT / REVIEW / PASS
private final String reason;
private RiskResult(String ruleName, String action, String reason) {
this.ruleName = ruleName;
this.action = action;
this.reason = reason;
}
public static RiskResult pass() {
return new RiskResult(null, "PASS", "通过所有规则");
}
public boolean isRejected() { return "REJECT".equals(action); }
public boolean isReviewRequired() { return "REVIEW".equals(action); }
public boolean isPassed() { return "PASS".equals(action); }
public String getRuleName() { return ruleName; }
public String getAction() { return action; }
public String getReason() { return reason; }
@Override
public String toString() {
return "RiskResult{action='" + action + "', rule='" + ruleName + "', reason='" + reason + "'}";
}
}
}10.4 使用示例
public class RiskEngineDemo {
public static void main(String[] args) {
// 配置风控规则
List<RiskRule> rules = Arrays.asList(
// 规则1:高金额订单 + 新设备
new RiskRule("高风险大额订单",
"orderAmount > 5000 && deviceId != null && orderCountLastHour <= 1",
"REVIEW", 10),
// 规则2:频繁下单
new RiskRule("频繁下单",
"orderCountLastHour >= 10",
"REJECT", 20),
// 规则3:新用户大额支付
new RiskRule("新用户大额支付",
"userLevel == 'NEW' && orderAmount > 2000 && paymentMethod == 'CREDIT_CARD'",
"REVIEW", 30),
// 规则4:IP 变更频繁(通过扩展字段)
new RiskRule("IP 异常",
"extra != null && extra['ipChangeCount'] != null " +
"&& T(Integer).parseInt(extra['ipChangeCount'].toString()) > 3",
"REJECT", 40),
// 规则5:正常订单放行
new RiskRule("正常订单",
"orderAmount <= 5000 && orderCountLastHour < 10",
"PASS", 100)
);
RiskRuleEngine engine = new RiskRuleEngine(rules);
// 构造一个高风险订单
RiskContext context = RiskContext.builder()
.userId("user_001")
.userLevel("NEW")
.orderAmount(8888.00)
.ipAddress("192.168.1.1")
.deviceId("DEVICE_NEW_001")
.orderCountLastHour(1)
.paymentMethod("CREDIT_CARD")
.shippingAddress("北京市朝阳区")
.extra(Map.of("ipChangeCount", "5"))
.build();
// 执行风控
RiskRuleEngine.RiskResult result = engine.evaluate(context);
System.out.println("风控结果: " + result);
// 输出: 风控结果: RiskResult{action='REJECT', rule='IP 异常', reason='规则命中: extra != null && ...'}
}
}11. 实战案例:优惠券动态计算引擎
使用 SpEL 实现一个可配置的优惠券计算引擎,支持运营人员自定义满减、折扣等优惠规则。
11.1 优惠券规则模型
import java.math.BigDecimal;
import java.time.LocalDateTime;
// 优惠券规则
public class CouponRule {
private String code; // 优惠券编码
private String name; // 优惠券名称
private String condition; // SpEL 条件表达式(是否可用),返回 boolean
private String discountExpr; // SpEL 折扣计算表达式,返回 BigDecimal
private String description; // 描述
private LocalDateTime validFrom; // 有效期开始
private LocalDateTime validTo; // 有效期结束
private boolean stackable; // 是否可叠加
// getters/setters 略
public CouponRule(String code, String name, String condition,
String discountExpr, boolean stackable) {
this.code = code;
this.name = name;
this.condition = condition;
this.discountExpr = discountExpr;
this.stackable = stackable;
this.validFrom = LocalDateTime.now().minusDays(1);
this.validTo = LocalDateTime.now().plusDays(30);
}
public String getCode() { return code; }
public String getName() { return name; }
public String getCondition() { return condition; }
public String getDiscountExpr() { return discountExpr; }
public String getDescription() { return description; }
public LocalDateTime getValidFrom() { return validFrom; }
public LocalDateTime getValidTo() { return validTo; }
public boolean isStackable() { return stackable; }
}11.2 订单上下文
import java.math.BigDecimal;
import java.util.List;
// 订单上下文,作为 SpEL 表达式的根对象
public class OrderContext {
private String orderId;
private String userId;
private String userLevel; // 用户等级
private BigDecimal totalAmount; // 订单总金额
private BigDecimal freight; // 运费
private List<OrderItem> items; // 订单项
private String category; // 商品类目
// getters/setters
public String getOrderId() { return orderId; }
public String getUserId() { return userId; }
public String getUserLevel() { return userLevel; }
public BigDecimal getTotalAmount() { return totalAmount; }
public BigDecimal getFreight() { return freight; }
public List<OrderItem> getItems() { return items; }
public String getCategory() { return category; }
// 辅助方法 — 可在 SpEL 中调用
public boolean hasCategory(String cat) {
return items.stream().anyMatch(item -> item.getCategory().equals(cat));
}
public BigDecimal totalByCategory(String cat) {
return items.stream()
.filter(item -> item.getCategory().equals(cat))
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public int itemCount() {
return items.size();
}
public static class OrderItem {
private String productId;
private String productName;
private String category;
private BigDecimal price;
private int quantity;
public OrderItem(String productId, String productName, String category,
BigDecimal price, int quantity) {
this.productId = productId;
this.productName = productName;
this.category = category;
this.price = price;
this.quantity = quantity;
}
public String getProductId() { return productId; }
public String getProductName() { return productName; }
public String getCategory() { return category; }
public BigDecimal getPrice() { return price; }
public int getQuantity() { return quantity; }
}
}