--- title: Spring 依赖注入 date: 2020-08-30 16:06:10 order: 04 categories: - Java - 框架 - Spring - Spring核心 tags: - Java - 框架 - Spring - IOC - 依赖注入 permalink: /pages/a7ad7991/ --- # Spring 依赖注入 DI,是 Dependency Injection 的缩写,即依赖注入。依赖注入是 IoC 的最常见形式。依赖注入是手动或自动绑定的方式,无需依赖特定的容器或 API。 依赖注入 (Dependency Injection,简称 DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),它通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。 使用 DI,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。 **容器全权负责组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象**。 DI 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下: - **谁依赖于谁:**当然是应用程序依赖于 IoC 容器; - **为什么需要依赖:**应用程序需要 IoC 容器来提供对象需要的外部资源; - **谁注入谁:**很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象; - **注入了什么**:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 ## IoC 依赖注入 API - 根据 Bean 名称注入 - 根据 Bean 类型注入 - 注入容器内建 Bean 对象 - 注入非 Bean 对象 - 注入类型 - 实时注入 - 延迟注入 ## 依赖注入模式 依赖注入模式可以分为手动注入模式和自动注入模式。 ### 手动注入模式 手动注入模式:配置或者编程的方式,提前安排注入规则 - XML 资源配置元信息 - Java 注解配置元信息 - API 配置元信息 ### 自动注入模式 自动注入模式即自动装配。自动装配(Autowiring)是指 Spring 容器可以自动装配 Bean 之间的关系。Spring 可以通过检查 `ApplicationContext` 的内容,自动解析合作者(其他 Bean)。 - 自动装配可以显著减少属性或构造函数参数的配置。 - 随着对象的发展,自动装配可以更新配置。 > 注:由于自动装配存在一些限制和不足,官方不推荐使用。 #### 自动装配策略 当使用基于 XML 的配置元数据时,可以使用 `` 元素的 `autowire` 属性为 Bean 指定自动装配模式。自动装配模式有以下类型: | 模式 | 说明 | | ------------- | ---------------------------------------------------------------------- | | `no` | 默认值,未激活 Autowiring,需要手动指定依赖注入对象。 | | `byName` | 根据被注入属性的名称作为 Bean 名称进行依赖查找,并将对象设置到该属性。 | | `byType` | 根据被注入属性的类型作为依赖类型进行查找,并将对象设置到该属性。 | | `constructor` | 特殊 byType 类型,用于构造器参数。 | `org.springframework.beans.factory.config.AutowireCapableBeanFactory` 是 `BeanFactory` 的子接口,它是 Spring 中用于实现自动装配的容器。 #### @Autowired 注入过程 - 元信息解析 - 依赖查找 - 依赖注入(字段、方法) #### 自动装配的限制和不足 自动装配有以下限制和不足: - 属性和构造函数参数设置中的显式依赖项会覆盖自动装配。您不能自动装配简单属性,例如基础数据类型、字符串和类(以及此类简单属性的数组)。 - 自动装配不如显式装配精准。Spring 会尽量避免猜测可能存在歧义的结果。 - Spring 容器生成文档的工具可能无法解析自动装配信息。 - 如果同一类型存在多个 Bean 时,自动装配时会存在歧义。容器内的多个 Bean 定义可能与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map 实例,这不一定是问题。但是,对于期望单值的依赖项,如果没有唯一的 Bean 定义可用,则会引发异常。 > 自动装配的限制和不足,详情可以参考官方文档:[Limitations and Disadvantages of Autowiring 小节](https://docs.spring.io/spring/docs/5.2.2.RELEASE/spring-frameworkreference/core.html#beans-autowired-exceptions) ## 依赖注入方式 依赖注入有如下方式: | 依赖注入方式 | 配置元数据举例 | | --------------- | -------------------------------------------------- | | Setter 方法注入 | `` | | 构造器注入 | `` | | 字段注入 | `@Autowired User user;` | | 方法注入 | `@Autowired public void user(User user) { ... }` | | 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | ### 构造器注入 - 手动模式 - xml 配置元信息 - 注解配置元信息 - Java 配置元信息 - 自动模式 - constructor 构造器注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造 bean 几乎是等价的,并且本次讨论对构造函数和静态工厂方法的参数进行了类似的处理。 下面是一个构造器注入示例: ```java public class SimpleMovieLister { // the SimpleMovieLister has a dependency on a MovieFinder private final MovieFinder movieFinder; // a constructor so that the Spring container can inject a MovieFinder public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // business logic that actually uses the injected MovieFinder is omitted... } ``` 构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。 ``` package x.y; public class ThingOne { public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { // ... } } ``` 假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 `` 元素中显式指定构造函数参数索引或类型。 ```xml ``` 当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,例如 `true` ,Spring 无法确定 value 的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类: ```java package examples; public class ExampleBean { // Number of years to calculate the Ultimate Answer private final int years; // The Answer to Life, the Universe, and Everything private final String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` 构造函数参数类型匹配 在上述场景中,如果您使用 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示: ```xml ``` 构造函数参数索引匹配 可以使用 `index` 属性显式指定构造函数参数的索引,如以下示例所示 ```xml ``` 构造函数参数名称匹配 ```xml ``` 可以使用 `@ConstructorProperties` 显式命名构造函数参数。 ```java package examples; public class ExampleBean { // Fields omitted @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` ### Setter 方法注入 - 手动模式 - xml 配置元信息 - 注解配置元信息 - Java 配置元信息 - 自动模式 - byName - byType Setter 方法注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。 以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。 ```java public class SimpleMovieLister { // the SimpleMovieLister has a dependency on the MovieFinder private MovieFinder movieFinder; // a setter method so that the Spring container can inject a MovieFinder public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // business logic that actually uses the injected MovieFinder is omitted... } ``` 在 Spring 中,可以混合使用构造器注入和 setter 方法注入。建议将构造器注入用于强制依赖项;并将 setter 方法注入或配置方法用于可选依赖项。需要注意的是,在 setter 方法上使用 `@Required` 注解可用于使属性成为必需的依赖项;然而,更建议使用构造器注入来完成这项工作。 ### 字段注入 手动模式(Java 注解配置元信息) - `@Autowired` - `@Resource` - `@Inject`(可选) ### 方法注入 手动模式(Java 注解配置元信息) - `@Autowired` - `@Resource` - `@Inject`(可选) - `@Bean` ### 接口回调注入 Aware 系列接口回调 | 內建接口 | 说明 | | -------------------------------- | ---------------------------------------------------------- | | `BeanFactoryAware` | 获取 IoC 容器- `BeanFactory` | | `ApplicationContextAware` | 获取 Spring 应用上下文- `ApplicationContext` 对象 | | `EnvironmentAware` | 获取 `Environment` 对象 | | `ResourceLoaderAware` | 获取资源加载器对象- `ResourceLoader` | | `BeanClassLoaderAware` | 获取加载当前 Bean Class 的 `ClassLoader` | | `BeanNameAware` | 获取当前 Bean 的名称 | | `MessageSourceAware` | 获取 `MessageSource` 对象,用于 Spring 国际化 | | `ApplicationEventPublisherAware` | 获取 `ApplicationEventPublishAware` 对象,用于 Spring 事件 | | `EmbeddedValueResolverAware` | 获取 `StringValueResolver` 对象,用于占位符处理 | ### 依赖注入选型 - 低依赖:构造器注入 - 多依赖:Setter 方法注入 - 便利性:字段注入 - 声明类:方法注入 ## 限定注入和延迟注入 ### 限定注入 - 使用 `@Qualifier` 注解限定 - 通过 Bean 名称限定 - 通过分组限定 - 通过 `@Qualifier` 注解扩展限定 - 自定义注解:如 Spring Cloud 的 `@LoadBalanced` ### 延迟注入 - 使用 `ObjectFactory` - 使用 `ObjectProvider`(推荐) ## 依赖注入数据类型 ### 基础类型 - 基础数据类型:`boolean`、`byte`、`char`、`short`、`int`、`float`、`long`、`double` - 标量类型:`Number`、`Character`、`Boolean`、`Enum`、`Locale`、`Charset`、`Currency`、`Properties`、`UUID` - 常规类型:`Object`、`String`、`TimeZone`、`Calendar`、`Optional` 等 - Spring 类型:`Resource`、`InputSource`、`Formatter` 等。 ### 集合类型 数组类型:基础数据类型、标量类型、常规类型、String 类型的数组 集合类型: - `Collection`:`List`、`Set` - `Map`:`Properties` ## 依赖处理过程 入口:`DefaultListableBeanFactory#resolveDependency` 依赖描述符:`DependencyDescriptor` 自定义绑定候选对象处理器:`AutowireCandidateResolver` `@Autowired`、`@Value`、`@javax.inject.Inject` 处理器:`AutowiredAnnotationBeanPostProcessor` 通用注解处理器:`CommonAnnotationBeanPostProcessor` - 注入注解 - `javax.xml.ws.WebServiceRef` - `javax.ejb.EJB` - `javax.annotation.Resources` - 生命周期注解 - `javax.annotation.PostConstruct` - `javax.annotation.PreDestroy` 自定义依赖注入注解: - 生命周期处理 - `InstantiationAwareBeanPostProcessor` - `MergedBeanDefinitionPostProcessor` - 元数据 - `InjectionMetadata` - `InjectionMetadata.InjectedElement` ## 依赖查找 VS. 依赖注入 | 类型 | 依赖处理 | 实现复杂度 | 代码侵入性 | API 依赖性 | 可读性 | | -------- | -------- | ---------- | ------------ | -------------- | ------ | | 依赖查找 | 主动 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 | | 依赖注入 | 被动 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 | ## 参考资料 - [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265)