生命周期与服务容器
受众:开发者 摘要:模块
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(...) |
| 模块需要声明配置 schema | 在 register() 中调用 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 示例:
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 或测试特殊加载路径时,可以使用:
com.springopen.starter.lifecycle.ServiceProvider=\
com.example.sample.SampleServiceProvider项目内新模块默认使用 @AutoService,不要手写 lifecycle spring.factories。
如果希望更接近 Laravel 的短写法,可以继承 ServiceProviderSupport。它只是对完整 ServiceProvider API 的便捷封装,不改变生命周期边界:
@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 的条件装配、排序、排除和过滤机制。
@Override
public void configure(AutoConfigurationRegistry registry) {
registry.imports(
SampleAutoConfiguration.class,
SampleWebAutoConfiguration.class);
}configure() 只声明 @AutoConfiguration 类,不读取运行时配置、不访问数据库、不注册 BeanDefinition。服务绑定、配置 schema 和贡献项仍放在 register();依赖运行时配置或数据库的启动逻辑仍放在 boot()。
低层 data-redis、cache、config 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:
ModuleDescriptor.of(SampleServiceProvider.class)
.dependsOn(ConfigServiceProvider.class)
.dependsOn(PaymentServiceProvider.class);如果 provider 不在标准 Maven 输出目录、测试需要构造虚拟模块,或当前模块不应直接依赖对方 provider class,可以使用显式字符串重载:
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-toolkitspring-open-starter-commonspring-open-starter-data-redisspring-open-starter-cachespring-open-starter-configspring-open-starter-lifecycle
通用 Provider 只描述模块身份和生命周期顺序,不需要为了“看起来完整”声明空配置 schema 或空扩展点。只有模块确实提供可配置项、服务绑定或标签聚合时,才在 register() 中声明对应内容。
register 阶段
register() 是声明阶段,只允许:
- 声明服务绑定、单例绑定和条件绑定。
- 声明配置 schema。
- 声明标签和贡献项。
不要在 register() 中读取运行时配置、访问业务数据库、启动线程、注册调度或发事件。此时 Spring 上下文还没有完全就绪,过早读取配置容易绕过数据库覆盖或触发循环依赖。
boot 阶段
boot() 在 Spring 单例初始化后执行。此时可以:
- 读取
ConfigManager。 - 通过
ServiceContainer解析 Bean。 - 同步配置元数据。
- 启动幂等的监听、刷新或调度注册逻辑。
失败默认 fail-fast;如果希望记录失败但继续启动,可配置:
spring:
open:
lifecycle:
fail-fast: falseready 阶段
ready() 在应用完全就绪(ApplicationReadyEvent,Web 容器已可对外服务)后按 boot 顺序执行,适合放启动收尾动作:
- 预热缓存、编译模板等首请求加速。
- 补扫孤儿数据、对账修复等就绪后一次性任务。
- 对注册中心 / 外部系统宣告上线。
不要再为这类需求手写 ApplicationReadyEvent 监听器——直接覆写 Provider 的 ready(BootContext),可获得统一顺序、阶段报告和 /actuator/lifecycle 可观测性。
实现要求幂等。应用此时已对外服务,因此 ready 失败一律软处理:写入 LifecycleReport 并输出 WARN,不会中断应用(不受 fail-fast 影响)。
@Override
public void ready(BootContext context) {
context.bean(SearchIndexWarmer.class).warmUp();
}继承 ServiceProviderSupport 时也可以写短入口,底座会检测子类是否真正声明了该 hook,未声明的子类不会产生 no-op 报告:
@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 不受该开关影响,始终输出:
spring:
open:
lifecycle:
report-enabled: falseLifecycleReport 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 中,需要时显式开放:
management:
endpoints:
web:
exposure:
include: [health, lifecycle]配置 schema
配置 schema 是元数据,不是运行时配置值。core-config 会把模块声明同步到 config_schema 表,后台可以据此展示分类、描述、默认值和敏感标记;真实配置值仍在 config_entry 中维护。
这意味着声明:
context.config().entry("spring.open.payment.default-provider", entry -> entry
.type(ConfigValueTypes.STRING)
.defaultValue("mock")
.category(CATEGORY)
.description("默认支付 provider"));项目内 Provider 声明配置 schema 时,标准值类型统一使用 ConfigValueTypes 常量,例如 ConfigValueTypes.BOOLEAN、ConfigValueTypes.INTEGER、ConfigValueTypes.DURATION。 模块内固定分类使用常量,优先放在 owning module 常量类;简单 starter 或单配置 Provider 也可以使用局部 CATEGORY 常量。类型和分类不要直接写裸字符串,描述面向后台配置中心展示, 必须使用中文业务语义。./.ai/scripts/check modules 会扫描生产 ServiceProvider 并拦截裸 .type("...")、裸 .category("...") 和不含中文的字面量 description。 defaultValue(...) 是具体业务默认值,优先使用模块已有 provider / status / key 常量,不需要为了数字或布尔值引入全局常量。
不会向 config_entry 写入 mock,也不会覆盖 .env、application.yml 或数据库里已有的真实配置。
模块 manifest
manifest() 用于声明模块能力清单,只记录元数据,不直接写业务表。它适合把菜单、权限、调度、 事件监听、健康检查等“模块提供了什么”集中暴露给后续同步器或诊断页面:
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 名打标签并按标签聚合 |
resolve | boot 阶段解析 Bean |
contains | 判断声明或 Bean 是否存在 |
启动完成后不要用它替换核心 Spring Bean。需要运行期变化时,优先通过 Provider、Registry、配置刷新或事件实现。
默认 bind / singleton 不会静默生成 #1 之类的重复 Bean 名,也不会在同类型已有 Bean 时悄悄覆盖。需要弱依赖用 bindIfMissing,需要多个实现用命名绑定,确实要覆盖时显式使用 replace。这能让开发期尽早暴露装配歧义,生产启动也更可预测。