跳到内容

生命周期与服务容器

受众:开发者 摘要:模块 register() / boot() / ready() / shutdown() 生命周期,以及建立在 Spring BeanFactory 之上的服务容器门面。

spring-open-starter-lifecycle 提供一个轻量的模块生命周期模型。它借鉴 Laravel Service Provider 的使用体验,但不重造独立 DI 容器;所有 Bean 最终仍由 Spring Boot 自动配置和 BeanFactory 管理。

何时使用

场景建议
模块需要声明 Spring Boot 自动配置类configure() 中调用 registry.imports(...)
模块需要声明服务绑定register() 中调用 context.container().bind(...)singleton(...)
模块需要声明配置 schemaregister() 中调用 context.config().entry(...)
模块需要聚合贡献项register() 中调用 context.contribute(...)context.container().tag(...)
模块需要在配置 / DB / BeanFactory 就绪后启动逻辑boot() 中执行
只是普通 Spring Bean 装配继续使用标准 @AutoConfiguration / @Bean,但导入入口放到 Provider configure()

基本写法

Provider 默认通过 @AutoService(ServiceProvider.class) 注册。Maven 父工程已配置 AutoService annotation processor,编译时会生成标准 META-INF/services/com.springopen.starter.lifecycle.ServiceProvider,运行时由 ServiceLoader 发现。

Provider 示例:

java
import com.google.auto.service.AutoService;
import com.springopen.starter.lifecycle.config.ConfigValueTypes;

@AutoService(ServiceProvider.class)
public class SampleServiceProvider implements ServiceProvider {

    private static final String CATEGORY = "sample";

    @Override
    public ModuleDescriptor describe() {
        return ModuleDescriptor.of(SampleServiceProvider.class)
                .phase(ModulePhase.BUSINESS)
                .dependsOn(ConfigServiceProvider.class)
                .order(100)
                .build();
    }

    @Override
    public void configure(AutoConfigurationRegistry registry) {
        registry.imports(SampleAutoConfiguration.class);
    }

    @Override
    public void register(RegisterContext context) {
        context.container().singleton(SampleService.class, DefaultSampleService.class);
        context.config().entry("spring.open.sample.enabled", entry -> entry
                .type(ConfigValueTypes.BOOLEAN)
                .defaultValue(true)
                .category(CATEGORY)
                .description("是否启用示例模块"));
    }

    @Override
    public void boot(BootContext context) {
        Boolean enabled = context.config().get("spring.open.sample.enabled", Boolean.class, true);
        if (Boolean.TRUE.equals(enabled)) {
            context.container().resolve(SampleService.class).start();
        }
    }
}

生命周期处理器也兼容 META-INF/spring.factories。外部模块无法启用注解处理、需要沿用 Spring factories SPI 或测试特殊加载路径时,可以使用:

properties
com.springopen.starter.lifecycle.ServiceProvider=\
com.example.sample.SampleServiceProvider

项目内新模块默认使用 @AutoService,不要手写 lifecycle spring.factories

如果希望更接近 Laravel 的短写法,可以继承 ServiceProviderSupport。它只是对完整 ServiceProvider API 的便捷封装,不改变生命周期边界:

java
@AutoService(ServiceProvider.class)
public class SampleServiceProvider extends ServiceProviderSupport {

    private static final String CATEGORY = "sample";

    @Override
    public ModuleDescriptor describe() {
        return ModuleDescriptor.of(SampleServiceProvider.class)
                .phase(ModulePhase.BUSINESS)
                .dependsOn(ConfigServiceProvider.class)
                .build();
    }

    @Override
    protected void configure() {
        autoConfigurations(SampleAutoConfiguration.class);
    }

    @Override
    protected void register() {
        singleton(SampleService.class, DefaultSampleService.class);
        configSchema().entry("spring.open.sample.enabled", entry -> entry
                .type(ConfigValueTypes.BOOLEAN)
                .defaultValue(true)
                .category(CATEGORY)
                .description("是否启用示例模块"));
        manifest().capability("sample", "Sample module");
    }

    @Override
    protected void boot() {
        if (Boolean.TRUE.equals(config().get("spring.open.sample.enabled", Boolean.class, true))) {
            container().resolve(SampleService.class).start();
        }
    }
}

