【大白话说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),但推荐优先使用 DI。citation: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. 解析阶段 :AutowiredAnnotationBeanPostProcessor 在 postProcessMergedBeanDefinition() 中扫描类的所有字段,找到标记 @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 的区别、三种注入方式的源码差异、以及循环依赖的处理机制------这才是大厂面试官想听到的深度。


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