SpEL — Spring 表达式语言
一句话定位:SpEL(Spring Expression Language)是 Spring 框架内置的"动态计算器",让你在 XML、注解或代码中编写表达式,在运行时查询对象属性、调用方法、操作集合,甚至做逻辑判断。
为什么需要 SpEL?
想象乐途公司的小崔在配置邮件服务时,需要根据不同环境动态设置 SMTP 地址。如果没有 SpEL,他只能写死配置或写大量 if-else。有了 SpEL,一行表达式搞定:
@Value("#{environment['spring.profiles.active'] == 'prod' ? 'smtp.learnto.cn' : 'localhost'}")
private String smtpHost;
SpEL 贯穿 Spring 的各个角落:
@Value("#{...}")注入动态值- Spring Security 的
@PreAuthorize("hasRole('ADMIN')") - 缓存注解的
key = "#userId" - XML 配置中的条件属性
核心语法速览
SpEL 表达式以 #{} 包裹(在 @Value 中),支持以下操作:
| 类型 | 示例 | 结果 |
|---|---|---|
| 字面量 | 'Hello SpEL' | 字符串 |
| 属性访问 | #user.name | 访问 user 对象的 name 属性 |
| 方法调用 | #user.getName() | 调用方法 |
| 静态方法 | T(java.lang.Math).random() | 调用 Math.random() |
| 运算符 | #age > 18 ? '成年' : '未成年' | 三元运算 |
| 正则 | #email matches '[a-z]+@learnto.cn' | 正则匹配 |
| 集合 | #orders.?[status == 'PAID'] | 过滤集合 |
| 投影 | #orders.![totalAmount] | 提取字段形成新列表 |
| 选择 | #orders.^[status == 'PAID'] | 找第一个匹配元素 |
在 Spring 中的使用场景
@Value 注入
@Service
public class FeixiangConfigService {
// 注入系统属性
@Value("#{systemProperties['os.name']}")
private String osName;
// 注入环境变量
@Value("#{systemEnvironment['HOME']}")
private String homePath;
// 条件表达式:生产环境用真实网关,测试用假网关
@Value("#{${feixiang.payment.mock:false} ? 'fakeGateway' : 'alipayGateway'}")
private String gatewayBeanName;
// 数学计算
@Value("#{T(java.lang.Math).PI * 2}")
private double twoPi;
}
缓存 Key 生成
乐途公司的商品详情页访问量巨大,小崔加了缓存:
@Cacheable(value = "product", key = "#productId + '_' + #region")
public Product getProduct(String productId, String region) {
return productRepository.findById(productId);
}
@CacheEvict(value = "product", key = "#product.id + '_*'")
public void updateProduct(Product product) {
productRepository.save(product);
}
安全权限控制
@RestController
public class SalaryController {
// 只有 HR 或本人能查看工资
@PreAuthorize("hasRole('HR') or #userId == authentication.principal.id")
@GetMapping("/api/salary/{userId}")
public Salary getSalary(@PathVariable Long userId) {
return salaryService.findByUserId(userId);
}
// CEO 大翔能看所有人的工资
@PreAuthorize("hasRole('CEO')")
@GetMapping("/api/salary/all")
public List<Salary> getAllSalaries() {
return salaryService.findAll();
}
}
编程式使用
除了注解,SpEL 也可以在代码中直接调用:
@Service
public class PromotionEngine {
private final ExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context;
public PromotionEngine() {
this.context = new StandardEvaluationContext();
// 注册自定义函数
try {
context.registerFunction("discount",
PromotionEngine.class.getDeclaredMethod("calculateDiscount", double.class, int.class));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
// 自定义折扣函数:满 100 减 10,满 200 减 30
public static double calculateDiscount(double amount, int quantity) {
if (amount >= 200) return 30;
if (amount >= 100) return 10;
return 0;
}
public double evaluatePromotion(Order order, String expression) {
// 将 order 设为根对象
context.setRootObject(order);
// 也可以设置变量
context.setVariable("vipLevel", order.getUser().getVipLevel());
Expression exp = parser.parseExpression(expression);
return exp.getValue(context, Double.class);
}
}
乐途公司双十一促销,小崔用 SpEL 让运营自己写促销规则:
// 运营配置的规则表达式
String rule = "#vipLevel >= 3 ? totalAmount * 0.8 : totalAmount * 0.95";
Order order = new Order();
order.setTotalAmount(188.88f * 2); // 买两把乐途机械键盘
order.setUser(new User(2024001L, 3)); // 钻石会员
double finalAmount = promotionEngine.evaluatePromotion(order, rule);
// 结果:377.76 * 0.8 = 302.208
集合操作
SpEL 对集合的支持非常强大:
// 过滤:找出所有已支付订单
List<Order> paidOrders = parser.parseExpression(
"#orders.?[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, List.class);
// 投影:提取所有订单金额
List<Float> amounts = parser.parseExpression(
"#orders.![totalAmount]"
).getValue(context, List.class);
// 选择第一个匹配
Order firstPaid = parser.parseExpression(
"#orders.^[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, Order.class);
// 选择最后一个匹配
Order lastPaid = parser.parseExpression(
"#orders.$[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, Order.class);
注意事项
| 注意点 | 说明 |
|---|---|
| 安全性 | 不要直接执行用户输入的 SpEL 表达式,存在代码注入风险。使用 SimpleEvaluationContext 限制权限 |
| 性能 | 频繁执行的表达式应编译缓存(Spring 4.1+ 支持 SpelCompilerMode.IMMEDIATE) |
| 类型转换 | SpEL 自动进行类型转换,但复杂场景建议显式指定 getValue(context, TargetType.class) |
| 空安全 | 使用 ?. 安全导航:#user?.address?.city,避免 NPE |
与 @Value("${...}") 的区别 | ${...} 是占位符解析(PropertyPlaceholder),#{...} 是 SpEL 表达式。两者可嵌套:@Value("${app.name:feixiang}") vs @Value("#{systemProperties['user.name']}") |
常见面试题
Q1:@Value("${key}") 和 @Value("#{expression}") 有什么区别?
${...}是属性占位符解析,从Environment/PropertySource中查找 key 对应的值,不支持运算。#{...}是 SpEL 表达式,支持属性访问、方法调用、运算、集合操作等完整表达式能力。两者可组合使用:@Value("#{${app.timeout} * 1000}")。
Q2:SpEL 的 EvaluationContext 是什么?
EvaluationContext是 SpEL 表达式的执行上下文,提供变量解析、方法解析、类型转换等服务。StandardEvaluationContext功能最全但权限大;SimpleEvaluationContext受限但更安全,适合解析不可信表达式。
Q3:如何在 SpEL 中调用 Spring Bean 的方法?
使用
@beanName.method()语法,例如@orderService.findById(8888)。需要在BeanResolver可用的上下文中执行。
Q4:SpEL 表达式编译模式有什么用?
默认 SpEL 是解释执行,每次都要解析 AST。开启
SpelCompilerMode.IMMEDIATE后,表达式会被编译成 JVM 字节码,执行速度提升 10~100 倍,适合高频执行的表达式(如缓存 key 生成)。