📌 PDF :大白话说Java面试题 --- 06_Spring篇
第8题:可以通过多少种方式完成依赖注入?
📚 回答:
- 核心考点 : 这个问题表面问"有几种方式",但大厂面试官真正想考察的是 Spring 官方推荐优先级背后的设计哲学 (为什么构造器注入是唯一推荐)、三种注入方式在源码层的处理差异 (
ConstructorResolvervsAutowiredMethodElementvsAutowiredFieldElement)、以及 被忽略的第 4 种方式------方法注入(Method Injection)和接口注入(Interface Injection)。面试官真正想判断的是:你是否将注入方式视为设计决策,而非仅仅是语法选择。
1. 三种主流注入方式------Spring 官方立场
Spring 官方文档明确支持三种注入方式,但有严格的推荐优先级:citation:1citation:4
| 注入方式 | Spring 推荐度 | 官方理由 | 处理类 |
|---|---|---|---|
| 构造器注入 | ⭐⭐⭐⭐⭐ 唯一强烈推荐 | 不可变、非空保证、依赖可见、测试友好 | ConstructorResolver |
| Setter 注入 | ⭐⭐⭐ 可选场景使用 | 灵活性高,适合可选依赖 | AutowiredMethodElement |
| 字段注入 | ⭐ 不推荐 | 隐藏依赖、破坏不可变性、测试困难 | AutowiredFieldElement |
Spring 官方原文:"The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null."citation:1
2. 构造器注入------官方唯一强烈推荐
-
2.1 为什么 Spring 强烈推荐? 构造器注入的优势不是"风格偏好",而是工程质量的硬性保障:
优势 源码机制 工程影响 不可变性 final字段,JVM 保证引用不可变线程安全,无需同步 非空保证 构造器执行前参数已校验 运行时 NPE 归零 依赖可见 参数列表即依赖清单 代码审查时一目了然 测试友好 new Service(mockDep)无需 Spring 容器,纯单元测试 循环依赖早暴露 ConstructorResolver检测启动时失败,而非生产环境偶发 java@Service public class OrderService { private final UserRepository userRepository; private final PaymentService paymentService; // Spring Boot 2.x+ 单构造器可省略 @Autowired public OrderService(UserRepository userRepository, PaymentService paymentService) { this.userRepository = userRepository; this.paymentService = paymentService; } } -
2.2 构造器注入的源码处理
ConstructorResolver.autowireConstructor()在createBeanInstance()阶段执行:citation:3java// AbstractAutowireCapableBeanFactory.createBeanInstance() protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); // 解析构造器参数类型,递归 resolveDependency() return autowireConstructor(beanName, mbd, ctors, args); }关键特征 :构造器注入在对象实例化阶段完成,这意味着对象创建时所有依赖必须已就绪。
3. Setter 注入------可选依赖场景
-
3.1 适用场景 Setter 注入适用于依赖可选 或运行期可动态替换的场景:
java@Service public class DataSourceConfig { private DataSource dataSource; @Autowired(required = false) // 可选依赖 public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }典型场景:
- 配置类中的可选数据源;
- 插件化架构中可选的功能模块;
- 运行期需要动态切换的实现(如 A/B 测试)。
-
3.2 Setter 注入的源码处理
AutowiredMethodElement.inject()在populateBean()阶段执行:citation:3javaprotected void inject(Object bean, String beanName, PropertyValues pvs) { Method method = (Method) this.member; Object[] arguments = resolveMethodArguments(method); ReflectionUtils.makeAccessible(method); method.invoke(bean, arguments); // 调用 Setter 方法 }关键特征 :Setter 注入在对象创建后执行,依赖可以为 null,运行期可通过再次调用 Setter 替换。
4. 字段注入------不推荐但广泛误用
-
4.1 字段注入的 5 大缺陷 IDEA 会提示 "Field injection is not recommended",原因如下:citation:2citation:5
缺陷 具体表现 后果 破坏不可变性 字段不能声明 final对象可变,线程不安全 隐藏依赖关系 依赖分散在私有字段中 无法通过公共 API 识别依赖 NPE 风险 构造器中访问注入字段为 null 启动时无法检测,运行时偶发 NPE 测试困难 必须通过反射注入 Mock 测试代码复杂,或依赖 Spring 容器 掩盖设计问题 参数过多无直观提示 类职责膨胀,违反 SRP java@Service public class UserService { @Autowired // ⚠️ IDEA 警告:Field injection is not recommended private UserRepository userRepository; } -
4.2 字段注入的源码处理
AutowiredFieldElement.inject()在populateBean()阶段通过反射直接设置字段值:citation:3javaprotected void inject(Object bean, String beanName, PropertyValues pvs) { Field field = (Field) this.member; Object value = beanFactory.resolveDependency( new DependencyDescriptor(field, this.required), beanName); ReflectionUtils.makeAccessible(field); field.set(bean, value); // 反射直接设置私有字段 }关键特征:反射设置私有字段,绕过 Java 访问控制,破坏了封装性。
5. 第 4 种方式:方法注入(Method Injection)------被忽略的高级特性
除了三种主流方式,Spring 还支持 方法注入 (Method Injection),用于解决 prototype Bean 注入到 singleton Bean 中的生命周期不匹配问题:citation:6
-
5.1 问题场景
singletonBean 持有prototypeBean 的引用,导致 prototype 无法被 GC:java@Component @Scope("prototype") public class Command { } @Component public class CommandManager { @Autowired private Command command; // ❌ 只注入一次,后续复用同一个 prototype 实例 } -
5.2 解决方案:@Lookup 方法注入 Spring 通过 CGLIB 代理重写方法,每次调用都返回新的 prototype 实例:
java@Component public abstract class CommandManager { // Spring 通过 CGLIB 代理实现此方法,每次调用创建新的 Command @Lookup protected abstract Command createCommand(); public void process() { Command command = createCommand(); // 每次获取新实例 // ... } }原理 :Spring 使用 CGLIB 生成子类,重写
@Lookup标注的方法,方法体从容器中获取 Bean。citation:6 -
5.3 另一种方案:ObjectFactory / Provider 注入
java@Component public class CommandManager { @Autowired private ObjectFactory<Command> commandFactory; // 延迟获取 public void process() { Command command = commandFactory.getObject(); // 每次获取新实例 // ... } }
6. 三种主流注入方式深度对比
| 对比维度 | 构造器注入 | Setter 注入 | 字段注入 |
|---|---|---|---|
| Spring 推荐度 | ⭐⭐⭐⭐⭐ 唯一强烈推荐 | ⭐⭐⭐ 可选场景 | ⭐ 不推荐 |
| 注入时机 | 实例化阶段(createBeanInstance) |
属性填充阶段(populateBean) |
属性填充阶段(populateBean) |
| 不可变性 | ✅ final 字段 |
❌ | ❌ |
| NPE 风险 | 无(构造器校验) | 有(可能未调用 Setter) | 有(构造器中访问为 null) |
| 依赖可见性 | 高(参数列表) | 中(Setter 方法) | 低(隐藏字段) |
| 单元测试 | new Service(mock) |
调用 Setter 传入 Mock | 反射注入或依赖容器 |
| 循环依赖检测 | 启动时暴露 | 运行时暴露(三级缓存解决) | 运行时暴露(三级缓存解决) |
| 可选依赖 | ❌ 不支持 | ✅ @Autowired(required=false) |
✅ @Autowired(required=false) |
| 动态替换 | ❌ 不支持 | ✅ 运行期可再次调用 Setter | ❌ 需反射修改 |
| 代码简洁度 | 中 | 低 | 高 |
7. 生产环境避坑指南
-
7.1 构造器参数过多 = 设计异味 如果构造器参数超过 4-5 个,说明类可能承担了过多职责:citation:4
java// ❌ 违反单一职责原则 public class OrderService { public OrderService(UserRepo userRepo, ProductRepo productRepo, InventoryRepo inventoryRepo, PaymentRepo paymentRepo, LogRepo logRepo, NotificationRepo notificationRepo) { } } // ✅ 拆分为多个小类 public class OrderCreationService { } public class OrderPaymentService { } public class OrderNotificationService { } -
7.2 混合使用多种注入方式的风险 一个类中同时使用构造器注入和字段注入,会导致依赖关系混乱:
java// ❌ 反模式:混合使用 @Service public class UserService { private final UserRepository userRepository; // 构造器注入 @Autowired private EmailService emailService; // 字段注入 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }问题:依赖关系分散在构造器和字段中,代码审查时容易遗漏字段注入的依赖。
-
7.3
@Autowired与@Resource的选择维度 @Autowired@Resource来源 Spring 注解 JSR-250 标准 匹配规则 先按类型,再按名称 先按名称,再按类型 注入位置 构造器、Setter、字段 Setter、字段 适用场景 Spring 项目 追求框架无关性 -
7.4 循环依赖的正确处理 遇到循环依赖首先反思设计。如果确实需要:
- 构造器注入:使用
@Lazy延迟注入; - 字段注入:Spring 自动通过三级缓存解决,但建议重构消除。
- 构造器注入:使用
8. 面试官追问与高分回答模板
-
追问 1:"Spring 中有多少种依赖注入方式?"
低分回答:"三种:构造器注入、Setter 注入、字段注入。"(忽略了方法注入)
高分回答:
"Spring 支持 4 种依赖注入方式:
- 构造器注入 :通过构造器参数注入,Spring 官方唯一强烈推荐的方式。优势是不可变性(
final)、非空保证、依赖可见、测试友好。源码由ConstructorResolver在createBeanInstance()阶段处理。 - Setter 注入 :通过 Setter 方法注入,适用于可选依赖或运行期需动态替换的场景。源码由
AutowiredMethodElement在populateBean()阶段处理。 - 字段注入 :通过
@Autowired直接注入字段,代码最简洁但 Spring 官方不推荐。源码由AutowiredFieldElement通过反射field.set()直接设置私有字段,破坏了封装性。 - 方法注入(Method Injection) :通过
@Lookup注解或ObjectFactory注入,用于解决prototypeBean 注入到singletonBean 时的生命周期不匹配问题。Spring 通过 CGLIB 代理重写@Lookup方法,每次调用返回新实例。
官方推荐优先级:构造器注入 > Setter 注入(可选依赖)> 字段注入(不推荐)。"
- 构造器注入 :通过构造器参数注入,Spring 官方唯一强烈推荐的方式。优势是不可变性(
-
追问 2:"为什么 Spring 强烈推荐构造器注入?字段注入有什么问题?"
高分回答:
"Spring 官方从 4.0 起明确推荐构造器注入,原因有五个:
- 不可变性 :构造器注入允许依赖声明为
final,对象创建后不可变,天然线程安全;字段注入无法使用final。 - 非空保证:构造器注入在对象创建时就要求所有依赖就绪,没有依赖就无法创建对象,从根本上杜绝 NPE;字段注入在构造器中访问会得到 null。
- 依赖可见:构造器参数列表一目了然,类的所有依赖关系清晰透明;字段注入的依赖隐藏在私有字段中,代码审查时容易遗漏。
- 测试友好 :单元测试可以直接
new Service(mockDep),无需反射或 Spring 容器;字段注入必须通过反射注入 Mock,或依赖@SpringBootTest启动容器。 - 设计约束 :构造器参数过多(>4个)会直观提示类职责过重,促使开发者重构;字段注入没有这个'预警'机制,容易导致类膨胀。
字段注入唯一的优点是代码简洁,但为了这点便利牺牲设计质量不值得。"
- 不可变性 :构造器注入允许依赖声明为
-
追问 3:"构造器注入和 Setter 注入在循环依赖处理上有什么区别?"
高分回答:
"两者的核心区别在于循环依赖暴露的时机 和解决机制:
- 构造器注入 :循环依赖在启动时 直接暴露。因为构造器注入在
createBeanInstance()阶段完成,此时对象尚未创建完成(构造器还没执行完),无法放入三级缓存。如果 A 的构造器依赖 B,B 的构造器依赖 A,Spring 会直接抛出BeanCurrentlyInCreationException。这是'早失败'原则,避免运行时偶发问题。 - Setter/字段注入 :循环依赖在运行时 暴露,但 Spring 通过三级缓存 自动解决。因为字段注入在
populateBean()阶段完成,此时对象已经通过无参构造器实例化,可以先放入三级缓存(singletonFactories),供循环依赖的对方获取早期引用。
工程建议 :遇到构造器循环依赖,首先反思设计是否违反了单一职责原则。如果确实需要,使用@Lazy延迟注入其中一个依赖,而不是改用字段注入绕过问题。"
- 构造器注入 :循环依赖在启动时 直接暴露。因为构造器注入在
-
追问 4:"@Lookup 方法注入是什么场景?怎么实现的?"
高分回答:
"
@Lookup方法注入用于解决 prototype Bean 注入到 singleton Bean 中的生命周期不匹配 问题。问题场景 :
singletonBean(如CommandManager)持有prototypeBean(如Command)的引用。如果使用字段注入,Command只在CommandManager创建时注入一次,后续所有调用复用同一个实例,失去了 prototype 的意义。解决方案:
java@Component public abstract class CommandManager { @Lookup protected abstract Command createCommand(); // 抽象方法 }Spring 使用 CGLIB 动态代理 生成
CommandManager的子类,重写createCommand()方法。方法体从容器中获取CommandBean,由于Command是 prototype,每次调用都会创建新实例。替代方案是使用
ObjectFactory<Command>或Provider<Command>注入,延迟获取 Bean。" -
追问 5:"如果构造器参数太多(比如 8 个),怎么办?"
高分回答:
"构造器参数超过 4-5 个是设计异味,说明类可能违反了单一职责原则。解决方案:
-
按职责拆分 :将大类拆分为多个小类。例如
OrderService拆分为OrderCreationService、OrderPaymentService、OrderNotificationService; -
使用 Facade 模式 :创建一个聚合类,将相关依赖分组:
javapublic class OrderDependencies { private final UserRepo userRepo; private final ProductRepo productRepo; // ... } public class OrderService { public OrderService(OrderDependencies deps) { } } -
Builder 模式:如果确实需要大量参数,使用 Builder 模式构建对象;
-
检查是否过度抽象 :某些依赖可能是实现细节,不应作为构造器参数(如
ObjectMapper、Clock等工具类)。
核心原则:构造器参数是类的'契约',参数过多说明类的职责边界不清晰。"
-
-
追问 6:"在一个项目中,三种注入方式混用有什么问题?"
高分回答:
"一个类中混用多种注入方式会导致以下问题:
- 依赖关系混乱:构造器注入的依赖在参数列表中可见,字段注入的依赖隐藏在类中。代码审查时容易遗漏字段注入的依赖,导致对类的依赖复杂度评估不准。
- 初始化顺序不一致 :构造器注入在实例化时完成,字段注入在
populateBean()阶段完成。如果构造器中访问字段注入的依赖,会得到 null(因为字段注入尚未执行)。 - 不可变性破坏 :构造器注入的依赖可以是
final,字段注入的依赖不能是final。混用导致类部分不可变、部分可变,线程安全模型混乱。 - 测试复杂度增加 :测试时需要同时处理构造器参数和字段注入,增加了 Mock 的复杂度。
最佳实践:一个类只使用一种注入方式。强制依赖用构造器注入,可选依赖用 Setter 注入,绝不混用。如果遗留代码中已混用,重构时应逐步统一为构造器注入。"
9. 方案选型速查表
| 场景 | 推荐注入方式 | 核心理由 |
|---|---|---|
| 强制依赖(核心业务类) | 构造器注入 | 不可变、非空保证、测试友好 |
| 可选依赖(配置、插件) | Setter 注入 | 灵活性高,运行期可替换 |
| 同类型多实例 | 构造器注入 + @Qualifier |
明确指定实现类 |
| 策略模式(多实现) | 构造器注入 + List<Interface> |
自动收集所有实现 |
| prototype → singleton | @Lookup / ObjectFactory |
每次获取新实例 |
| 循环依赖(构造器注入) | @Lazy + 构造器注入 |
保持构造器优势,延迟打破循环 |
| 配置属性注入 | @Value + 字段/构造器 |
配置外部化 |
| 框架无关性要求 | @Inject(JSR-330) |
不绑定 Spring |
💡 面试官想要的满分总结:
Spring 支持 4 种依赖注入方式:构造器注入、Setter 注入、字段注入、方法注入(
@Lookup)。但官方只强烈推荐构造器注入,Setter 注入用于可选依赖,字段注入不推荐,方法注入用于特定生命周期场景。构造器注入的优势不是"风格偏好",而是工程质量的硬性保障:不可变性(
final)、非空保证、依赖可见、测试友好、循环依赖早暴露。这些特性通过ConstructorResolver在createBeanInstance()阶段实现。字段注入虽然代码简洁,但隐藏依赖、破坏不可变性、测试困难、掩盖设计问题------IDEA 会明确警告 "Field injection is not recommended"。
@Lookup方法注入是被忽略但重要的第 4 种方式,通过 CGLIB 代理解决 prototype Bean 在 singleton Bean 中的生命周期不匹配。理解注入方式的本质,是理解 Spring 设计哲学的关键------构造器注入不是选择,而是约束,它用编译期和启动期的硬性检查,换取运行时的稳定性。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