医院系统(挂号/结算/报表/EMR)常见形态:多院区部署 + 多厂商对接 + 多功能开关
常见两类痛点:
1)启动慢:医保/支付/字典等初始化太重
2)启动挂:某院区不开某功能(报表/审计/监管),强依赖注入导致 No qualifying bean

本文用“挂号 + 报表 + 结算”小案例,讲清 @LazyObjectProvider 的最佳使用姿势。


一、先给结论:两者解决的问题不同

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);
    }
  }
}

关键点:即使没有 AuditReporterRegistrationService 也能创建;运行时拿不到就返回 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:帮你“抗院区差异”(可选能力、多实现动态选择)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