【大白话说Java面试题 第147题】【06_Spring篇】第7题:什么是依赖注入?

📌 PDF :大白话说Java面试题 --- 06_Spring篇

第7题:什么是依赖注入?

📚 回答:

  • 核心考点 : "什么是依赖注入"是 Spring 面试的入门题,但大厂面试官不会满足于"容器自动注入依赖"这种教科书定义。真正想考察的是 DI 在 SOLID 原则中的定位 (依赖倒置原则的具体实现模式)、三种注入方式的源码级差异ConstructorResolver/AutowiredMethodElement/AutowiredFieldElement 的处理时机和线程安全影响)、以及 DI 与 Service Locator 模式的本质区别(推送 vs 拉取)。面试官真正想判断的是:你是否将 DI 视为一种架构设计模式,而非仅仅是 Spring 的语法特性。
1. 依赖注入的精确定义------不是"自动创建对象",而是"推送依赖"
  • 1.1 Martin Fowler 的原始定义 Martin Fowler 在 2004 年的经典文章《Inversion of Control Containers and the Dependency Injection pattern》中明确定义:citation:1

    "依赖注入是一种将组件的依赖关系从外部注入的技术,组件无需自己查找依赖,也无需知道依赖的具体实现。"

    核心特征

    特征 说明
    外部注入 依赖由容器/调用方提供,而非组件自己创建
    接口依赖 组件声明依赖接口,不绑定具体实现
    无感知 组件不知道容器的存在(与 Service Locator 的关键区别)
    可替换 运行时可通过不同实现替换依赖
  • 1.2 DI 与 Service Locator 的本质区别 这是面试中最容易混淆的两个概念:citation:1

    维度 依赖注入(DI) 服务定位器(Service Locator)
    依赖获取方式 推送(Push):容器主动注入 拉取(Pull):组件主动查找
    容器耦合 ❌ 组件不知道容器存在 ✅ 组件依赖容器 API
    依赖可见性 高(构造器/Setter 暴露) 低(隐藏查找逻辑)
    测试友好度 直接传入 Mock,无需容器 需模拟 Service Locator
    典型代码 new Service(mockDep) ServiceLocator.get("dep")
    java 复制代码
    // ❌ Service Locator:组件耦合了容器 API
    public class OrderService {
        private UserRepository userRepo = ServiceLocator.get("userRepository");
    }
    
    // ✅ 依赖注入:组件完全无感知
    public class OrderService {
        private final UserRepository userRepo;
        public OrderService(UserRepository userRepo) {  // 容器推送依赖
            this.userRepo = userRepo;
        }
    }

    关键认知 :DI 是 IOC 原则的一种实现模式,Service Locator 是另一种实现模式。Spring 同时支持两者(@Autowired 是 DI,getBean() 是 Service Locator),但推荐优先使用 DIcitation:1

2. DI 的三种实现方式------源码级差异

Spring 对三种注入方式的处理逻辑完全不同,体现在不同的处理类和注入时机:citation:3citation:4

  • 2.1 构造器注入(Constructor Injection)------官方推荐 Spring 从 4.0 起明确推荐构造器注入:citation:4

    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;
        }
    }

    Spring 源码处理ConstructorResolver.autowireConstructor()

    java 复制代码
    // AbstractAutowireCapableBeanFactory.createBeanInstance()
    protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
        Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
        // 解析构造器参数类型,递归解析依赖
        return autowireConstructor(beanName, mbd, ctors, args);
    }

    注入时机实例化阶段createBeanInstance),对象创建时就完成依赖注入。

    核心优势

    优势 说明
    不可变性 final 字段,对象创建后不可变,线程安全
    非空保证 无依赖则无法创建对象,杜绝 NPE
    依赖可见 构造器参数列表即依赖清单
    测试友好 直接 new Service(mockDep)
    循环依赖早暴露 启动时抛出异常,而非运行时
  • 2.2 Setter 注入(Setter Injection)------可选依赖 适用于依赖可选、运行期可替换的场景:

    java 复制代码
    @Service
    public class UserService {
        private DataSource dataSource;
    
        @Autowired(required = false)
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    }

    Spring 源码处理AutowiredMethodElement.inject()

    java 复制代码
    // AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement
    protected 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 方法注入
    }

    注入时机属性填充阶段populateBean),对象创建后通过反射调用 Setter 方法注入。

  • 2.3 字段注入(Field Injection)------不推荐 IDEA 会提示警告:

    java 复制代码
    @Service
    public class UserService {
        @Autowired  // ⚠️ Field injection is not recommended
        private UserRepository userRepository;
    }

    Spring 源码处理AutowiredFieldElement.inject()

    java 复制代码
    // AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement
    protected 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);  // 反射直接设置字段值
    }

    注入时机属性填充阶段populateBean),对象创建后通过反射直接设置字段值。

  • 2.4 三种注入方式深度对比

    对比维度 构造器注入 Setter 注入 字段注入
    处理类 ConstructorResolver AutowiredMethodElement AutowiredFieldElement
    注入时机 实例化阶段 属性填充阶段 属性填充阶段
    不可变性 final
    NPE 风险 无(构造器校验) 有(可能未调用 Setter) 有(构造器中访问为 null)
    依赖可见性 高(参数列表) 中(Setter 方法) 低(隐藏字段)
    循环依赖 启动时暴露 运行时暴露(三级缓存解决) 运行时暴露(三级缓存解决)
    Spring 推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐
    IDEA 警告 ⚠️ "Field injection is not recommended"
