1. 项目概述:为什么依赖注入是Spring的灵魂
如果你用Java做后端开发,尤其是企业级应用,那Spring框架几乎是你绕不开的基石。而Spring框架之所以能成为事实上的标准,其核心设计思想——控制反转(IoC)和依赖注入(DI)——功不可没。今天我们不谈那些宏大的概念,就聚焦在“依赖注入的三种方式”这个看似基础,实则藏着无数细节和实战抉择的话题上。这不仅仅是面试八股文里的一个考点,更是你日常编码中每天都在用,却未必深思其所以然的关键技术点。
简单来说,依赖注入就是把你对象(比如一个Service)所需要的其他对象(比如一个Repository),由外部(Spring容器)来创建并“注入”给它,而不是让它自己动手去new。这样做的好处显而易见:代码解耦了,测试方便了,维护性也大大提升。Spring提供了三种主流的方式来实现这个“注入”的动作:构造器注入、Setter方法注入和字段注入。每一种方式都有其适用的场景、优缺点和背后的设计哲学。很多开发者可能只是习惯性地用
@Autowired
注解在字段上一标了事,但当你面临复杂的循环依赖、需要编写不可变对象、或者追求更清晰的单元测试时,深入了解这三种方式的区别就变得至关重要。这篇文章,我将结合我十多年的Spring实战经验,为你彻底拆解这三种注入方式,从原理到选型,从配置到避坑,让你不仅会用,更能用得明白、用得优雅。
2. 核心原理与设计思想深度剖析
在深入三种具体方式之前,我们必须先理解Spring依赖注入所承载的设计重量。它不仅仅是“把A对象给B对象”这么简单,其背后是面向对象设计原则和框架哲学的体现。
2.1 控制反转(IoC)与依赖注入(DI)的关系
很多人会把IoC和DI混为一谈,其实它们是从不同角度描述同一件事。IoC是一种设计原则,一种思想,它主张将程序的控制权从应用程序代码转移到外部容器或框架。传统的程序流程是“我主动去获取我需要的依赖”,而在IoC模式下,变成了“我声明我需要什么,然后等着别人给我”。DI则是实现IoC这种思想的一种具体技术模式。Spring通过其IoC容器(ApplicationContext)来管理所有被称为“Bean”的对象生命周期和依赖关系,DI就是它用来组装这些Bean、实现控制反转的具体手段。所以,你可以说Spring的IoC容器通过DI机制来运作。
2.2 Spring容器的核心工作流程
理解注入方式,需要知道Spring容器在背后做了什么。简单来说,它的工作分为几个阶段:
- Bean定义加载 :容器启动时,会扫描或读取配置(XML或注解),了解需要管理哪些Bean,以及它们之间的依赖关系。这相当于一张蓝图。
- Bean实例化 :根据Bean定义,调用构造方法创建Bean的原始对象。
- 依赖注入 :这是最关键的一步。容器根据依赖关系,将其他Bean的引用设置到当前Bean的对应属性中(构造器参数、Setter方法或字段)。
-
初始化
:如果Bean实现了
InitializingBean接口或定义了init-method,容器会调用这些方法进行一些自定义的初始化。 - 就绪 :此时Bean已经完全组装好,可以被其他Bean使用或通过容器获取。
- 销毁 :容器关闭时,会按照相反的顺序销毁Bean。
三种注入方式,主要影响的就是第3步——“依赖注入”这个环节是如何发生的。
2.3 三种注入方式的设计哲学对比
为什么要有三种方式?这反映了对不同场景和代码风格的考量。
- 构造器注入 :强调“完全初始化的不可变状态”。一个对象在创建时就必须获得其所有必需的依赖,一旦创建,其依赖关系就不可更改。这符合不可变对象的设计理念,能有效避免空指针异常,并且对线程安全友好。
- Setter方法注入 :强调“灵活性”和“可选依赖”。依赖可以在对象创建后被设置或更改,适用于那些依赖是可选的,或者需要在运行时动态变化的场景。它也符合JavaBean的规范。
- 字段注入 :本质上是一种“便利性”优先的折中方案。它通过反射直接设置私有字段,代码最简洁,但牺牲了清晰性和对容器的一定程度的隐藏依赖。
Spring官方从Spring Framework 4.3版本开始,就推荐使用构造器注入作为首选方式,特别是在Spring Boot中,如果一个类只有一个构造器,那么这个构造器的
@Autowired
注解甚至可以省略。这背后是推动开发者编写更健壮、更易于测试的代码。
3. 构造器注入:坚固的基石
构造器注入是我个人最推崇,也是目前社区和Spring官方最推荐的方式。它的形式是将依赖项作为参数传递给类的构造方法。
3.1 基本用法与注解
在Spring中,你可以使用
@Autowired
注解在构造方法上,但自从Spring 4.3后,如果类只有一个构造方法,这个注解可以省略。
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
// @Autowired 注解在此处可以省略
public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
public void processOrder(Order order) {
// 使用 orderRepository 和 paymentService
orderRepository.save(order);
paymentService.charge(order);
}
}
在上面的代码中,
OrderRepository
和
PaymentService
通过构造器参数注入。注意我将这两个字段声明为
final
,这意味着它们必须在构造器中被赋值,并且之后不可修改,这强制实现了不可变性。
3.2 核心优势与适用场景
-
不可变性(Immutability)
:结合
final关键字,可以确保Bean的依赖在生命周期内不变。不可变对象本质上是线程安全的,并且状态明确,减少了出错的概率。 -
完全初始化的状态
:对象一旦被容器创建出来,其所有必需依赖就已经就位,处于一个“随时可用”的状态。你永远不会得到一个依赖为
null的OrderService实例(除非你手动new,但那不是Spring管理的Bean)。这从根本上避免了运行时的NullPointerException。 - 清晰的依赖声明 :构造器的参数列表就像一份清晰的“必需品”清单。一眼就能看出这个类要正常运转,到底需要哪些东西。这对于代码阅读者和维护者非常友好。
-
易于测试
:在单元测试中,你可以直接使用
new关键字,通过构造器传入mock对象(如Mockito模拟的OrderRepository),无需任何特殊的测试框架支持或反射技巧,测试代码非常直观。@Test void testProcessOrder() { OrderRepository mockRepo = Mockito.mock(OrderRepository.class); PaymentService mockPayment = Mockito.mock(PaymentService.class); OrderService service = new OrderService(mockRepo, mockPayment); // ... 执行测试 } -
解决循环依赖
:Spring默认支持单例Bean的Setter注入和字段注入下的循环依赖(通过三级缓存),但对于构造器注入的循环依赖,Spring会直接抛出
BeanCurrentlyInCreationException。这看似是缺点,实则是一个巨大的优点!它强迫你在设计阶段就发现并消除循环依赖这种不良设计。循环依赖通常是代码结构不合理(职责不清)的信号,构造器注入帮你提前暴露了这个问题。
3.3 实战注意事项与避坑指南
注意 :当有多个构造器时,Spring需要你明确指定用哪个来进行注入。通常使用
@Autowired注解在你希望使用的那个构造器上。如果没有任何构造器被标注@Autowired,Spring会尝试使用默认的无参构造器;如果没有无参构造器,则会报错。
- 依赖过多怎么办? 如果一个构造器的参数列表非常长(比如超过7个),这本身可能是一个代码坏味道(Code Smell),暗示这个类承担了过多的职责。你应该考虑使用 领域驱动设计(DDD) 中的聚合根、值对象等概念,或者通过 外观模式(Facade) 、 服务拆分 来重构,而不是简单地选择其他注入方式去掩盖问题。
-
可选依赖的处理
:构造器注入天然适合必需依赖。如果某个依赖是可选的,你可以考虑以下几种方案:
-
使用Java 8的
Optional类型作为参数(Optional<SomeService>),但这在Spring中需要谨慎配置。 - 更常见的做法是,将可选依赖通过Setter方法注入,而必需依赖通过构造器注入。这种混合模式在实践中很常见。
-
使用
@Nullable注解(来自JSR-305或Spring)标注参数,但容器仍会尝试注入,找不到Bean时会注入null,你需要做好空值判断。
-
使用Java 8的
-
与
@Bean配置方法结合 :在Java配置类(@Configuration)中定义Bean时,构造器注入的逻辑同样清晰。@Configuration public class AppConfig { @Bean public OrderService orderService(OrderRepository repo, PaymentService payment) { // 这里调用构造器,依赖由Spring自动传入 return new OrderService(repo, payment); } }
4. Setter方法注入:灵活的补充
Setter方法注入通过类的setter方法来完成依赖的赋值。它曾经是Spring早期XML配置时代的主流,在注解驱动开发中依然有其用武之地。
4.1 基本用法与注解
在setter方法上添加
@Autowired
注解即可。
@Service
public class NotificationService {
private EmailSender emailSender;
private SmsSender smsSender;
@Autowired
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
@Autowired(required = false) // 表示这个依赖是可选的
public void setSmsSender(SmsSender smsSender) {
this.smsSender = smsSender;
}
public void sendAlert(String message) {
emailSender.send(message);
if (smsSender != null) { // 需要对可选依赖做判空
smsSender.send(message);
}
}
}
4.2 核心优势与适用场景
-
可选依赖
:这是Setter注入最大的用武之地。通过
@Autowired(required = false),你可以声明某个依赖不是必须的。如果容器中没有对应类型的Bean,Spring会跳过这个注入,而不会报错。这在集成第三方库或存在条件化Bean时非常有用。 - 动态重新配置 :理论上,依赖可以在Bean的生命周期内通过再次调用setter方法来改变。虽然在实际的Spring单例Bean中很少这样做(因为通常由容器完全管理),但在某些原型(Prototype)作用域或特殊场景下,这提供了灵活性。
- 与JavaBean标准兼容 :许多工具和框架(如UI绑定、持久化框架)都依赖于标准的JavaBean规范(有无参构造器和getter/setter),Setter注入能很好地与这些生态协作。
- 解决特定循环依赖 :Spring解决单例Bean循环依赖的机制(三级缓存)主要就是为Setter(或字段)注入设计的。对于无法通过重构消除的、确实必要的循环依赖,使用Setter注入是一种解决方案(但应优先考虑重构)。
4.3 实战注意事项与避坑指南
-
对象状态的不一致性
:使用Setter注入的Bean,在构造器调用之后,setter方法调用之前,处于一种“部分初始化”的不完整状态。如果其他Bean在初始化回调(如
@PostConstruct)中引用了这个Bean的可选依赖,而该依赖尚未被注入,就可能引发空指针异常。你需要非常清楚Bean的初始化顺序。 -
线程安全考量
:如果Setter方法可能在多线程环境下被调用以更改依赖(尽管不常见),你需要考虑setter方法本身的线程安全性。而构造器注入配合
final字段则天然线程安全。 - 避免滥用 :不要因为“参数多”就放弃构造器注入转而用一堆setter。这会让你的Bean在初始化后的一段时间内处于无效状态,增加了复杂度。 基本原则是:必需依赖用构造器,可选依赖用Setter。
-
XML配置的遗留
:在XML配置中,Setter注入的配置非常直观(
<property name="..." ref="..."/>)。如果你在维护老项目,会大量见到这种模式。但在基于注解的开发中,它的必要性已大大降低。
5. 字段注入:争议中的便捷
字段注入是三种方式中最简洁的,也是很多初学者最容易“上手”的方式。它直接在字段上使用
@Autowired
(或
@Resource
、
@Inject
)注解。
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private DiscountCalculator discountCalculator;
public Product getProductWithDiscount(Long id) {
Product product = productRepository.findById(id);
product.setFinalPrice(discountCalculator.calculate(product.getPrice()));
return product;
}
}
代码看起来非常干净,没有多余的构造器或setter方法。
5.1 看似便利背后的隐患
然而,字段注入的缺点非常突出,这也是它备受争议的原因:
- 破坏了封装性 :它通过反射直接对私有字段赋值,绕过了类可能定义的任何设置逻辑(如参数校验、触发事件等)。这违反了面向对象的基本封装原则。
- 隐藏了依赖 :依赖关系不再是类公共契约(构造器或方法签名)的一部分。仅仅阅读类的代码,你无法一眼看出它有哪些依赖,必须逐个检查所有字段上的注解。这降低了代码的可读性和可维护性。
-
不利于单元测试
:这是最致命的缺点。由于依赖是私有字段,你在编写单元测试时,无法通过构造器或setter轻松地传入mock对象。你必须借助像
SpringJUnit4ClassRunner这样的Spring测试运行器来启动整个Spring容器,或者使用ReflectionTestUtils这样的反射工具来手动设置字段。这导致测试不再是纯粹的单元测试,而是变成了集成测试,速度慢且笨重。// 不好的测试方式:需要Spring容器 @RunWith(SpringRunner.class) @SpringBootTest public class ProductServiceTest { @Autowired private ProductService productService; // 测试的是真实的Bean // ... 测试可能涉及数据库等外部资源 } // 稍好的方式(但仍不理想):使用反射工具 @Test void testWithReflection() { ProductService service = new ProductService(); ProductRepository mockRepo = Mockito.mock(ProductRepository.class); ReflectionTestUtils.setField(service, "productRepository", mockRepo); // 使用反射注入 // ... 测试 } -
与不可变性无缘
:你无法将注入字段声明为
final,因为字段注入发生在对象实例化之后。这意味着你的Bean状态是可变的,且依赖关系在理论上可以被改变(尽管Spring不会这么做)。 - 对容器强依赖 :类完全与Spring框架耦合。如果你想将这个类实例化用于非Spring环境(比如一个简单的工具类),几乎是不可能的,因为它依赖Spring来注入字段。
5.2 可能的使用场景(极其有限)
考虑到以上严重缺点,字段注入的使用场景应该被严格限制:
-
简单的配置类(
@Configuration)中的Bean引用 :有时在配置类中注入一个Bean用于创建另一个Bean,由于配置类本身生命周期简单,且通常不涉及复杂测试,可以酌情使用。 - 快速原型开发 :在验证想法、编写一次性脚本或演示代码时,为了极致的编码速度,可以暂时使用。但一旦代码进入正式项目,应立即重构。
- 框架内部或第三方库代码 :有些库为了减少对使用者的API干扰,可能会在内部使用字段注入。但作为应用开发者,你应该有更高的标准。
强烈建议 :在新的Spring Boot项目中,将字段注入视为一种“反模式”,并禁用相关的检查(很多IDE和代码检查工具如SonarQube都支持此规则)。坚持使用构造器注入为主,Setter注入为辅的原则。
6. 高级话题与混合使用策略
在实际的大型项目中,三种注入方式并非完全对立,而是可以根据实际情况混合使用,以达到最佳的设计效果。
6.1
@Autowired
的变体与选择
除了
@Autowired
,Spring还支持JSR-330标准的
@Inject
(需要额外依赖
javax.inject
)和JSR-250的
@Resource
。
-
@Inject:功能与@Autowired几乎相同,但不支持required=false属性。它更标准,但功能稍弱。 -
@Resource:行为有所不同。它默认按名称(name)进行装配,如果找不到名字再按类型。它不支持构造器注入,主要用于字段和Setter注入。在需要按特定Bean名称注入时有用。
通常,在纯Spring环境中,使用
@Autowired
即可。如果你追求Java标准,或者项目可能迁移到其他DI容器,可以考虑
@Inject
。
6.2 处理多个同类型Bean的注入:
@Qualifier
与
@Primary
当容器中存在多个同一类型的Bean时,仅靠类型注入就会产生歧义,Spring会抛出
NoUniqueBeanDefinitionException
。
@Configuration
public class Config {
@Bean
public Sender emailSender() { return new EmailSender(); }
@Bean
public Sender smsSender() { return new SmsSender(); }
}
@Service
public class AlertService {
// 这会报错,因为有两个Sender类型的Bean
@Autowired
private Sender sender;
}
解决方案1:使用
@Qualifier
指定Bean名称
@Service
public class AlertService {
@Autowired
@Qualifier("emailSender") // 指定注入名为`emailSender`的Bean
private Sender sender;
}
或者在
@Bean
定义时就指定名称:
@Bean(“emailSender”)
public Sender sender1() { ... }
解决方案2:使用
@Primary
设置首选Bean
在其中一个Bean定义上加上
@Primary
,当存在多个同类型Bean时,优先使用它。
@Configuration
public class Config {
@Bean
@Primary // 标记为首选
public Sender emailSender() { return new EmailSender(); }
@Bean
public Sender smsSender() { return new SmsSender(); }
}
// 此时AlertService中的Sender字段会自动注入emailSender
如何选择?
@Primary
用于定义默认的、通用的实现;
@Qualifier
用于在需要特别指定的场景进行精确匹配。
6.3 构造器注入与Setter注入的混合模式
这是最实用、最推荐的模式。它结合了两种方式的优点:
- 使用构造器注入所有必需的、不可变的依赖 。这保证了核心依赖的稳定性和不可变性。
- 使用Setter注入可选的、或可能在生命周期中变化的依赖 。这提供了必要的灵活性。
@Service
public class ComplexService {
// 必需的核心依赖,final修饰
private final EssentialRepository repository;
private final CriticalClient client;
// 可选的或配置性的依赖
private Optional<CacheManager> cacheManager;
private MetricsCollector metricsCollector;
// 构造器注入必需依赖
public ComplexService(EssentialRepository repository, CriticalClient client) {
this.repository = repository;
this.client = client;
}
// Setter注入可选依赖
@Autowired(required = false)
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = Optional.ofNullable(cacheManager);
}
@Autowired
public void setMetricsCollector(MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
@PostConstruct
public void init() {
// 可以安全地使用metricsCollector,因为它在@PostConstruct前已被注入
// 使用cacheManager需要检查isPresent()
}
}
这种模式清晰地表达了类的依赖契约:哪些是创建时必须的,哪些是运行时可以没有或后续设置的。
6.4 在Spring Boot中的最佳实践
Spring Boot极大地简化了配置,并对依赖注入有隐式的约定:
-
自动构造器注入
:如前所述,在Spring Boot中,如果一个
@Component(包括@Service,@Repository等)只有一个构造器,那么@Autowired可以省略。这是对构造器注入的强力推动。 -
@ConfigurationProperties绑定 :对于外部配置(如application.yml)的注入,通常使用Setter注入,因为配置属性需要在Bean创建后绑定。@Component @ConfigurationProperties(prefix = "app.mail") public class MailProperties { private String host; // 通过setter注入 private int port; public void setHost(String host) { this.host = host; } public void setPort(int port) { this.port = port; } // ... getters } -
测试支持
:Spring Boot Test提供了
@MockBean注解,可以轻松地将Spring上下文中的真实Bean替换为Mockito mock对象,这在一定程度上缓解了字段注入带来的测试困难,但依然不如构造器注入直接明了。
7. 常见问题排查与实战技巧
即使理解了原理,在实际开发中还是会遇到各种稀奇古怪的问题。这里记录一些高频问题和我的排查心得。
7.1 注入失败常见原因速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
NoSuchBeanDefinitionException
|
1. Bean未被Spring管理(缺少
@Component
等注解)。
2. 组件扫描路径未包含该类。 3. Bean的创建条件不满足(如
@ConditionalOnProperty
)。
|
1. 检查类上是否有
@Component
,
@Service
等注解。
2. 检查启动类
@SpringBootApplication
的扫描范围,或自定义的
@ComponentScan
。
3. 检查
@ConditionalXXX
注解的条件是否匹配。
|
NoUniqueBeanDefinitionException
| 存在多个同一类型的Bean。 |
1. 使用
@Qualifier
指定Bean名称。
2. 将其中一个Bean标记为
@Primary
。
3. 检查是否意外创建了多个相同类型的Bean(如通过
@Bean
方法和组件扫描都创建了)。
|
BeanCreationException
(构造器注入)
|
1. 循环依赖(构造器注入无法解决)。
2. 构造器参数对应的Bean不存在。 3. 构造器本身抛出异常。 |
1. 重构代码,打破循环依赖(提取公共部分到第三个类,使用Setter/字段注入作为
最后手段
)。
2. 确保依赖的Bean已被正确定义和扫描。 3. 检查构造器内的业务逻辑。 |
字段为
null
|
1. 使用了字段注入,但该类不是由Spring容器创建的(例如,自己
new
出来的)。
2.
@Autowired(required=false)
且Bean不存在。
3. 在
@PostConstruct
方法中访问了尚未注入的Setter依赖。
|
1. 确保对象是从Spring容器获取的(如通过
@Autowired
注入,或
ApplicationContext.getBean()
)。
2. 对可能为
null
的字段进行判空。
3. 理清初始化顺序,Setter注入的依赖在
@PostConstruct
前不一定可用。
|
| 注入的Bean不是期望的实现 |
1. 存在多个实现,未正确使用
@Qualifier
。
2. 动态代理(AOP)导致Bean类型变化(如JDK动态代理生成的是接口类型)。 |
1. 确认
@Qualifier
名称是否正确。
2. 注入时使用具体类而非接口(不推荐,破坏面向接口编程),或通过
AopContext.currentProxy()
获取代理(高级用法)。
|
7.2 循环依赖的识别与解决
循环依赖是依赖注入中的一个经典难题。Spring默认支持 单例Bean 通过 Setter/字段注入 的循环依赖。
场景
:
AService
依赖
BService
,同时
BService
也依赖
AService
。
Setter/字段注入下的Spring解决方案(三级缓存) :
-
实例化
AService(原始对象)。 -
将
AService的原始对象放入三级缓存(早期引用)。 -
准备注入
AService的依赖BService。 -
实例化
BService。 -
将
BService的原始对象放入三级缓存。 -
准备注入
BService的依赖AService。 -
从三级缓存中拿到
AService的早期引用(虽然未完全初始化),注入给BService。 -
BService初始化完成,从三级缓存升级到二级缓存(或完全初始化后放入一级缓存)。 -
将初始化好的
BService注入给AService。 -
AService初始化完成。
构造器注入为何不行?
因为构造器注入发生在实例化阶段,
AService
在实例化时必须立刻得到
BService
的实例,而此时
BService
还没开始创建,无法提供,所以直接失败。
实战建议 :
-
首选方案:重构设计
。循环依赖通常意味着职责划分不清。考虑是否可以将相互依赖的部分提取到一个新的第三方类(如
CService)中,让A和B都依赖C。或者使用事件驱动(ApplicationEvent)进行解耦。 - 妥协方案:改用Setter/字段注入 。如果循环依赖确实难以避免(且是单例),可以将其中一个依赖改为Setter注入。 记住,这只是一个妥协,而不是最佳实践。
-
使用
@Lazy注解 。在其中一个注入点添加@Lazy,告诉Spring延迟初始化这个依赖。这实际上创建了一个代理对象,在第一次真正使用时才去获取真实Bean,从而打破实例化时的死锁。这同样是一种补救措施。@Service public class AService { private final BService bService; public AService(@Lazy BService bService) { // 构造器注入使用@Lazy this.bService = bService; } }
7.3 使用
@PostConstruct
和初始化顺序
@PostConstruct
注解的方法会在依赖注入完成后、Bean正式投入使用前被调用。这是进行自定义初始化的好地方。但要注意初始化顺序:
- 实例化对象。
- 依赖注入 (执行构造器、Setter或字段注入)。
-
调用
@PostConstruct方法。 这意味着,在@PostConstruct方法中,你可以安全地使用通过 构造器注入 和 Setter注入 (如果Setter方法在@PostConstruct前被调用)设置的依赖。但对于字段注入,由于整个过程由Spring控制,在@PostConstruct阶段字段也一定是注入完成的。
一个常见的坑是:在
@PostConstruct
方法中,调用了一个依赖Bean的方法,而那个依赖Bean的
@PostConstruct
方法可能还未执行。Spring虽然会管理基本的依赖顺序,但同层级Bean的初始化顺序并不完全确定。对于有严格顺序要求的初始化,可以考虑使用
@DependsOn
注解,或者实现
SmartLifecycle
接口来精确控制。
7.4 单元测试中的注入策略
这是选择注入方式时最重要的考量之一。为了让测试更纯粹、快速:
-
极力推荐构造器注入
:可以轻松地使用
new和mock对象进行测试,无需Spring上下文。@Test void testService() { // 创建mock Repository mockRepo = Mockito.mock(Repository.class); Client mockClient = Mockito.mock(Client.class); // 通过构造器创建被测对象 MyService service = new MyService(mockRepo, mockClient); // 定义mock行为 Mockito.when(mockRepo.findById(1L)).thenReturn(new Entity()); // 执行测试 service.doSomething(1L); // 验证交互 Mockito.verify(mockClient).call(any()); } - 如果使用Setter注入 :测试时可以先创建对象,再调用setter方法注入mock。
-
如果不得不测试使用字段注入的遗留代码
:
-
使用
ReflectionTestUtils.setField(object, “fieldName”, mockValue)。 -
或者,考虑使用
@SpringBootTest进行集成测试,但这会慢得多。 - 长远来看,重构为构造器注入是根本解决方案。
-
使用
依赖注入是Spring框架的基石,三种方式各有千秋,但趋势是明确的:
构造器注入作为首选,用于强制依赖;Setter注入作为补充,用于可选依赖;字段注入则应尽量避免在新代码中使用
。理解其背后的原理、优缺点和适用场景,能帮助你在实际开发中做出更合理的选择,写出更健壮、更易测试、更易维护的代码。这不仅仅是遵循一个规范,更是培养一种清晰、严谨的编码态度。下次当你下意识地打出
@Autowired
时,不妨先思考一下:这个依赖,真的适合放在这里吗?

355

被折叠的 条评论
为什么被折叠?



