Skip to content

模型注册中心

角色定位

ModelRegistry 是扫描阶段的唯一出口

  • 上游 DdlInitializer 把解析好的 ModelDefinition 一个个 register(...) 进来
  • 下游(未来阶段的 SQL 生成器、DDL 执行器、外部 REST endpoint 等)通过 getModel / getAllModels 消费

整个注册中心刻意保持薄、只读多写少、零外部依赖

选型:为什么是 ConcurrentHashMap

java
private final Map<String, ModelDefinition> registry = new ConcurrentHashMap<>();
  • 键唯一性modelName 全局唯一,天然是 Map 形态
  • 并发可见性:注册行为发生在 ApplicationRunner 的单线程内,但读取行为可能由 Web 线程、Scheduled 线程等并发触发;ConcurrentHashMap 提供的 happens-before 语义保证 put-then-get 的正确性
  • 零成本:JDK 自带,无外部依赖

为什么不用 HashMap + synchronized?

本场景写入只在启动期一次性发生,读取却是高并发的。ConcurrentHashMap 的分段读优化在这种 write-once-read-many 的访问模式下性能优于 synchronizedMap,且代码更简洁。

注册策略:覆盖 + 警告

java
public void register(ModelDefinition definition) {
    if (definition == null || definition.getModelName() == null) {
        throw new IllegalArgumentException("ModelDefinition and modelName must not be null");
    }
    ModelDefinition previous = registry.put(definition.getModelName(), definition);
    if (previous != null) {
        log.warn("Duplicate model registration overwritten: modelName={}", definition.getModelName());
    }
}

两条规则:

  1. 入参校验nullmodelName == null 直接 IllegalArgumentException —— 这是契约违反,不容静默
  2. 重名覆盖:后注册的赢,旧的丢;同时 WARN 日志告诉开发者「这里有问题」

为什么是覆盖而不是抛错?

  • 抛错可能导致启动失败,给开发期带来体感很差的“半挂”状态
  • 注解扫描在不同包扫到同名 @Model 是合法但可疑的场景,告警而非阻塞更适合一个还在迭代的框架
  • 后续阶段如果需要严格模式,可以提供一个 @ConditionalOnProperty("forge.ddl.strict-duplicate=true") 的开关切换为抛错

读取:返回不可修改视图

java
public Collection<ModelDefinition> getAllModels() {
    return Collections.unmodifiableCollection(registry.values());
}

这里有一个细节:返回的是 Collections.unmodifiableCollection(...),调用方拿到的视图:

  • 不能调用 add / remove —— 否则 UnsupportedOperationException
  • 会随原 Map 变化 —— unmodifiableCollection 只是写保护,不是快照

阶段①扫描完成后注册中心不再变化,上述区别不重要;阶段③(DDL 执行)启动后可能需要快照,那时再视情况切换为 List.copyOf(...) 即可。

当前数据结构

ModelDefinition.sourceClass 字段被 @ToString.Exclude 标记,避免日志里把整个 Class 信息打印出来,保持日志清爽。

演进预留

后续阶段可能新增的注册中心能力
SQL 生成Collection<ModelDefinition> getStoreModels() —— 只取 STORE 类型
DDL 执行引入版本号,支持 snapshot() 拿一致性视图
继承组合register(...) 之前先做 ABSTRACT 父模型合并

当前阶段刻意不引入这些接口,避免无用泛化。