3. DI 与 SOLID 原则的关系

DI 不是孤立的技术,而是支撑多个 SOLID 原则的具体模式:citation:2

SOLID 原则 DI 的支撑方式 代码体现
依赖倒置(DIP) 高层模块依赖抽象接口,低层模块实现接口 OrderService 依赖 UserRepository 接口
开闭原则(OCP) 新增功能只需添加新实现类,无需修改使用方 新增 WeChatPayService implements PaymentService
单一职责(SRP) 每个类只负责自己的职责,依赖由容器管理 OrderService 不处理对象创建
接口隔离(ISP) 依赖最小接口,而非胖接口 UserRepository 只暴露必要方法
里氏替换(LSP) 子类实现可无缝替换父类/接口 MockUserRepository 替换 MySQLUserRepository
4. DI 在 Spring 中的完整执行链路

理解 DI 必须理解 Spring 从配置到注入的完整链路:citation:3

复制代码
@Configuration 配置类 / @ComponentScan 扫描
    ↓
ClassPathBeanDefinitionScanner 扫描包路径
    ↓
AnnotatedBeanDefinitionReader 解析 @Configuration
    ↓
BeanDefinitionRegistry 注册 BeanDefinition(元数据)
    ↓
refresh() → finishBeanFactoryInitialization()
    ↓
preInstantiateSingletons() 遍历所有非懒加载单例
    ↓
getBean() → doGetBean()
    ↓
createBean() → doCreateBean()
    ↓
createBeanInstance() 实例化(构造器注入在此完成)
    ↓
populateBean() 属性填充(Setter/字段注入在此完成)
    ↓
initializeBean() 初始化(@PostConstruct、BPP 前后处理)
    ↓
单例 Bean 放入 singletonObjects 缓存
5. DI 的高级特性
  • 5.1 按类型注入 + @Qualifier 限定

    java 复制代码
    @Service
    public class PaymentRouter {
        @Autowired
        @Qualifier("aliPayService")  // 指定注入 aliPayService
        private PaymentService paymentService;
    }
  • 5.2 集合注入------策略模式的天然支持

    java 复制代码
    @Service
    public class PaymentRouter {
        // 自动收集所有 PaymentService 的实现类
        @Autowired
        private List<PaymentService> paymentServices;
    
        public PaymentService route(String channel) {
            return paymentServices.stream()
                .filter(p -> p.supports(channel))
                .findFirst()
                .orElseThrow();
        }
    }
  • 5.3 @Value 注入------配置外部化

    java 复制代码
    @Service
    public class OrderService {
        @Value("${order.timeout:30000}")
        private int timeout;
    
        @Value("${order.max-retry:3}")
        private int maxRetry;
    }
  • 5.4 @Lazy 延迟注入

    java 复制代码
    @Service
    public class ServiceA {
        private final ServiceB serviceB;
        public ServiceA(@Lazy ServiceB serviceB) {  // 延迟注入,解决循环依赖
            this.serviceB = serviceB;
        }
    }
6. 生产环境避坑指南
  • 6.1 构造器注入参数过多 = 设计异味 如果构造器参数超过 4-5 个,说明类可能承担了过多职责,应使用 Facade 模式或拆分:

    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 { }
  • 6.2 循环依赖的正确处理 遇到循环依赖首先反思设计是否合理。如果确实需要:

    • 构造器注入:使用 @Lazy 延迟注入;
    • 字段注入:Spring 自动通过三级缓存解决,但建议重构消除。
  • 6.3 避免 @Autowired 注入 null 如果 Bean 未被 Spring 管理(如 new 创建的对象),@Autowired 不会生效:

    java 复制代码
    // ❌ userRepository 为 null,因为 OrderService 不是 Spring 管理的 Bean
    OrderService service = new OrderService();
    service.createOrder();  // NPE!
    
    // ✅ 必须从容器中获取
    OrderService service = applicationContext.getBean(OrderService.class);
  • 6.4 @Resource@Autowired 的选择

    维度 @Autowired @Resource
    来源 Spring 注解 JSR-250 标准
    匹配规则 先按类型,再按名称 先按名称,再按类型
    适用场景 Spring 项目 追求框架无关性
