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

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

你正给后台加个报表导出:用户点一下,PDF、Excel、CSV 随便选。三个实现你全备齐了:

java 复制代码
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

然后随手一注:

kotlin 复制代码
@Service
public class ReportService {
    @Autowired
    private ReportExporter exporter;   // 容器里仨,要哪个?
}

启动,啪------

yaml 复制代码
NoUniqueBeanDefinitionException: No qualifying bean of type 'ReportExporter' available:
expected single matching bean but found 3: csvExporter, excelExporter, pdfExporter

一个接口仨实现,Spring 懵了:你到底要哪个?

18 篇聊注入的"方式"(用哪种),这篇聊注入的"匹配"(注入哪个)

  1. 一个接口多个实现,Spring 凭什么选?
  2. 能不能一次把所有实现都拿到?
  3. 有些依赖"可能有也可能没有",怎么不报错?

一、一个接口多个实现,Spring 听谁的

容器里有三个 ReportExporter,Spring 不知道选哪个。三个解法,各有脾气。

解法一:@Primary------"有多个时,默认选我"

less 复制代码
@Service
@Primary
public class ExcelExporter implements ReportExporter { ... }

@Primary 标在某个实现上,意思是"有多个候选时,默认选我"。这下 @Autowired ReportExporter 不报错了,注入的是 ExcelExporter

适合场景:大多数情况都用某一个实现,偶尔切换。比如默认导出 Excel,个别要 PDF 的再单独点名。

解法二:@Qualifier------"我点名要这个"

less 复制代码
@Service
public class ReportService {
    @Autowired
    @Qualifier("pdfExporter")
    private ReportExporter exporter;
}

@Qualifier 跟在 @Autowired 后面,按 Bean 名字精确指定。比 @Primary 更明确------你要谁就是谁。代价是耦合到了具体的 Bean 名字上,Bean 一改名,注入也得跟着改。

适合场景:这个字段就是要那个特定的实现,没得商量。

解法三:@Resource------"我按名字找"

kotlin 复制代码
@Service
public class ReportService {
    @Resource(name = "pdfExporter")
    private ReportExporter exporter;
}

@Resource 是 JSR-250 标准注解(不是 Spring 的),匹配规则是先按名字,再按类型 。你可以理解成"加强版的 @Autowired + @Qualifier"。

那它和 @Autowired 到底差在哪?

对比维度 @Autowired @Resource
来源 Spring 自带 JSR-250 标准(Java 规范)
默认匹配 按类型 按名字
找不到时 报错(除非 required=false) 回退按类型
能用在构造器上吗 不能(Spring 只支持字段/setter 上的 @Resource)
推荐场景 构造器注入配它,或字段/setter 字段/setter 注入按名字找

Spring 到底怎么选的?

候选有多个时,Spring 按下面这个顺序逐级往下试,命中哪级就用哪级:

试到哪级 Spring 问自己 命中就
你标了 @Qualifier 点名? 注入它指定的
有 @Primary 实现吗? 注入那个
字段名恰好等于某个 Bean 名? 按名字注入
都不中 报 NoUniqueBeanDefinitionException

@Qualifier 居首,@Primary 次之,"凑名字"殿后。

二、一次注入全部实现:List 和 Map

换个思路------多个实现,干嘛非得选一个?全都要。

还记得第 14 篇《用 SpringBean 实现多态、告别 if-else》吗?那里用到一个技巧:把多个实现一次性注入成集合。这里正式讲清楚。

注入 List:拿到所有实现

arduino 复制代码
@Service
public class ReportService {

    private final List<ReportExporter> exporters;