自动配置声明

Spring Boot 仍通过 lifecycle starter 自身的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 找到 bootstrap 入口;普通模块和上层 starter 的自动配置类统一由 ServiceProvider.configure(...) 声明。这样每个模块只维护一个 Provider 入口,同时仍保留 Spring Boot 的条件装配、排序、排除和过滤机制。

java
@Override
public void configure(AutoConfigurationRegistry registry) {
    registry.imports(
            SampleAutoConfiguration.class,
            SampleWebAutoConfiguration.class);
}

configure() 只声明 @AutoConfiguration 类,不读取运行时配置、不访问数据库、不注册 BeanDefinition。服务绑定、配置 schema 和贡献项仍放在 register();依赖运行时配置或数据库的启动逻辑仍放在 boot()

低层 data-rediscacheconfig starter 不依赖 lifecycle,它们的自动配置作为 lifecycle bootstrap 内建导入,避免基础层反向依赖生命周期模块。

模块 ID

ModuleDescriptor 默认使用 Maven artifactId。推荐使用通用入口 ModuleDescriptor.of(...) 传入当前 provider class,由 lifecycle starter 在 IDE target/classes 场景从最近的 pom.xml 解析,在 jar 场景从 META-INF/maven/**/pom.properties 解析,并按 artifactId 自动推断内部模块类型:

场景写法解析结果
Core 模块ModuleDescriptor.of(ConfigServiceProvider.class)spring-open-core-config,类型 core
Starter 模块ModuleDescriptor.of(PaymentServiceProvider.class)spring-open-starter-payment,类型 starter
业务模块ModuleDescriptor.of(SampleServiceProvider.class)provider 所在模块的 artifactId,类型 module
应用模块ModuleDescriptor.of(ApplicationServiceProvider.class)provider 所在应用模块的 artifactId,类型 application
三方扩展ModuleDescriptor.of(ThirdPartyProvider.class)provider 所在 artifactId,无法识别项目内前缀时类型为 extension

依赖声明也推荐使用通用 class anchor,这样依赖 id 会和被依赖模块的 Maven artifactId 保持一致,不需要调用者区分 core / starter / module:

java
ModuleDescriptor.of(SampleServiceProvider.class)
        .dependsOn(ConfigServiceProvider.class)
        .dependsOn(PaymentServiceProvider.class);

如果 provider 不在标准 Maven 输出目录、测试需要构造虚拟模块,或当前模块不应直接依赖对方 provider class,可以使用显式字符串重载:

java
ModuleDescriptor.of("spring-open-core-config");

模块 ID 会用于依赖排序、配置 schema 归属和生命周期报告。默认使用 artifactId 可以和 Maven 模块、发布物、诊断日志保持一致。core(...)starter(...)module(...)application(...) 仍可作为少数需要显式覆盖类型的便捷方法;常规模块接入优先使用 of(...)

接入覆盖与基础 Starter 例外

当前生命周期入口已经覆盖可接入的 core 模块、business module、应用模块和上层 starter。每个接入模块都应提供一个 ServiceProvider,并通过 @AutoService(ServiceProvider.class) 声明。

以下低层底座 starter 不接入 lifecycle,避免基础工具层反向依赖生命周期模块或形成启动循环:

  • spring-open-starter-toolkit
  • spring-open-starter-common
  • spring-open-starter-data-redis
  • spring-open-starter-cache
  • spring-open-starter-config
  • spring-open-starter-lifecycle

通用 Provider 只描述模块身份和生命周期顺序,不需要为了“看起来完整”声明空配置 schema 或空扩展点。只有模块确实提供可配置项、服务绑定或标签聚合时,才在 register() 中声明对应内容。

register 阶段

register() 是声明阶段,只允许:

  • 声明服务绑定、单例绑定和条件绑定。
  • 声明配置 schema。
  • 声明标签和贡献项。

不要在 register() 中读取运行时配置、访问业务数据库、启动线程、注册调度或发事件。此时 Spring 上下文还没有完全就绪,过早读取配置容易绕过数据库覆盖或触发循环依赖。

boot 阶段

boot() 在 Spring 单例初始化后执行。此时可以:

  • 读取 ConfigManager
  • 通过 ServiceContainer 解析 Bean。
  • 同步配置元数据。
  • 启动幂等的监听、刷新或调度注册逻辑。

