你是否想过,为什么 @Value("${server.port}") 能自动读取配置?为什么 Spring Security 的 @PreAuthorize("hasRole('ADMIN')") 能识别复杂的权限表达式?
甚至在很多大厂的自研中间件里,无论是动态路由、灰度发布,还是规则引擎,底层都躺着同一个身影——SpEL (Spring Expression Language)。
它是 Spring 框架中最被低估的“元编程”能力。很多开发者以为它只是框架内部的“黑盒”,但实际上,它是 Pivotal 团队送给我们的一把手术刀。学会使用它,你就能在只有 Java 语法的世界里,获得动态语言般的灵活性。
今天,我们不空谈理论,聚焦两个能直接落地的生产级实战案例。
拒绝丑陋的 String 拼接:AOP + SpEL 实现优雅分布式锁
在微服务场景下,为了防止缓存击穿或数据不一致,我们经常用 Redis 分布式锁。但你现在的代码可能是这样的:
public void updateOrder(OrderReq req){
// 丑陋的字符串拼接,散落在业务代码各处,容易拼错
String lockKey = "lock:order:" + req.getTenantId() + ":" + req.getOrderId();
RLock lock = redisson.getLock(lockKey);
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
这种代码不仅污染了业务逻辑,而且无法复用。资深开发的做法是:自定义注解 + AOP + SpEL 解析。
1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistLock {
// 允许传入 SpEL 表达式,如 “#req.orderId”
String key();
long expire() default 10;
}
2. 生产级切面实现
这里有个生产环境的大坑:Java 编译后默认会丢失方法参数名(变成 arg0, arg1),导致 SpEL 无法识别 #req。必须使用 DefaultParameterNameDiscoverer。
@Aspect
@Component
@Slf4j
public class DistLockAspect{
@Autowired
private RedissonClient redissonClient;
// 核心组件:用于解析 SpEL
private final ExpressionParser parser = new SpelExpressionParser();
// 核心组件:用于获取方法参数名(生产环境必备)
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(distLock)")
public Object around(ProceedingJoinPoint joinPoint, DistLock distLock) throws Throwable {
// 获取方法签名和参数值
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
// 准备 SpEL 上下文
EvaluationContext context = new StandardEvaluationContext();
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 生产容错:防止某些特殊代理类导致拿不到参数名
if (paramNames != null) {
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
// 解析 Key (这是最骚的一步)
// 假设注解是 @DistLock(key = “‘order:’ + #req.orderId”)
String lockKey = parser.parseExpression(distLock.key()).getValue(context, String.class);
// 加锁逻辑 (Redisson)
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock(distLock.expire(), TimeUnit.SECONDS)) {
try {
return joinPoint.proceed();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
throw new BusinessException(“系统繁忙,请勿重复提交“);
}
}
}
3. 优化后的业务代码
@Service
public class OrderService{
// 业务逻辑里再也没有那一堆 lock 代码了
@DistLock(key = “‘lock:order:’ + #req.tenantId + ‘:’ + #req.orderId“)
public void updateOrder(OrderReq req){
// 纯粹的业务逻辑
}
}
告别频繁发版:打造“不用上线”的规则引擎
回到开头的场景:运营频繁调整优惠规则。如果我们把规则抽象成 SpEL 表达式存入数据库,就能实现热加载。
1. 数据库设计 (简化版)
我们有一张表 biz_rules,其中字段 expression 存储规则:
| id |
rule_name |
expression (SpEL) |
discount |
| 1 |
VIP大促 |
#user.vipLevel > 3 and #order.amount > 5000 |
0.8 |
| 2 |
周末狂欢 |
#order.createTime.getDayOfWeek().getValue() >= 6 |
0.9 |
2. 核心解析器
@Service
public class RuleEngineService{
private final ExpressionParser parser = new SpelExpressionParser();
/**
* @param user 当前用户上下文
* @param order 当前订单上下文
* @param ruleScript 从数据库查出来的 SpEL 字符串
*/
public boolean matchRule(UserDTO user, OrderDTO order, String ruleScript){
StandardEvaluationContext context = new StandardEvaluationContext();
// 注入上下文变量
context.setVariable(“user“, user);
context.setVariable(“order“, order);
// 进阶技巧:注册自定义函数,或者允许调用静态方法
// 比如允许在表达式里用 T(java.lang.Math).random()
try {
// 解析并执行
return parser.parseExpression(ruleScript).getValue(context, Boolean.class);
} catch (EvaluationException | ParseException e) {
// 生产环境必须要有兜底策略,规则写错了不能崩系统
log.error(“规则解析失败: script={}, error={}“, ruleScript, e.getMessage());
return false;
}
}
}
3. 实际效果
当运营说:“我们要搞个针对北京地区的活动。” 你只需要在后台配置这条规则存入数据库,一行代码都不用改,立刻生效:#user.address.city == ‘Beijing‘ and #order.amount >= 1000
避坑指南
虽然 SpEL 很香,但在生产环境使用,我有两点血泪建议:
-
安全红线: 永远不要执行用户(前端)传过来的 SpEL 字符串! SpEL 强大到可以执行 T(java.lang.Runtime).getRuntime().exec(“rm -rf /“)。如果你的表达式内容是由外部输入的,那就是给黑客留了个 RCE(远程代码执行)后门。表达式必须由内部受信任的人员(如开发者、运营后台)配置。
-
性能考量: ExpressionParser 是线程安全的,建议定义为全局单例(static final 或 Spring Bean),不要每次调用都 new 一个 Parser,否则在高并发下会产生大量的临时对象,增加 GC 压力。
结语
代码是死的,但逻辑应该是活的。
从硬编码到配置化,是初级开发迈向架构师的重要一步。SpEL 不仅仅是一个工具类,它是一种“元编程”的思维方式。借助 Spring AOP 的切面能力和 微服务 中配置动态化的需求,我们可以构建出更加灵活健壮的 规则引擎。
希望这两个实战案例能为你打开思路。代码的最终形态,或许就藏在这些强大而优雅的原生能力之中。如果你对这类提升开发效率的实践感兴趣,欢迎来云栈社区交流探讨。