一个接口多个实现,Spring 怎么“适配多场景“?

发布时间:2026/7/21 19:23:06
一个接口多个实现,Spring 怎么“适配多场景“? 一个接口多个实现Spring 怎么”适配多场景”你正给后台加个报表导出用户点一下PDF、Excel、CSV 随便选。三个实现你全备齐了public interface ReportExporter { boolean supports(String format); // 我支持啥格式 byte[] export(List rows); // 导出 } Service public class PdfExporter implements ReportExporter { ... } // PDF Service public class ExcelExporter implements ReportExporter { ... } // Excel Service public class CsvExporter implements ReportExporter { ... } // CSV然后随手一注Service public class ReportService { Autowired private ReportExporter exporter; // 容器里仨要哪个 }启动啪——NoUniqueBeanDefinitionException: No qualifying bean of type ReportExporter available: expected single matching bean but found 3: csvExporter, excelExporter, pdfExporter一个接口仨实现Spring 懵了你到底要哪个18 篇聊注入的”方式”用哪种这篇聊注入的”匹配”注入哪个一个接口多个实现Spring 凭什么选能不能一次把所有实现都拿到有些依赖”可能有也可能没有”怎么不报错一、一个接口多个实现Spring 听谁的容器里有三个ReportExporterSpring 不知道选哪个。三个解法各有脾气。解法一Primary——”有多个时默认选我”Service Primary public class ExcelExporter implements ReportExporter { ... }Primary标在某个实现上意思是”有多个候选时默认选我”。这下Autowired ReportExporter不报错了注入的是ExcelExporter。适合场景大多数情况都用某一个实现偶尔切换。比如默认导出 Excel个别要 PDF 的再单独点名。解法二Qualifier——”我点名要这个”Service public class ReportService { Autowired Qualifier(pdfExporter) private ReportExporter exporter; }Qualifier跟在Autowired后面按 Bean 名字精确指定。比Primary更明确——你要谁就是谁。代价是耦合到了具体的 Bean 名字上Bean 一改名注入也得跟着改。适合场景这个字段就是要那个特定的实现没得商量。解法三Resource——”我按名字找”Service public class ReportService { Resource(name pdfExporter) private ReportExporter exporter; }Resource是 JSR-250 标准注解不是 Spring 的匹配规则是先按名字再按类型。你可以理解成”加强版的Autowired Qualifier“。那它和Autowired到底差在哪对比维度AutowiredResource来源Spring 自带JSR-250 标准Java 规范默认匹配按类型按名字找不到时报错除非 requiredfalse回退按类型能用在构造器上吗能不能Spring 只支持字段/setter 上的 Resource推荐场景构造器注入配它或字段/setter字段/setter 注入按名字找Spring 到底怎么选的候选有多个时Spring 按下面这个顺序逐级往下试命中哪级就用哪级试到哪级Spring 问自己命中就①你标了 Qualifier 点名注入它指定的②有 Primary 实现吗注入那个③字段名恰好等于某个 Bean 名按名字注入都不中报 NoUniqueBeanDefinitionExceptionQualifier居首Primary次之”凑名字”殿后。二、一次注入全部实现List 和 Map换个思路——多个实现干嘛非得选一个全都要。还记得第 14 篇《用 SpringBean 实现多态、告别 if-else》吗那里用到一个技巧把多个实现一次性注入成集合。这里正式讲清楚。注入 List拿到所有实现Service public class ReportService { private final ListReportExporter exporters; public ReportService(ListReportExporter exporters) { this.exporters exporters; } public byte[] export(String format, List rows) { for (ReportExporter e : exporters) { if (e.supports(format)) { return e.export(rows); } } throw new IllegalArgumentException(不支持的格式 format); } }Spring 看到你要ListReportExporter会把容器里所有ReportExporter类型的 Bean 收集起来按顺序塞进 List。你哪天加个HtmlExporter啥都不用改自动进 List。这是策略模式最优雅的 Spring 实现——注册即生效零配置。注入 Map按 Bean 名字索引更进一步的玩法Service public class ReportService { private final MapString, ReportExporter exporterMap; public ReportService(MapString, ReportExporter exporterMap) { this.exporterMap exporterMap; } public byte[] export(String format, List rows) { ReportExporter e exporterMap.get(format Exporter); // pdf - pdfExporter if (e null) { throw new IllegalArgumentException(不支持的格式 format); } return e.export(rows); } }MapString, ReportExporter的 key 是 Bean 名字默认类名首字母小写value 是 Bean 实例。这样就能按名字 O(1) 直接查找不用循环——用户传formatpdfget(pdfExporter)直接拿到。注入方式key 是什么适合场景List接口顺序索引需要遍历所有实现、按条件分发MapString, 接口Bean 名字需要按名字精准定位某个实现这一招结合第 14 篇的多态玩法能干掉业务代码里几乎所有的if-else分支。三、兜底三件套Lazy、ObjectProvider、Optional前面都是”依赖一定存在”的情况。可现实里有些依赖可能有、也可能没有——比如通知服务配了就发、没配就跳过。这时候硬注入会报错。三个兜底工具挑一个用。Lazy循环依赖的创可贴第 17 篇聊循环依赖时说过构造器循环依赖是无解的——除非上LazyService public class AService { private final BService bService; public AService(Lazy BService bService) { this.bService bService; } }Lazy的作用Spring 不真注入 BService而是注入它的代理对象。等 AService 真正调用bService的方法时代理才去容器里找真正的 Bean。这就打破了”构造 A 需要 B、构造 B 又需要 A”的死循环——A 构造时拿到的是 B 的代理占位B 可以照常构造。但记住Lazy是创可贴不是治本的药。循环依赖本身是设计问题最好的解法是第 17 篇说的——重新设计、用事件解耦。Lazy留给实在改不动的老代码。ObjectProvider延迟 可选Spring 原生ObjectProviderT是 Spring 提供的延迟获取容器Service public class OrderService { private final ObjectProviderNotificationService notificationProvider; public OrderService(ObjectProviderNotificationService notificationProvider) { this.notificationProvider notificationProvider; } public void placeOrder(Order order) { saveOrder(order); // 容器里没有 NotificationService 也不报错 notificationProvider.ifAvailable(ns - ns.notify(order)); } }它一手解决两件事延迟获取——构造时只拿到一个 Provider真正用时才查容器可选依赖——用ifAvailable/getIfAvailable找不到也不报错比Autowired(required false)优雅的地方在于它把”可选”这件事显式表达在类型上——光看构造器签名你就知道这个依赖是 optional 的不用翻注解。OptionalJava 8 风格的可选注入从 Spring 4.3 起构造器里还能直接注入OptionalService public class OrderService { private final OptionalNotificationService notificationService; public OrderService(OptionalNotificationService notificationService) { this.notificationService notificationService; } }语义和ObjectProvider类似但用的是 JDK 自带的java.util.Optional。哪个顺眼看个人喜好——语义上都清楚。三个怎么选工具解决什么典型场景Lazy延迟到使用时才真正创建/查找打破构造器循环依赖ObjectProviderT延迟 可选Spring 原生可选依赖、延迟获取OptionalT可选JDK 原生可选依赖语义清晰一句话体会这三个都是兜底不是日常。四、总结如果系统永远只导出一种格式给那个实现标个Primary就完事了。但对产品来说固定一种格式太单一——今天要 Excel明天加 PDF后天又要 CSV每加一种都得改代码、重新部署扩展性太差。真正”适配多场景”的正解落回第二章那个Map 注入按前端传进来的format参数直接从 Map 里取对应的导出器——ReportExporter e exporterMap.get(format Exporter); // formatpdf - pdfExporter哪天产品要加 HTML 导出写个HtmlExporter扔进容器ReportService一行都不用改自动生效。注册即生效、按需路由——这才是”适配多场景”该有的样子。回过头看一个接口多个实现从来不是 Spring 的 bug而是它给你留的多场景适配口子。至于Lazy、ObjectProvider这些兜底工具留给真撞墙的时候日常依赖老老实实写在构造器上、让它”必须存在”是便宜得多的保险。