一佳技术

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。两种解决方案:

  1. 继承 DefaultConfigStorage,重写 save/delete 后手动触发
  2. 用 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备注
InMemoryRepositoryConcurrentHashMap✅ prefix filter✅ 完整实现线程安全,重启丢失
FileRepositoryYAML 文件✅ 每个 namespace 一个文件✅ 完整实现非泛型,硬编码 ConfigDefinition;可 readOnly=true 只加载 classpath 资源
RedisRepositoryRedisTemplate✅ 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。