失败默认 fail-fast;如果希望记录失败但继续启动,可配置:

yaml
spring:
  open:
    lifecycle:
      fail-fast: false

ready 阶段

ready() 在应用完全就绪(ApplicationReadyEvent,Web 容器已可对外服务)后按 boot 顺序执行,适合放启动收尾动作:

  • 预热缓存、编译模板等首请求加速。
  • 补扫孤儿数据、对账修复等就绪后一次性任务。
  • 对注册中心 / 外部系统宣告上线。

不要再为这类需求手写 ApplicationReadyEvent 监听器——直接覆写 Provider 的 ready(BootContext),可获得统一顺序、阶段报告和 /actuator/lifecycle 可观测性。

实现要求幂等。应用此时已对外服务,因此 ready 失败一律软处理:写入 LifecycleReport 并输出 WARN,不会中断应用(不受 fail-fast 影响)。

java
@Override
public void ready(BootContext context) {
    context.bean(SearchIndexWarmer.class).warmUp();
}

继承 ServiceProviderSupport 时也可以写短入口,底座会检测子类是否真正声明了该 hook,未声明的子类不会产生 no-op 报告:

java
@Override
protected void ready() {
    bean(SearchIndexWarmer.class).warmUp();
}

shutdown 阶段

shutdown() 在应用上下文关闭(ContextClosedEvent)时按 boot 逆序执行。此时 Bean 尚未销毁,适合优雅收尾:

  • 停止消息 / 事件监听,拒绝新任务。
  • 排空进行中的队列或缓冲。
  • 从注册中心 / 外部系统注销。

shutdown 失败不阻断关停流程:写入报告并输出 WARN 后继续执行剩余模块的收尾。与 Spring 自带 @PreDestroy 的区别:shutdown 阶段有确定的跨模块顺序(依赖方先收尾、被依赖方后收尾)、统一报告和日志汇总。

ready / shutdown 只对真正覆写了方法的 Provider 执行并记录报告,未覆写的模块不产生阶段记录。继承 ServiceProviderSupport 的 Provider 可覆写短入口 protected ready() / protected shutdown();需要直接使用完整上下文时仍可覆写接口方法 ready(BootContext) / shutdown(BootContext)

启动日志与可观测性

生命周期编排在启动期输出可观测日志,便于在不依赖额外端点的情况下确认编排范围和定位失败:

  • 每个 register() / boot() / ready() / shutdown() 阶段结束输出一行汇总,例如 Lifecycle boot completed: 92 providers, 0 failed, totalDuration=128ms, slowest=module:example(34ms);存在失败时升级为 WARN
  • fail-fast=false 时单个 provider 失败会被记录后继续启动,此时输出 WARN Lifecycle boot failed for module <id> (fail-fast disabled, continuing): <原因>(携带异常栈),避免软失败在生产环境静默丢失。

汇总日志由 report-enabled 控制(默认开启)。关闭后不再输出阶段汇总日志,并且不再发布 LifecycleReport Bean;单个 provider 失败的 WARN 不受该开关影响,始终输出:

yaml
spring:
  open:
    lifecycle:
      report-enabled: false

LifecycleReport Bean 记录每个模块 register / boot / ready / shutdown 的阶段、状态、时间戳、耗时和失败原因,可注入用于诊断或自定义健康检查。ready / shutdown 阶段同样输出汇总日志(仅当有模块参与时)。

当应用引入 actuator(例如 spring-open-starter-observability)时,生命周期报告还会以只读 /actuator/lifecycle 端点暴露(同样受 report-enabled 控制)。端点返回四部分内容,是模块 register 阶段声明面的统一内省消费方:

  • stages:各阶段(register / boot / ready / shutdown)的总数、失败数、总耗时、最慢模块和最慢耗时
  • steps:逐模块步骤明细(阶段、模块类型 / id、状态、时间戳、耗时、失败原因)
  • manifests:各模块通过 manifest() 声明的能力清单(capability / permission / menu / schedule / event-listener / health)
  • contributions:各模块通过 contribute(...) 声明的贡献项索引(贡献类型 → 贡献名列表,只暴露声明元数据,不暴露原始值对象)

