真正起作用的是“类加载时初始化 + 线程安全容器 + 合理淘汰策略”的组合,static 只是入口点;需用 concurrenthashmap 配合 caffeine/guava 实现自动淘汰,缓存对象须不可变、无上下文引用,并区分语法解析与执行计划的缓存层级。

直接用 static 声明缓存表达式本身不安全,也不高效。真正起作用的是“类加载时初始化 + 线程安全容器 + 合理淘汰策略”的组合,static 只是触发这一机制的入口点。
缓存对象必须线程安全且带生命周期控制
单纯写 public static Map<string expression> cache = new HashMap();</string> 会出问题:HashMap 非线程安全,多规则并发解析时可能 corrupt;没有大小限制,长期运行 OOM;无过期机制,配置变更后旧表达式仍被复用。
- Java 推荐用
ConcurrentHashMap作为底层容器,但不能裸用——需配合 Guava 或 Caffeine 实现自动淘汰 - 例如:
static final LoadingCache<string expression> PARSER_CACHE = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(5, TimeUnit.MINUTES).build(key -> parseExpression(key));</string> - 避免在
static初始化块中调用外部服务或文件读取,失败会导致类加载中断,整个规则引擎不可用
表达式解析结果应不可变且可复用
缓存的不是原始字符串,而是解析后的 AST 或编译后的执行单元(如 Java 的 MethodHandle、Groovy 的 CompiledScript)。这些对象必须设计为不可变(immutable),否则共享状态下被某次匹配修改状态,会影响后续所有请求。
- 推荐封装成只读结构:
public final class CompiledRule { private final String exprText; private final BiPredicate<context object> evaluator; ... }</context> - 不要缓存含上下文引用的对象(如带
this或闭包的 lambda),它们绑定特定实例,无法跨请求复用 - 若使用 SpEL、Aviator 等引擎,优先启用其内置缓存(如
SpelExpressionParser的setCacheLimit())
区分缓存层级:语法解析 vs. 执行优化
一个表达式从字符串到可执行逻辑,通常分两步:词法/语法解析(耗 CPU)、执行计划生成(可能依赖运行时数据)。这两层适合不同策略。
- 语法树(AST)适合长期缓存——结构稳定,变化少,可用
static final+ LRU 控制 - 执行计划(如 SQL 查询计划、条件分支路径)更适合请求级缓存或软引用缓存,避免持有太多运行时对象
- Spring 表达式引擎中,
@Cacheable注解比手写static缓存更合适,它自动集成 AOP、失效通知和统计监控
注意多租户与热更新场景
规则引擎常需支持租户隔离或动态上线新规则。static 缓存天然不具备租户维度,也难响应配置刷新。
- 租户场景下,缓存 key 应包含租户 ID:
tenantId:expressionStr,而非仅表达式原文 - 配置中心推送变更时,不能只清空局部 key,建议用版本号标记缓存项,让旧版本自然过期
- 避免用
static缓存依赖 Spring 上下文的对象(如@AutowiredBean),它们在类加载时尚未注入,会 NPE











