easy-store 使用文档
easy-store 提供抽象化的配置存储抽象层,支持三种持久化后端(文件 / Redis / 内存),支持 CRUD + 缓存 + 配置变更监听。引擎组件(easy-flows、easy-rules)通过注入 ConfigStorage 实现配置持久化,后端通过 ConfigRepository SPI 灵活切换,业务代码无需修改。
1. 引入依赖
<!-- 核心依赖(必需) -->
<dependency>
<groupId>io.matrix</groupId>
<artifactId>easy-store</artifactId>
<version>${revision}</version>
</dependency>
<!-- Redis 后端(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
文件 / 内存后端不需要额外依赖,easy-store 自带实现。
2. 选择后端
| 后端 | 适合场景 | 前置条件 |
|---|---|---|
| InMemory(内存) | 开发期、单元测试、小数据量配置 | 无(默认) |
| File(文件) | 静态配置、classpath 资源、无需动态更新 | 无(YAML 格式,自动扫描) |
| Redis | 生产环境、需要动态更新、多实例共享 | spring-boot-starter-data-redis |
3. 配置
easy-store 没有独立的 easy.store.* ConfigurationProperties。通过 Spring Boot 自动配置或手动创建 Bean。
手动创建示例:
@Configuration
public class StoreConfig {
// 内存后端(开发/测试)
@Bean
public ConfigStorage<ConfigDefinition> memoryConfigStorage() {
InMemoryRepository<ConfigDefinition> repo = new InMemoryRepository<>();
DefaultConfigStorage<ConfigDefinition> storage = new DefaultConfigStorage<>(repo);
storage.setNamespace("rules");
storage.setDescription("规则引擎存储");
return storage;
}
// 文件后端(静态配置)
@Bean
public ConfigStorage<ConfigDefinition> fileConfigStorage() {
FileRepository repo = new FileRepository();
repo.setBasePath("config/"); // 可选,默认 classpath:META-INF/config/
DefaultConfigStorage<ConfigDefinition> storage = new DefaultConfigStorage<>(repo);
storage.setNamespace("flows");
return storage;
}
// Redis 后端(生产)
@Bean
public ConfigStorage<ConfigDefinition> redisConfigStorage(StringRedisTemplate redisTemplate) {
RedisRepository<ConfigDefinition> repo = new RedisRepository<>(redisTemplate);
DefaultConfigStorage<ConfigDefinition> storage = new DefaultConfigStorage<>(repo);
storage.setNamespace("rules");
return storage;
}
}
4. 编程式 API
4.1 基础 CRUD
@Service
public class RuleService {
@Autowired
@Qualifier("fileConfigStorage")
private ConfigStorage<ConfigDefinition> storage;
public void saveRule(String ruleKey, String mvelText) {
ConfigDefinition def = new ConfigDefinition();
def.setNamespace("rules");
def.setKey(ruleKey);
def.setText(mvelText);
def.setEngine("mvel");
def.setVersion(1);
storage.save("rules", ruleKey, def);
}
public Optional<String> loadRule(String ruleKey) {
return storage.load("rules", ruleKey)
.map(ConfigDefinition::getText);
}
public void deleteRule(String ruleKey) {
storage.delete("rules", ruleKey);
}
public boolean hasRule(String ruleKey) {
return storage.exists("rules", ruleKey);
}
}
4.2 批量操作
// 批量保存
Map<String, ConfigDefinition> configs = Map.of(
"rule-1", rule1Def,
"rule-2", rule2Def,
"rule-3", rule3Def
);
storage.saveAll("rules", configs);
// 批量加载(全 namespace)
Map<String, ConfigDefinition> all = storage.loadAll("rules");
// 批量加载(指定 keys)
Map<String, ConfigDefinition> subset = storage.loadAll("rules", List.of("rule-1", "rule-3"));
// 列出所有 key
Set<String> keys = storage.listKeys("rules");
// 删除整个 namespace
storage.deleteAll("rules");
4.3 缓存行为
ConfigStorage 内部维护了一个 ConcurrentMap<String, T> cache(key 格式 namespace:key):
| 操作 | 缓存行为 |
|---|---|
save() | ⚠️ 不写缓存(写路径跳过缓存) |
load() | computeIfAbsent():miss 时从 Repository 读 + 写入缓存 |
delete() | 先清缓存再删 Repository |
loadAll() | 批量加载 + 全部写入缓存 |
注意:save 后立即 load,如果该 key 之前已被缓存过,可能读到旧值。delete 先清缓存则能避免这个问题。
4.4 配置变更监听
// 需要 DefaultConfigStorage 的子类或 AOP 触发 fireConfigChangeEvent
WatchableConfigStorage<ConfigDefinition> watchable =
(WatchableConfigStorage<ConfigDefinition>) storage;
// 监听单个 key
WatchHandle handle = watchable.watch("rules", "rule-1", event -> {
if (event.getType() == ConfigChangeEvent.ChangeType.UPDATE) {
System.out.println("规则更新了: " + event.getNewValue().getText());
}
});
// 监听整个 namespace
WatchHandle nsHandle = watchable.watchNamespace("rules", event -> {
System.out.println("规则事件: " + event.getType() + " key=" + event.getKey());
});
// 批量监听
WatchHandle batchHandle = watchable.watchKeys("rules", List.of("rule-1", "rule-2"), event -> {
// ...
});
// 取消监听
watchable.unwatch(handle);
⚠️ 当前版本监听不会自动触发:
DefaultConfigStorage没有在 save/delete 时调fireConfigChangeEvent。两种解决方案:
- 继承 DefaultConfigStorage,重写 save/delete 后手动触发
- 用 Spring AOP 拦截 ConfigStorage 的 save/delete 方法
子类方案示例:
@Component
public class ObservableConfigStorage extends DefaultConfigStorage<ConfigDefinition> {
public ObservableConfigStorage(ConfigRepository<ConfigDefinition> repository) {
super(repository);
}
@Override
public void save(String namespace, String key, ConfigDefinition value) {
// 先读旧值(用于对比)
ConfigDefinition old = getRepository().find(namespace, key);
super.save(namespace, key, value);
// 清缓存 + 触发事件
getCache().remove(cacheKey(namespace, key));
ConfigChangeEvent.ChangeType type = (old == null)
? ConfigChangeEvent.ChangeType.CREATE
: ConfigChangeEvent.ChangeType.UPDATE;
fireConfigChangeEvent(namespace, key, old, value, type);
}
@Override
public void delete(String namespace, String key) {
ConfigDefinition old = load(namespace, key).orElse(null);
super.delete(namespace, key);
fireConfigChangeEvent(namespace, key, old, null, ConfigChangeEvent.ChangeType.DELETE);
}
}
4.5 懒加载附加对象
ConfigDefinition 有一个 attachment 字段(Object 类型),用于存懒加载的派生对象:
// 从 ConfigDefinition 懒加载 MVEL 表达式
String expression = storage.loadAttachment("rules", "rule-1", def -> {
return MVEL.compileExpression(def.getText());
});
// attachment 存入 ConfigDefinition,后续 loadAttachment 不再重新创建
5. 各后端特点
| 后端 | 持久化 | 命名空间 | listKeys | 备注 |
|---|---|---|---|---|
| InMemoryRepository | ConcurrentHashMap | ✅ prefix filter | ✅ 完整实现 | 线程安全,重启丢失 |
| FileRepository | YAML 文件 | ✅ 每个 namespace 一个文件 | ✅ 完整实现 | 非泛型,硬编码 ConfigDefinition;可 readOnly=true 只加载 classpath 资源 |
| RedisRepository | RedisTemplate | ✅ namespace:key 格式 | ❌ list() 返回空 | 半成品:listKeys / delete(namespace) 不可用 |
FileRepository 存储格式:
# config/rules.yaml(一个 namespace 一个文件)
namespace: rules
engine: mvel
configs:
- key: rule-1
text: "price > 1000 ? 'HIGH' : 'LOW'"
version: 1
- key: rule-2
text: "amount < 0 ? 'ERROR' : 'OK'"
version: 1
6. 自动配置
easy-store 当前没有完整的 auto-configuration(不像 easy-cache 那样有 EasyCacheAutoConfiguration),Bean 需使用方手动创建。EngineAutoConfiguration 负责引擎(rules / flows)的自动装配,不直接管理存储。
7. 注意事项
| 项 | 说明 |
|---|---|
| save 不刷缓存 | save 后立即 load 可能读旧值(如果之前已缓存)。delete 先清缓存则能避免。建议子类重写 save 时先清缓存 |
| 监听被动触发 | fireConfigChangeEvent 是 protected 方法,DefaultConfigStorage 不自动触发。需子类或 AOP |
| RedisRepository 未完成 | listKeys / delete(namespace) 不可用,Redis 后端仅支持单 key 的 save/load/delete |
| FileRepository 非泛型 | 硬编码 ConfigDefinition,不能用于其他泛型类型 |
| namespace + key 双维度 | 命名空间做逻辑隔离(不同引擎/业务各一套存储),key 在 namespace 内唯一 |
| save 是 upsert 模式 | ConfigRepository 没有 update 方法,save 语义即”存或更新” |
| ConfigDefinition.attachment 运行时填充 | attachment 不参与持久化(Repository 层只存 text/version/extra 等固定字段) |
| 并发安全 | 三个 Repository 实现都是线程安全的;ConfigStorage 的 cache 是 ConcurrentHashMap |
8. 常见问题
Q: save 后 load 为什么读旧值? A: 这是 easy-store 的已知行为——save 不刷缓存。有两个解法:(1) 子类重写 save 先 getCache().remove(cacheKey(ns, key));(2) 先 delete 再 save(delete 会清缓存)。
Q: RedisRepository 能用吗? A: 有限能用。单 key 的 save/load/delete 可以,listKeys / delete(namespace) 不可用。如果需要完整功能,等后续补全 SCAN 命令实现。
Q: FileRepository 怎么加载 classpath 下的配置? A: 默认 basePath=META-INF/config/,readOnly=true。把 YAML 放进 src/main/resources/META-INF/config/{namespace}.yaml,启动时自动加载。
Q: 一个 ConfigStorage 实例能管多个 namespace 吗? A: 可以。ConfigStorage 实例持有默认 namespace,但 load/save/delete 都有带 namespace 参数的重载方法。
Q: InMemoryRepository 的 key 格式? A: namespace:key,和 ConfigStorage.cacheKey() 一致。InMemoryRepository 内部用 prefix filter 实现 list(namespace) / delete(namespace)。
Q: Redis 后端的 key 格式? A: namespace:key,和 InMemoryRepository 一致。但 RedisRepository 的 list(namespace) 未实现,所以没法列出某个 namespace 下所有 key。