7. 面试官追问与高分回答模板
  • 追问 1:"什么是依赖注入?"

    低分回答:"依赖注入就是 Spring 容器自动把依赖对象注入到类中。"(没有触及本质)

    高分回答

    "依赖注入(DI)是一种设计模式,是控制反转(IOC)原则的一种具体实现。它的核心特征是**'推送'而非'拉取'**------依赖由外部容器在组件创建时主动注入,组件无需自己查找依赖,也无需知道依赖的具体实现。

    与 Service Locator(服务定位器)的关键区别在于:DI 的组件完全不知道容器的存在(通过构造器/Setter/字段接收依赖),而 Service Locator 需要组件主动调用容器 API(如 getBean())查找依赖,导致组件与容器耦合。

    Spring 支持三种注入方式:构造器注入(ConstructorResolver 处理,实例化时注入)、Setter 注入(AutowiredMethodElement 处理,属性填充时注入)、字段注入(AutowiredFieldElement 处理,反射直接注入)。官方推荐构造器注入。"

  • 追问 2:"为什么 Spring 推荐构造器注入而不是字段注入?"

    高分回答

    "Spring 官方从 4.0 起明确推荐构造器注入,原因有五个:

    1. 不可变性 :构造器注入允许依赖声明为 final,对象创建后不可变,天然线程安全;字段注入无法使用 final
    2. 非空保证:构造器注入在对象创建时就要求所有依赖就绪,没有依赖就无法创建对象,从根本上杜绝 NPE;字段注入在构造器中访问会得到 null。
    3. 依赖可见:构造器参数列表一目了然,类的所有依赖关系清晰透明;字段注入的依赖隐藏在私有字段中。
    4. 测试友好 :单元测试可以直接 new Service(mockDep),无需反射或 Spring 容器;字段注入必须通过反射注入 Mock。
    5. 设计约束 :构造器参数过多(>4个)会直观提示类职责过重,促使开发者重构;字段注入没有这个'预警'机制。
      从源码角度看,构造器注入在 createBeanInstance() 阶段完成,Setter 和字段注入在 populateBean() 阶段完成。构造器注入的循环依赖在启动时直接暴露(BeanCurrentlyInCreationException),而字段注入的循环依赖被三级缓存掩盖。"
  • 追问 3:"依赖注入和依赖查找有什么区别?"

    高分回答

    "两者都是 IOC 原则的实现方式,但核心区别在于谁主动

    • 依赖注入(DI):容器主动将依赖'推送'给组件。组件通过构造器参数、Setter 方法或字段声明'我需要什么',容器负责'给什么'。组件完全不知道容器的存在。
    • 依赖查找(DL) :组件主动向容器'请求'依赖。组件需要知道容器的 API(如 ApplicationContext.getBean()),通过名称或类型查找依赖。
      对比
      | 维度 | DI | DL |
      | 耦合 | 组件与容器零耦合 | 组件与容器 API 耦合 |
      | 测试 | 直接传入 Mock | 需模拟容器或启动容器 |
      | 依赖可见性 | 高(构造器/Setter 暴露) | 低(隐藏查找逻辑) |
      | Spring 支持 | @Autowired@Inject | getBean() |
      Spring 同时支持两者,但推荐 DI。只有在无法通过注入获取依赖时(如静态方法中),才使用 DL。"
  • 追问 4:"构造器注入和 Setter 注入在循环依赖处理上有什么区别?"

    高分回答

    "两者的核心区别在于循环依赖暴露的时机解决方式

    • 构造器注入 :循环依赖在启动时 直接暴露。因为构造器注入在 createBeanInstance() 阶段完成,此时对象尚未创建完成(构造器还没执行完),无法放入三级缓存。如果 A 的构造器依赖 B,B 的构造器依赖 A,Spring 会直接抛出 BeanCurrentlyInCreationException
    • Setter/字段注入 :循环依赖在运行时 暴露,但 Spring 通过三级缓存 自动解决。因为字段注入在 populateBean() 阶段完成,此时对象已经实例化(通过无参构造器),可以先放入三级缓存(singletonFactories),供循环依赖的对方获取早期引用。
      工程建议 :遇到构造器循环依赖,首先反思设计是否违反了单一职责原则。如果确实需要,使用 @Lazy 延迟注入其中一个依赖,而不是改用字段注入绕过问题。"
  • 追问 5:"@Autowired 的注入过程是怎样的?从源码角度说说。"

    高分回答

    "@Autowired 的注入过程因注入方式不同而有所差异,以字段注入为例:

    1. 解析阶段AutowiredAnnotationBeanPostProcessorpostProcessMergedBeanDefinition() 中扫描类的所有字段,找到标记 @Autowired 的字段,创建 AutowiredFieldElement 对象缓存;
    2. 注入阶段 :在 populateBean() 中调用 AutowiredFieldElement.inject()
      • 通过 DependencyDescriptor 封装字段信息(类型、是否必需);
      • 调用 beanFactory.resolveDependency() 解析依赖:先按类型查找所有候选 Bean,如果多个则按 @Qualifier 或字段名过滤;
      • 如果找不到且 required=true,抛出 NoSuchBeanDefinitionException
      • 通过反射 field.set(bean, value) 直接设置字段值。
    3. 构造器注入的差异 :在 createBeanInstance() 中由 ConstructorResolver 处理,解析构造器参数类型,递归调用 resolveDependency() 获取每个参数,然后反射调用 Constructor.newInstance(args) 创建对象。
      关键认知:@Autowired 的处理者是 AutowiredAnnotationBeanPostProcessor(一个 BeanPostProcessor),它在 populateBean() 阶段介入,这是 ApplicationContext 自动注册的 BPP 之一。"
  • 追问 6:"如果不用 Spring,怎么实现依赖注入?手写一个极简版。"

    高分回答

    "不依赖 Spring 实现 DI 的核心思路是:扫描注解 → 解析依赖 → 递归创建 → 反射注入。

    java 复制代码
    public class SimpleDIContainer {
        private Map<Class<?>, Object> instances = new HashMap<>();
    
        public void register(Class<?> clazz) {
            // 1. 解析构造器参数
            Constructor<?> ctor = clazz.getDeclaredConstructors()[0];
            Class<?>[] paramTypes = ctor.getParameterTypes();
    
            // 2. 递归解析依赖
            Object[] args = new Object[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++) {
                args[i] = getOrCreate(paramTypes[i]);  // 递归创建依赖
            }
    
            // 3. 反射创建对象
            Object instance = ctor.newInstance(args);
            instances.put(clazz, instance);
        }
    
        private Object getOrCreate(Class<?> type) {
            if (instances.containsKey(type)) return instances.get(type);
            return register(type);  // 递归创建
        }
    }

    这个极简版展示了 DI 的核心:递归解析构造器参数、缓存单例实例。实际框架还需处理:循环依赖(三级缓存)、作用域、生命周期回调、AOP 代理等。"