    public ReportService(List<ReportExporter> 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 看到你要 List<ReportExporter>,会把容器里所有 ReportExporter 类型的 Bean 收集起来,按顺序塞进 List。你哪天加个 HtmlExporter,啥都不用改,自动进 List。

这是策略模式最优雅的 Spring 实现------注册即生效,零配置

注入 Map:按 Bean 名字索引

更进一步的玩法:

typescript 复制代码
@Service
public class ReportService {

    private final Map<String, ReportExporter> exporterMap;

    public ReportService(Map<String, 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);
    }
}

Map<String, ReportExporter> 的 key 是 Bean 名字(默认类名首字母小写),value 是 Bean 实例。这样就能按名字 O(1) 直接查找,不用循环------用户传 format=pdfget("pdfExporter") 直接拿到。

注入方式 key 是什么 适合场景
List<接口> 顺序索引 需要遍历所有实现、按条件分发
Map<String, 接口> Bean 名字 需要按名字精准定位某个实现

这一招结合第 14 篇的多态玩法,能干掉业务代码里几乎所有的 if-else 分支。

三、兜底三件套:@Lazy、ObjectProvider、Optional

前面都是"依赖一定存在"的情况。可现实里有些依赖可能有、也可能没有------比如通知服务,配了就发、没配就跳过。这时候硬注入会报错。三个兜底工具,挑一个用。

@Lazy:循环依赖的创可贴

第 17 篇聊循环依赖时说过,构造器循环依赖是无解的------除非上 @Lazy

less 复制代码
@Service
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 原生

ObjectProvider<T> 是 Spring 提供的延迟获取容器:

java 复制代码
@Service
public class OrderService {

    private final ObjectProvider<NotificationService> notificationProvider;

    public OrderService(ObjectProvider<NotificationService> notificationProvider) {
        this.notificationProvider = notificationProvider;
    }

    public void placeOrder(Order order) {
        saveOrder(order);
        // 容器里没有 NotificationService 也不报错
        notificationProvider.ifAvailable(ns -> ns.notify(order));
    }
}

它一手解决两件事:

  1. 延迟获取------构造时只拿到一个 Provider,真正用时才查容器
  2. 可选依赖 ------用 ifAvailable / getIfAvailable,找不到也不报错

@Autowired(required = false) 优雅的地方在于:它把"可选"这件事显式表达在类型上------光看构造器签名,你就知道这个依赖是 optional 的,不用翻注解。

Optional:Java 8 风格的可选注入

从 Spring 4.3 起,构造器里还能直接注入 Optional

kotlin 复制代码
@Service
public class OrderService {

    private final Optional<NotificationService> notificationService;

    public OrderService(Optional<NotificationService> notificationService) {
        this.notificationService = notificationService;
    }
}

语义和 ObjectProvider 类似,但用的是 JDK 自带的 java.util.Optional。哪个顺眼,看个人喜好------语义上都清楚。

三个怎么选

工具 解决什么 典型场景
@Lazy 延迟到使用时才真正创建/查找 打破构造器循环依赖
ObjectProvider 延迟 + 可选,Spring 原生 可选依赖、延迟获取
Optional 可选,JDK 原生 可选依赖,语义清晰

一句话体会:这三个都是兜底,不是日常。

四、总结

如果系统永远只导出一种格式,给那个实现标个 @Primary 就完事了。但对产品来说,固定一种格式太单一------今天要 Excel,明天加 PDF,后天又要 CSV,每加一种都得改代码、重新部署,扩展性太差。

真正"适配多场景"的正解,落回第二章那个 Map 注入 :按前端传进来的 format 参数,直接从 Map 里取对应的导出器------

ini 复制代码
ReportExporter e = exporterMap.get(format + "Exporter");   // format=pdf -> pdfExporter

哪天产品要加 HTML 导出?写个 HtmlExporter 扔进容器,ReportService 一行都不用改,自动生效。注册即生效、按需路由------这才是"适配多场景"该有的样子。

回过头看,一个接口多个实现从来不是 Spring 的 bug,而是它给你留的多场景适配口子。至于 @LazyObjectProvider 这些兜底工具,留给真撞墙的时候;日常依赖老老实实写在构造器上、让它"必须存在",是便宜得多的保险。

相关推荐
花开彼岸天~5 小时前
鸿蒙原生开发手记:徒步迹 - 自定义组件开发规范
后端·华为·harmonyos·鸿蒙系统
songroom6 小时前
Kimi K3:Rust封装XTP接口详细教程实践
开发语言·后端·rust
kebeiovo6 小时前
游戏服务端开发:Actor模型详解(Go语言)
开发语言·后端·golang
红烧大青虫6 小时前
setInterval 倒计时实现:60s 验证码发送逻辑
后端·华为·harmonyos·鸿蒙系统
小村儿7 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
你驴我7 小时前
WhatsApp 多账号下消息已读回执的实时聚合与推送实践
后端·python
用户208046804567 小时前
Python3 注释编写完全指南:从基础规范到高效实践
后端
苏三说技术7 小时前
Jackson3来了,变化真大!
后端
Larcher8 小时前
LangChain RAG 排错实录:.env 为什么没有生效
vue.js·后端