找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3027

积分

0

好友

417

主题
发表于 昨天 06:25 | 查看: 1| 回复: 0

你是否想过,为什么 @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 很香,但在生产环境使用,我有两点血泪建议:

  1. 安全红线: 永远不要执行用户(前端)传过来的 SpEL 字符串! SpEL 强大到可以执行 T(java.lang.Runtime).getRuntime().exec(“rm -rf /“)。如果你的表达式内容是由外部输入的,那就是给黑客留了个 RCE(远程代码执行)后门。表达式必须由内部受信任的人员(如开发者、运营后台)配置。

  2. 性能考量ExpressionParser 是线程安全的,建议定义为全局单例(static final 或 Spring Bean),不要每次调用都 new 一个 Parser,否则在高并发下会产生大量的临时对象,增加 GC 压力。

结语

代码是死的,但逻辑应该是活的。

从硬编码到配置化,是初级开发迈向架构师的重要一步。SpEL 不仅仅是一个工具类,它是一种“元编程”的思维方式。借助 Spring AOP 的切面能力和 微服务 中配置动态化的需求,我们可以构建出更加灵活健壮的 规则引擎

希望这两个实战案例能为你打开思路。代码的最终形态,或许就藏在这些强大而优雅的原生能力之中。如果你对这类提升开发效率的实践感兴趣,欢迎来云栈社区交流探讨。




上一篇:拆解Lumentum光芯片布局与AI算力供应链全景
下一篇:Python FastAPI实战:WebSocket消息收发与会话删除接口开发
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 00:42 , Processed in 0.307205 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表