Java依赖注入中带Impl实现类的核心实践与避坑指南
1. 从“java imple_带imple的依赖注入”这个标题说起第一次看到“java imple_带imple的依赖注入”这个标题我愣了几秒。imple这个前缀在 Java 里太常见了——implements、Implementation、Impl后缀几乎每个写 Java 的人每天都在跟它打交道。但把imple单独拎出来和“依赖注入”绑在一起讲这背后一定藏着某种特定的工程实践或者踩坑经验。我仔细琢磨了一下这个标题大概率指向的是这样一类场景在 Java 项目中我们定义接口然后用XxxImpl这样的类去实现它再通过依赖注入DI框架把这些实现类装配到需要它们的地方。听起来很基础对吧但恰恰是这种“基础到不能再基础”的东西在实际项目里翻车的概率高得离谱。比如同一个接口有多个Impl实现类Spring 不知道该注入哪一个启动直接报NoUniqueBeanDefinitionException又比如包扫描路径没配对Impl类根本没被注册成 Bean运行时报NoSuchBeanDefinitionException再比如循环依赖A 的Impl依赖 B 的ImplB 的Impl又反过来依赖 A 的Impl容器启动直接卡死。这篇文章就是要把这些围绕“接口 Impl 实现类 依赖注入”的典型问题掰开揉碎讲清楚。不管你是刚接触 Spring 的新手还是写了几年业务代码但一直没搞明白 DI 底层逻辑的老手都能从里面找到可以直接抄作业的方案。我会从设计思路、核心细节、实操过程、常见问题四个维度展开把“带 Impl 的依赖注入”这件事彻底讲透。2. 整体设计与思路拆解为什么接口和 Impl 要分开2.1 接口与实现分离的本质逻辑在 Java 工程里我们习惯性地定义一个UserService接口然后写一个UserServiceImpl去实现它。这个习惯不是凭空来的它背后有一套很实在的工程逻辑。最直接的原因是解耦。假设你的业务代码里直接new UserServiceImpl()那这个类就和调用方死死绑在一起了。哪天你想换一套实现——比如从本地缓存换成 Redis 缓存——你得把所有new UserServiceImpl()的地方全找出来改掉。但如果通过接口注入调用方只认UserService这个接口具体用的是哪个实现类由容器在启动时决定调用方完全无感知。第二个原因是可测试性。单元测试的时候你不想真的去连数据库、调远程服务这时候你可以写一个UserServiceMockImpl把它注入到被测类里测试就跑得飞快。如果没有接口这层抽象你很难做到这一点。第三个原因是AOP 代理的需要。Spring 的事务、缓存、日志这些横切关注点底层都是通过动态代理实现的。而 JDK 动态代理要求目标类必须实现至少一个接口。如果你的UserServiceImpl没有实现任何接口Spring 就只能退而求其次用 CGLIB 去生成子类代理。虽然现在 CGLIB 也很成熟了但在很多团队的技术规范里面向接口编程仍然是一条铁律。所以“接口 Impl”这个组合本质上是在为依赖注入提供一个“可替换的插槽”。容器需要知道当某个地方需要一个UserService的时候我该把哪个Impl塞进去。这个“塞”的过程就是依赖注入的核心动作。2.2 依赖注入的三种装配方式与选型考量Spring 提供了三种主要的依赖注入方式构造器注入、Setter 注入、字段注入。这三种方式在“带 Impl 的依赖注入”场景下各有优劣选错了会在后期维护时付出代价。构造器注入是我最推荐的方式。它的写法是把依赖通过构造方法的参数传进来Spring 在创建 Bean 的时候会自动解析这些参数并注入对应的实现类。这种方式的优势非常明显依赖关系在对象创建时就确定了对象一旦创建出来就是完整可用的状态不会出现“对象有了但依赖还是 null”的尴尬。而且构造器注入天然支持final关键字依赖字段不可变线程安全性更好。更重要的是如果你的Impl类依赖了五六个其他服务构造器参数列表会变得很长这其实是一个信号——告诉你这个类的职责太多了该拆了。Setter 注入适合可选依赖的场景。比如某个Impl类有一个可配置的缓存组件有就用没有就降级走默认逻辑这时候用 Setter 注入就比较合适。但 Setter 注入的问题是对象在构造完成后、Setter 被调用前处于一个“半成品”状态如果这时候被其他线程拿到引用可能会出问题。字段注入是写法最简洁的——直接在字段上标Autowired一行搞定。但它的缺点也最致命无法用final修饰依赖关系对外不可见单元测试时没法直接new出来传 mock 对象必须依赖反射或者 Spring 容器。很多团队现在已经明令禁止字段注入了新项目里尽量不要用。注意如果你用的是 Spring Boot 2.x 之后的版本官方文档已经明确建议优先使用构造器注入。这不是风格偏好问题而是实打实的工程最佳实践。2.3 多实现类场景下的策略选择当一个接口有多个Impl实现类时依赖注入就变得微妙起来。比如你定义了一个PaymentService接口然后有AlipayServiceImpl、WechatPayServiceImpl、BankCardServiceImpl三个实现类。这时候如果你在某个地方直接注入PaymentServiceSpring 会直接报错因为它不知道你要哪一个。解决这个问题有几种常见策略。第一种是用Primary注解标记一个“默认实现”当没有明确指定的时候就用它。第二种是用Qualifier注解在注入点指定 Bean 的名称精确制导。第三种是注入一个MapString, PaymentServiceSpring 会自动把所有实现类按 Bean 名称作为 key 塞进 Map 里你在运行时根据业务参数动态选择。第四种是注入ListPaymentService然后自己写逻辑去遍历匹配。这几种策略没有绝对的好坏关键看你的业务场景。如果有一个明显的默认实现用Primary最省事。如果需要根据用户选择的支付方式动态路由用Map注入最灵活。如果多个实现类需要按顺序依次尝试用List注入配合Order注解最合适。3. 核心细节解析与实操要点3.1 Bean 的注册与发现Impl 类是怎么被 Spring 找到的要让一个XxxImpl类被 Spring 管理起来第一步是让它被扫描到。Spring Boot 默认会扫描主启动类所在包及其子包下的所有类凡是标注了Component、Service、Repository、Controller这些注解的类都会被注册成 Bean。这里有一个非常容易踩的坑包路径。假设你的主启动类在com.example.demo包下但你有一个UserServiceImpl写在com.example.other包下那 Spring 默认是扫不到的。这时候你有两个选择要么把启动类挪到更顶层的包要么在启动类上显式加ComponentScan(basePackages {com.example.demo, com.example.other})。另一个坑是类名冲突。Spring 默认用类名首字母小写作为 Bean 名称。如果你有两个不同包下的UserServiceImpl类名一模一样Spring 会报ConflictingBeanDefinitionException。解决办法是给其中一个用Service(customName)显式指定 Bean 名称。还有一种情况是条件注册。比如你有一个CacheServiceImpl只在配置了 Redis 的环境下才需要注册。这时候可以用ConditionalOnProperty或者Profile来控制。我见过一个项目开发环境用的是本地缓存LocalCacheServiceImpl生产环境用的是RedisCacheServiceImpl通过Profile(prod)和Profile(!prod)来切换非常干净。3.2 注入点的匹配规则Spring 到底怎么选 Impl当 Spring 需要给一个注入点装配 Bean 的时候它的匹配逻辑大致是这样的第一步按类型匹配。Spring 会去找所有类型兼容的 Bean。比如注入点声明的是UserService那所有实现了UserService接口的 Bean 都会被列为候选。第二步如果有多个候选看有没有Primary。被Primary标记的那个会被优先选中。第三步如果还有多个候选看注入点上有没有Qualifier。Qualifier的值会和 Bean 的名称做匹配。第四步如果还没有确定尝试按字段名或参数名匹配 Bean 名称。比如字段名叫userService那 Bean 名称也是userService的那个会被选中。第五步如果还是确定不了直接抛NoUniqueBeanDefinitionException。这个匹配链条看起来清晰但实际项目中经常出问题的地方在于Bean 名称的生成规则。Spring 默认用类名的首字母小写作为 Bean 名称但有两个特殊情况。第一如果类名前两个字母都是大写比如URLServiceImpl那 Bean 名称就是URLServiceImpl而不是uRLServiceImpl。第二如果你用了Service不指定名称但类名是UserServiceImpl那 Bean 名称就是userServiceImpl而不是userService。很多人在Qualifier里写userService结果匹配不上就是因为这个。实操心得我个人的习惯是凡是接口有多个实现类的情况一律在Service里显式指定 Bean 名称比如Service(alipayPayment)然后在注入点用Qualifier(alipayPayment)精确指定。这样虽然多写几个字但永远不会因为命名规则的问题翻车。3.3 循环依赖Impl 之间互相引用怎么破循环依赖是依赖注入里最让人头疼的问题之一。典型场景是这样的OrderServiceImpl依赖UserService而UserServiceImpl又依赖OrderService。两个类互相注入Spring 在启动时就会陷入死循环。Spring 通过三级缓存机制来解决这个问题。简单来说当 Spring 创建OrderServiceImpl的时候发现它依赖UserService于是去创建UserServiceImpl。创建UserServiceImpl的时候又发现它依赖OrderService这时候 Spring 会从缓存里拿到OrderServiceImpl的早期引用还没完全初始化完的对象先注入进去让UserServiceImpl能完成创建。等UserServiceImpl创建完了再回头把OrderServiceImpl的依赖补全。但这个机制只对单例 Bean 的 Setter 注入或字段注入有效。如果你用的是构造器注入Spring 没法提前暴露一个“半成品”对象因为构造器还没执行完对象根本不存在。所以构造器注入的循环依赖是解不了的Spring 会直接报错。那怎么解决循环依赖呢最根本的办法是重构代码消除循环。循环依赖往往意味着两个类的职责边界不清晰把互相依赖的那部分逻辑抽到一个第三个类里让两个类都依赖这个新类循环就打破了。如果实在没法重构退而求其次的办法是在其中一个注入点上加Lazy注解让 Spring 注入一个代理对象真正调用的时候才去解析目标 Bean。或者把其中一个改为 Setter 注入给 Spring 三级缓存一个机会。注意Spring Boot 2.6 之后默认禁止了循环依赖如果你的项目升级后突然报BeanCurrentlyInCreationException不要慌先检查是不是有循环依赖然后按上面的思路去解。4. 实操过程与核心环节实现4.1 从零搭建一个带多 Impl 的依赖注入示例光说理论没意思我们直接上手搭一个完整的示例。假设我们要做一个消息通知模块支持邮件通知和短信通知两种方式。首先定义接口public interface NotificationService { void send(String target, String message); }然后写两个实现类Service(emailNotification) public class EmailNotificationServiceImpl implements NotificationService { Override public void send(String target, String message) { System.out.println(发送邮件到 target 内容 message); } } Service(smsNotification) public class SmsNotificationServiceImpl implements NotificationService { Override public void send(String target, String message) { System.out.println(发送短信到 target 内容 message); } }接下来是使用方。假设我们有一个AlertService需要根据配置决定用哪种通知方式Service public class AlertService { private final MapString, NotificationService notificationMap; public AlertService(MapString, NotificationService notificationMap) { this.notificationMap notificationMap; } public void alert(String channel, String target, String message) { NotificationService service notificationMap.get(channel); if (service null) { throw new IllegalArgumentException(不支持的通知渠道: channel); } service.send(target, message); } }这里用构造器注入了一个MapString, NotificationServiceSpring 会自动把所有NotificationService类型的 Bean 塞进去key 就是 Bean 名称。这样AlertService完全不需要知道具体有哪些实现类新增一种通知方式只需要加一个Impl类AlertService的代码一行都不用改。如果你不想用 Map也可以注入ListNotificationService然后给每个实现类加Order注解来控制顺序Service Order(1) public class EmailNotificationServiceImpl implements NotificationService { ... } Service Order(2) public class SmsNotificationServiceImpl implements NotificationService { ... }然后在AlertService里注入ListNotificationService遍历调用。这种方式适合“依次尝试所有渠道直到成功”的场景。4.2 用 Qualifier 精确指定实现类的完整流程有些场景下你不需要动态路由就是要在编译期确定用哪个实现类。这时候Qualifier是最直接的工具。假设我们有一个ReportGenerator接口有两个实现PdfReportGeneratorImpl和ExcelReportGeneratorImpl。在某个特定的服务里我们明确要用 PDF 格式Service public class MonthlyReportService { private final ReportGenerator reportGenerator; public MonthlyReportService( Qualifier(pdfReportGenerator) ReportGenerator reportGenerator) { this.reportGenerator reportGenerator; } public void generate() { reportGenerator.generate(); } }对应的实现类Service(pdfReportGenerator) public class PdfReportGeneratorImpl implements ReportGenerator { ... } Service(excelReportGenerator) public class ExcelReportGeneratorImpl implements ReportGenerator { ... }这里的关键点是Qualifier里的名称必须和Service里指定的名称完全一致。如果Service没指定名称那就用默认的类名首字母小写。我建议统一显式指定避免记错。还有一种更简洁的写法如果实现类的 Bean 名称和注入点的参数名一致Spring 会自动匹配连Qualifier都不用写。比如参数名叫pdfReportGeneratorBean 名称也是pdfReportGenerator那就直接注入了。但这种依赖命名巧合的做法我不推荐代码可读性差别人看了不知道你是故意的还是碰巧的。4.3 条件化注入根据环境动态选择 Impl实际项目里经常需要根据运行环境选择不同的实现。比如开发环境用 Mock 实现测试环境用内存实现生产环境用真实实现。Spring 提供了几种条件化注入的机制。第一种是Profile。你可以给不同的实现类标注不同的 ProfileService Profile(dev) public class MockUserServiceImpl implements UserService { ... } Service Profile(prod) public class RealUserServiceImpl implements UserService { ... }然后在配置文件里指定spring.profiles.activedev或prodSpring 就只会注册对应 Profile 的 Bean。第二种是ConditionalOnProperty。这个更灵活可以根据配置项的值来决定是否注册Service ConditionalOnProperty(name app.cache.type, havingValue redis) public class RedisCacheServiceImpl implements CacheService { ... } Service ConditionalOnProperty(name app.cache.type, havingValue local, matchIfMissing true) public class LocalCacheServiceImpl implements CacheService { ... }matchIfMissing true表示如果配置项不存在默认注册这个 Bean。这样即使配置文件里没写app.cache.type系统也能正常启动用的是本地缓存。第三种是自定义Conditional注解。这个适合更复杂的条件判断比如根据某个类是否存在、根据某个 Bean 是否已经注册等。实现方式是自己写一个类实现Condition接口在matches方法里写判断逻辑。这个用法相对高级一般项目里用前两种就够了。实操心得条件化注入最大的坑是“条件不满足导致没有 Bean 可用”。比如你写了两个实现类一个Profile(dev)一个Profile(prod)结果启动时没指定 Profile两个都不注册注入点直接报NoSuchBeanDefinitionException。我的习惯是至少给一个实现类加matchIfMissing true或者用Profile(!prod)这种反向条件保证任何环境下都有兜底。5. 常见问题与排查技巧实录5.1 NoUniqueBeanDefinitionException 的排查与解决这个异常是“带 Impl 的依赖注入”里出现频率最高的。报错信息通常是这样的NoUniqueBeanDefinitionException: No qualifying bean of type com.example.UserService available: expected single matching bean but found 2: userServiceImpl,adminUserServiceImpl看到这个报错说明你的注入点声明的是接口类型但容器里有两个或更多实现类Spring 不知道选哪个。排查步骤很简单先看报错信息里列出的候选 Bean 名称确认是不是确实有多个实现类。然后决定用哪种策略解决场景推荐方案具体操作有一个明显的默认实现Primary在默认实现类上加Primary每个注入点明确知道要用哪个Qualifier在注入点加Qualifier(beanName)需要运行时动态选择注入Map或List构造器注入MapString, T多个实现需要按顺序执行OrderList实现类加Order注入ListT我个人的经验是如果一个接口有多个实现类优先考虑用Map注入因为这种方式最灵活新增实现类不需要改任何现有代码。只有在确实需要固定使用某一个实现的时候才用Qualifier。5.2 NoSuchBeanDefinitionException 的常见原因这个异常的意思是“找不到符合条件的 Bean”。和上一个异常正好相反一个是太多一个是没有。常见原因有这几个包扫描路径不对。这是最常见的。你的Impl类写在了启动类扫描范围之外的包Spring 根本没发现它。解决办法是调整包结构或者在启动类上加ComponentScan。忘记加注解。Impl类上没写Service、Component这些注解Spring 不会把它注册成 Bean。这个错误在新手身上特别常见因为 IDE 不会提示你。条件化注解没满足。比如ConditionalOnProperty的条件不成立或者Profile和当前激活的 Profile 不匹配。这时候需要检查配置文件里的相关配置项。Bean 名称冲突导致覆盖。Spring Boot 2.1 之后默认禁止 Bean 定义覆盖如果两个 Bean 名称相同后注册的会被跳过导致原本期望的 Bean 不存在。解决办法是给其中一个改名字或者在配置文件里开启覆盖spring.main.allow-bean-definition-overridingtrue。但我不建议开启覆盖这会让问题更难排查。泛型类型不匹配。如果你注入的是RepositoryUser但实际注册的是RepositoryOrderSpring 在解析泛型时会匹配失败。这种问题比较隐蔽需要仔细看泛型参数。5.3 循环依赖的三种破解思路与实操循环依赖的报错信息通常是BeanCurrentlyInCreationException: Error creating bean with name orderServiceImpl: Requested bean is currently in creation: Is there an unresolvable circular reference?破解思路一重构消除循环。这是最根本的解决办法。把两个类互相依赖的那部分逻辑抽出来放到第三个类里。比如OrderServiceImpl和UserServiceImpl互相依赖往往是因为订单逻辑里需要查用户信息用户逻辑里又需要查订单信息。可以把“查用户信息”和“查订单信息”都抽到一个QueryService里两个Impl都依赖QueryService循环就打破了。破解思路二用 Lazy 打破初始化时的循环。在其中一个注入点上加LazyService public class OrderServiceImpl implements OrderService { private final UserService userService; public OrderServiceImpl(Lazy UserService userService) { this.userService userService; } }Lazy会让 Spring 注入一个代理对象真正调用userService的方法时才去解析目标 Bean。这样初始化阶段就不会形成循环了。破解思路三改用 Setter 注入。如果你用的是构造器注入导致的循环依赖可以尝试把其中一个改为 Setter 注入。Spring 的三级缓存对 Setter 注入是有效的能解决一部分循环依赖问题。但这不是长久之计还是建议尽量重构。注意Spring Boot 2.6 及以上版本默认禁止循环依赖。如果你的项目是从低版本升级上来的升级后可能会突然报循环依赖错误。这时候不要急着在配置文件里开启spring.main.allow-circular-referencestrue而是应该借这个机会把循环依赖清理掉。开启配置只是把问题藏起来后面会以更隐蔽的方式爆发。5.4 常见问题速查表问题现象可能原因排查方法解决方案NoUniqueBeanDefinitionException多个 Impl 实现同一接口看报错信息里的候选 Bean 列表用 Primary、Qualifier 或 Map 注入NoSuchBeanDefinitionExceptionImpl 未被扫描到检查包路径和类注解调整包结构或加 ComponentScanBeanCurrentlyInCreationException循环依赖看报错信息里的 Bean 名称链重构、Lazy 或改 Setter 注入Bean 名称冲突两个 Impl 类名相同看启动日志里的覆盖警告用 Service(name) 显式命名注入的 Bean 为 null字段注入但对象是 new 出来的检查对象创建方式改为从容器获取或构造器注入Qualifier 不生效Bean 名称写错打印容器里所有 Bean 名称确认 Service 指定的名称6. 进阶技巧与工程化建议6.1 用自定义注解简化多实现类的选择当你的系统里有大量“一个接口多个实现”的场景时每个注入点都写Qualifier会很啰嗦。这时候可以考虑自定义一个注解来简化。比如你有一个支付渠道的选择逻辑可以定义一个PaymentChannel注解Target({ElementType.FIELD, ElementType.PARAMETER}) Retention(RetentionPolicy.RUNTIME) Qualifier public interface PaymentChannel { String value(); }然后在实现类上标注Service PaymentChannel(alipay) public class AlipayServiceImpl implements PaymentService { ... } Service PaymentChannel(wechat) public class WechatPayServiceImpl implements PaymentService { ... }注入的时候就可以用public OrderService(PaymentChannel(alipay) PaymentService paymentService) { this.paymentService paymentService; }这样语义更清晰而且PaymentChannel这个注解本身也起到了文档的作用一看就知道这里注入的是支付渠道相关的实现。6.2 用工厂模式配合依赖注入管理复杂选择逻辑如果选择实现类的逻辑比较复杂——比如需要根据用户等级、订单金额、活动类型等多个维度来决定用哪个实现——那把这些逻辑写在注入点里就不合适了。更好的做法是用工厂模式。定义一个PaymentServiceFactory注入MapString, PaymentService然后对外提供一个getService(PaymentContext context)方法在里面写选择逻辑Service public class PaymentServiceFactory { private final MapString, PaymentService serviceMap; public PaymentServiceFactory(MapString, PaymentService serviceMap) { this.serviceMap serviceMap; } public PaymentService getService(PaymentContext context) { if (context.getAmount() 10000) { return serviceMap.get(bankCard); } if (context.getUserLevel() UserLevel.VIP) { return serviceMap.get(alipay); } return serviceMap.get(wechat); } }这样选择逻辑集中在一处业务代码只需要调用factory.getService(context)就行完全不需要关心具体有哪些实现类。新增支付渠道只需要加一个Impl类工厂里的逻辑按需调整其他代码不受影响。6.3 单元测试中如何优雅地替换 Impl依赖注入带来的一个巨大好处是可测试性。在单元测试里你可以用 Mockito 轻松替换掉真实的实现类。假设你要测试AlertService但不希望真的发邮件或短信ExtendWith(MockitoExtension.class) class AlertServiceTest { Mock private NotificationService emailNotification; Mock private NotificationService smsNotification; private AlertService alertService; BeforeEach void setUp() { MapString, NotificationService map new HashMap(); map.put(emailNotification, emailNotification); map.put(smsNotification, smsNotification); alertService new AlertService(map); } Test void testAlertWithEmail() { alertService.alert(emailNotification, testexample.com, hello); verify(emailNotification).send(testexample.com, hello); verifyNoInteractions(smsNotification); } }这里的关键是AlertService用的是构造器注入测试的时候可以直接new出来把 mock 对象传进去。如果用的是字段注入就必须启动 Spring 容器或者用反射来设置字段麻烦很多。这也是我反复强调构造器注入的原因之一。6.4 项目结构组织建议最后聊一下项目结构。当接口和实现类多了之后包结构怎么组织会影响代码的可维护性。我见过两种常见的组织方式。第一种是按层组织service包下放所有接口service.impl包下放所有实现类。这种方式结构清晰一眼就能看出哪些是接口哪些是实现。第二种是按功能模块组织user包下放UserService和UserServiceImplorder包下放OrderService和OrderServiceImpl。这种方式内聚性更好改一个功能只需要在一个包下操作。我个人更倾向于第二种尤其是中大型项目。按层组织在项目小的时候没问题但项目一大service.impl包下几十个类找起来很费劲。按功能模块组织的话每个模块的接口和实现放在一起修改的时候不用来回跳包。不管用哪种方式有一条原则是通用的接口和实现类的包路径不要相差太远。如果接口在com.example.api实现在com.example.internal.impl.v2那维护起来会很痛苦。尽量让它们在同一层级或者相邻层级。7. 我踩过的那些坑和最后的建议写了这么多最后分享几个我实际踩过的坑都是文档里不会写的。第一个坑是Bean 名称的大小写。有一次我定义了一个URLValidatorImpl用Service没指定名称然后在另一个地方用Qualifier(uRLValidator)去注入死活注入不进去。后来才发现 Spring 对连续大写字母开头的类名有特殊处理URLValidatorImpl的默认 Bean 名称是URLValidatorImpl而不是uRLValidatorImpl。从那以后我所有需要Qualifier的地方都显式指定 Bean 名称再也不依赖默认规则了。第二个坑是Primary 和 Qualifier 同时存在时的优先级。我原本以为Qualifier的优先级更高后来发现如果Primary标记的 Bean 和Qualifier指定的 Bean 不是同一个Spring 会优先用Qualifier指定的。但如果Qualifier指定的 Bean 不存在Spring 不会回退到Primary而是直接报错。这个行为在排查问题时很容易让人困惑。第三个坑是条件化注入的加载顺序。ConditionalOnProperty的判断发生在 Bean 注册阶段如果你的条件依赖于另一个 Bean 的状态可能会因为加载顺序问题导致判断结果不符合预期。这种场景下用ConditionalOnBean更合适但也要注意 Bean 的依赖顺序。最后一个建议不要过度设计。我见过一些项目一个接口只有一个实现类也要拆成接口和实现两个文件然后还要用工厂模式去创建。这种为了“面向接口编程”而面向接口编程的做法除了增加代码量之外没有任何好处。接口的价值在于“有多个实现”或者“需要跨模块解耦”如果这两个条件都不满足直接写一个类就行了。等真的需要第二个实现的时候再抽接口也不迟。依赖注入这个技术点说简单也简单说复杂也复杂。简单在于日常写业务代码加个Service加个Autowired就能跑起来。复杂在于一旦出了问题报错信息往往不够直观需要你对 Bean 的生命周期、匹配规则、代理机制有足够的理解才能快速定位。希望这篇文章能帮你把“带 Impl 的依赖注入”这件事彻底搞清楚下次再遇到NoUniqueBeanDefinitionException或者循环依赖的时候能心里有底、手上有招。