8. 方案选型速查表
场景 推荐注入方式 核心理由
强制依赖(核心业务类) 构造器注入 不可变、非空保证、测试友好
可选依赖(配置、插件) Setter 注入 灵活性高,运行期可替换
同类型多实例 构造器注入 + @Qualifier 明确指定实现类
策略模式(多实现) 构造器注入 + List<Interface> 自动收集所有实现
循环依赖(构造器) @Lazy + 构造器注入 保持构造器优势,延迟打破循环
配置属性注入 @Value + 字段/构造器 配置外部化
框架无关性要求 @Inject(JSR-330) 不绑定 Spring

💡 面试官想要的满分总结

依赖注入不是"Spring 自动注入对象"这么简单,而是一种支撑现代软件架构的设计模式

核心定义:DI 是 IOC 原则的一种实现,特征是**'推送'而非'拉取'**------依赖由外部容器在组件创建时主动注入,组件无需知道容器的存在。这与 Service Locator(组件主动查找)有本质区别。

Spring 支持三种注入方式:构造器注入(ConstructorResolver 处理,实例化时注入,官方推荐)、Setter 注入(AutowiredMethodElement 处理,属性填充时注入,可选依赖场景)、字段注入(AutowiredFieldElement 处理,反射直接注入,不推荐)。构造器注入的优势在于不可变性、非空保证、依赖可见、测试友好------这些不是"风格偏好",而是工程质量的保障。

DI 支撑了 SOLID 原则中的依赖倒置(DIP)和开闭原则(OCP),是解耦、可测试、可扩展的架构基石。理解 DI 必须理解它与 Service Locator 的区别、三种注入方式的源码差异、以及循环依赖的处理机制------这才是大厂面试官想听到的深度。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