lifecycle starter 以 optional 方式依赖 spring-boot-actuator,未引入 actuator 时不装配端点。该端点默认不在 management.endpoints.web.exposure.include 中,需要时显式开放:

yaml
management:
  endpoints:
    web:
      exposure:
        include: [health, lifecycle]

配置 schema

配置 schema 是元数据,不是运行时配置值。core-config 会把模块声明同步到 config_schema 表,后台可以据此展示分类、描述、默认值和敏感标记;真实配置值仍在 config_entry 中维护。

这意味着声明:

java
context.config().entry("spring.open.payment.default-provider", entry -> entry
        .type(ConfigValueTypes.STRING)
        .defaultValue("mock")
        .category(CATEGORY)
        .description("默认支付 provider"));

项目内 Provider 声明配置 schema 时,标准值类型统一使用 ConfigValueTypes 常量,例如 ConfigValueTypes.BOOLEANConfigValueTypes.INTEGERConfigValueTypes.DURATION。 模块内固定分类使用常量,优先放在 owning module 常量类;简单 starter 或单配置 Provider 也可以使用局部 CATEGORY 常量。类型和分类不要直接写裸字符串,描述面向后台配置中心展示, 必须使用中文业务语义。./.ai/scripts/check modules 会扫描生产 ServiceProvider 并拦截裸 .type("...")、裸 .category("...") 和不含中文的字面量 description。 defaultValue(...) 是具体业务默认值,优先使用模块已有 provider / status / key 常量,不需要为了数字或布尔值引入全局常量。

不会向 config_entry 写入 mock,也不会覆盖 .envapplication.yml 或数据库里已有的真实配置。

模块 manifest

manifest() 用于声明模块能力清单,只记录元数据,不直接写业务表。它适合把菜单、权限、调度、 事件监听、健康检查等“模块提供了什么”集中暴露给后续同步器或诊断页面:

java
protected void register() {
    manifest().permission("sample:order:view", "查看示例订单", "允许查看示例订单");
    manifest().menu("sample.orders", "示例订单", "sample", "/sample/orders", 100);
    manifest().schedule("sample.order-expire", "0 */5 * * * ?", "示例订单过期扫描");
}

Manifest 不是运行期配置,也不是自动执行器。权限 / 菜单 / 调度是否落库、如何启停、是否按租户隔离, 仍由对应 owning module 的同步逻辑负责。

框架内置的默认消费方是只读的 /actuator/lifecycle 端点:它把所有模块声明的 manifest 与 contribute(...) 贡献项聚合后暴露,作为诊断与内省入口,确保声明面始终有可观测的消费者。需要业务级落库(例如把 manifest 权限 / 菜单同步进 IAM)时,由 owning module 注入 ModuleManifestRegistry 在 boot 阶段按自身规则消费, 不在底座 starter 内耦合具体业务表。继承 ServiceProviderSupport 的 Provider 用 contribute(type, name, value) 声明贡献项,直接实现 ServiceProvider 的 Provider 用 context.contribute(...),两者等价。

服务容器边界

ServiceContainer 是 Spring BeanFactory 之上的门面,提供模块声明阶段更简洁的绑定 API:

方法说明
bind声明普通 BeanDefinition;默认同类型已有 Bean 时 fail-fast
singleton声明单例 BeanDefinition;默认同类型已有 Bean 时 fail-fast
bindIfMissing / singletonIfMissing仅缺少同类型 Bean 时绑定
replace显式替换现有同类型 BeanDefinition;只用于明确覆盖
instance把已有实例声明为单例 Bean
命名绑定bind("name", ...) / singleton("name", ...) 允许同类型多个实现
tag / tagBean / tagged给类型或 Bean 名打标签并按标签聚合
resolveboot 阶段解析 Bean
contains判断声明或 Bean 是否存在

启动完成后不要用它替换核心 Spring Bean。需要运行期变化时,优先通过 Provider、Registry、配置刷新或事件实现。

默认 bind / singleton 不会静默生成 #1 之类的重复 Bean 名,也不会在同类型已有 Bean 时悄悄覆盖。需要弱依赖用 bindIfMissing,需要多个实现用命名绑定,确实要覆盖时显式使用 replace。这能让开发期尽早暴露装配歧义,生产启动也更可预测。

Released under the Apache License 2.0.