Spring 最佳实践:用 ObjectProvider 做可选注入,用 @Lazy 优化初始化
本文针对医院系统中常见的多院区部署和功能开关问题,提出两种Spring依赖注入优化方案。对于必选但初始化较重的组件(如医保结算SDK),建议使用@Lazy注解将初始化推迟到首次使用时;对于可选功能或存在多实现的场景(如报表导出器),推荐采用ObjectProvider实现运行时动态选择。文章通过挂号、报表和结算三个典型场景,详细对比了普通注入的错误用法与优化方案,并给出四条选择建议:必选不重用普通
文章目录
医院系统(挂号/结算/报表/EMR)常见形态:多院区部署 + 多厂商对接 + 多功能开关。
常见两类痛点:
1)启动慢:医保/支付/字典等初始化太重
2)启动挂:某院区不开某功能(报表/审计/监管),强依赖注入导致No qualifying bean本文用“挂号 + 报表 + 结算”小案例,讲清
@Lazy与ObjectProvider的最佳使用姿势。
一、先给结论:两者解决的问题不同
1、@Lazy:解决启动慢(依赖一定存在)
适用对象:必选但很重的组件,例如:
- 医保结算 SDK(证书/码表/鉴权/连接)
- 大字典加载(药品/诊疗/医保目录)
一句话:把 Bean 的创建从“启动时”推迟到“第一次使用时”。
2、ObjectProvider:解决可选/多实现(依赖可能没有或多个)
适用对象:插件式能力,例如:
- 报表导出器(CSV/JSON/Excel/PDF)
- 操作审计/留痕(有的院区强制,有的暂不启用)
一句话:注入“提供者”,运行时决定“要不要用/用哪个/拿不到怎么降级”。
常用方法:
getIfAvailable():可选(无 Bean 返回null)stream():多实现选择getObject():强制必须有(可选场景一般不推荐)
二、经典坑:普通注入为什么会启动失败?
1、场景:某院区不开报表导出(Bean 不创建)
用开关控制导出器是否创建:
@Component
@ConditionalOnProperty(name="report.csv.enabled", havingValue="true")
public class CsvReportExporter implements ReportExporter { }
某院区配置:
report.csv.enabled=false
此时容器里没有 ReportExporter。
2、错误写法:强依赖导致启动挂
@Service
public class BadReportService {
private final ReportExporter exporter;
public BadReportService(ReportExporter exporter) {
this.exporter = exporter;
}
}
为什么挂?
Spring 创建 BadReportService 时必须注入 ReportExporter,但容器里没有 -> 启动失败。
典型报错:
No qualifying bean of type ‘xxx.ReportExporter’ available
三、正确姿势:用 ObjectProvider 做可选模块
1、挂号:可选审计留痕(getIfAvailable())
目标:
- 有审计:写日志
- 无审计:跳过,但挂号必须成功
@Service
public class RegistrationService {
private final ObjectProvider<AuditReporter> auditProvider;
public RegistrationService(ObjectProvider<AuditReporter> auditProvider) {
this.auditProvider = auditProvider;
}
public void register(String patientName, String dept) {
// 挂号主流程...
AuditReporter reporter = auditProvider.getIfAvailable();
if (reporter != null) {
reporter.report("REGISTER", patientName + "," + dept);
}
}
}
关键点:即使没有 AuditReporter,RegistrationService 也能创建;运行时拿不到就返回 null,业务自己降级。
2、报表:多实现导出器选择(stream())
目标:
- 0 个实现:提示“未启用”
- 多个实现:按
format选择
@Service
public class ReportService {
private final ObjectProvider<ReportExporter> exporters;
public ReportService(ObjectProvider<ReportExporter> exporters) {
this.exporters = exporters;
}
public String export(String format) {
var all = exporters.stream().toList();
if (all.isEmpty()) return "导出未启用";
var chosen = all.stream()
.filter(e -> e.format().equalsIgnoreCase(format))
.findFirst()
.orElse(all.get(0));
return chosen.export();
}
}
收益:
- 功能不开不影响启动
- 新增 Excel/PDF 导出器只需新增实现类 + 配置开关
- 多厂商/多策略可运行时选择
四、结算提速:用 @Lazy 延迟初始化医保/支付
1、结算场景(必选但重)
目标:系统启动不初始化医保 SDK,结算时再初始化。
@Service
public class SettlementService {
private final NhsaClient nhsaClient;
public SettlementService(@Lazy NhsaClient nhsaClient) {
this.nhsaClient = nhsaClient;
}
public String settle(String patientId) {
return nhsaClient.settle(patientId);
}
}
注意:
@Lazy不会降低初始化成本,只是把成本挪到第一次调用- 窗口第一笔怕慢,可在启动后后台预热一次(视业务要求)
五、怎么选(记住这 4 条就够了)
1、必选 + 不重
- 普通构造注入
2、必选 + 很重 + 想晚点初始化
@Lazy
3、可选(可能不存在)
ObjectProvider#getIfAvailable()
4、多实现(多厂商/多格式/多策略)
ObjectProvider#stream()
六、落地建议(挂号/报表/结算)
1、挂号主流程
- 核心链路用强依赖(数据库、核心服务)
- 审计/短信/随访等做可选插件(ObjectProvider)
2、报表模块
- 导出器插件化(CSV/JSON/Excel/PDF)
- 无实现时返回提示,不要抛 500
3、结算模块
- 医保/支付客户端
@Lazy推迟初始化 - 必要时做预热,避免窗口首单卡顿
七、总结
@Lazy:帮你“减启动压力”(必选重依赖延迟初始化)ObjectProvider:帮你“抗院区差异”(可选能力、多实现动态选择)
更多推荐



所有评论(0)