企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
依赖注入(DI)是对象之间定义依赖关系的过程,即协作对象,通过构造参数,工厂方法参数,或在对象实例化之后通过设置属性的方式注入.容器在创建bean的时候会注入这些依赖,这和bean自己控制本身的实例化,或通过类的构造指定依赖位置或服务定位模式创建对象的过程是相反的,因此叫控制反转(IoC). 依赖注入的原则使代码更清晰,而且依赖的对象之间是解耦的.对象不用查看它的依赖关系,也无需知道依赖关系的类和位置.因此你的类更容易测试,尤其当类实现了接口或抽象类,在单元测试中可以使用存根或模拟实现. 依赖注入主要有两种形式,构造方式注入和set方法注入 [TOC] ## Constructor-based dependency injection 基于构造方法注入由容器调用带参的构造方法完成,每一个参数代表一个依赖.调用静态工厂方法指定参数构造对象也是类似的.下面示例通过构造方法注入依赖,主意,这就是一个普通的pojo类,没有实现接口,没有注解, ~~~java public class SimpleMovieLister { // 依赖MovieFinder private MovieFinder movieFinder; // 构造方法,需要一个参数 MovieFinder,spring容器可以注入依赖 public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } } ~~~ ### Constructor argument resolution 构造参数的解析是按类型匹配的,如果参数之间不存在混淆的情况,参数的顺序和bean定义顺序保持一致.如下: ~~~java package x.y; public class Foo { public Foo(Bar bar, Baz baz) { // ... } } ~~~ 这两个类Bar和Baz不存在混淆,没有相同的继承关系,所以,如下配置就行,不需要在元素`<constructor-arg/>`中特别指定参数的位置或类型: ~~~xml <beans> <bean id="foo" class="x.y.Foo"> <constructor-arg ref="bar"/> <constructor-arg ref="baz"/> </bean> <bean id="bar" class="x.y.Bar"/> <bean id="baz" class="x.y.Baz"/> </beans> ~~~ 当参数是引用了一个已知的bean,那么可以正确匹配(如上示例),当参数为基本数据类型,如<value>true</value>时,spring无法确定这个值的类型,还需要更多帮助的才能正确匹配.如下: ~~~java package examples; public class ExampleBean { // 计算最终的年数 private int years; // 生活,学习等 private String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ~~~ ### Constructor argument type matching 在上面的场景,容器能用`type`属性来匹配基本数据类型,例如: ~~~xml <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg type="int" value="7500000"/> <constructor-arg type="java.lang.String" value="42"/> </bean> ~~~ ### Constructor argument index 使用`index`明确指定参数的索引,例如 ~~~xml <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg index="0" value="7500000"/> <constructor-arg index="1" value="42"/> </bean> ~~~ 如果是是两个相同类型的构造参数,使用索引来避免混淆.注意索引是从0开始的 ### Constructor argument name 也可以使用参数的名称来避免混淆 ~~~xml <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg name="years" value="7500000"/> <constructor-arg name="ultimateAnswer" value="42"/> </bean> ~~~ 请记住,为了保证这种方式正确工作,必须在debug模式下通过编译,让spring通过构造方法找到参数,如果debug模式没有通过,你可以使用java注解@ConstructorProperties 指定参数名称,如下: ~~~java package examples; public class ExampleBean { // 属性忽略 @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ~~~ ## Setter-based dependency injection 基于setter方法的依赖注入是由容器在实例化bean之后,调用setter方法来完成的. 下面示例纯setter方法注入,就是个普通的java类 ~~~java public class SimpleMovieLister { // 依赖 MovieFinder private MovieFinder movieFinder; // setter方法,容器用来注入依赖 public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } } ~~~ ApplicationContext支持基于构造参数和setter方法的依赖注入.也支持在构造参数注入之后再使用setter方式注入.结合`PropertyEditor `实例改变属性格式,以`BeanDefinition`的形式配置依赖关系.然而大多数spring用户不使用这些类(即编程方式),而是用xml定义bean,注解组件(@Component, @Controller等注解的类),或者java注解@Configuration类中的@Bean方法.这些资源在内部转换为`BeanDefinition `,并用于加载整个spring容器实例. > :-: Constructor-based or setter-based DI? > > 基于构造参数和setter方式的依赖注入可以混合使用,较好的经验是,构造参数用于基础依赖,setter方法用于可选的依赖.注意使用`@Required`注解的setter方法是必须的. > spring提倡构造参数是不变的且不是null.此外,构造参数对于代码调用者是完全初始化状态.从侧面说明,太多构造参数的代码是糟糕的,表示这个类有太多责任,应该考虑重构把相关的内容分割到合适的地方. > setter主要应用于可选的依赖,这些依赖在类中分配合适的默认值.另外,使用依赖的地方需要做非空坚持.使用setter注入的一个好处就是可以重新配置或注入对象.`JMX MBeans`的管理就是setter的注入方式 >依赖注入的风格对一些特殊类很有意义,当处理第三方类且没有源码,你没有可选的.例如第三方类库,没有暴露出任何setter方法,那么构造注入的方式是依赖注入的最后一句话. > ## Dependency resolution process 容器处理依赖的过程表现为以下几步: * ApplicationContext创建并初始化,且包含所有配置的bean,配置信息可以使用xml,java 代码或注解. * 每一个bean,它的依赖表现为属性,构造参数,静态方法的参数(当做构造参数),只有当bean实际创建的时候才提供这些依赖给它. * 每一个属性或构造参数实际上定义的一个需要设置的值,或引用容器中的另一个bean * 作为值的属性或构造参数,从指定格式转为实际类型.默认spring可以吧字符串的值转为所有内置类型,如int,long,String等. 容器创建的时候会校验所有bean的配置信息.然而,bean的属性在bean创建的时候才会设置.bean默认是单例且预先实例化的,随容器创建而创建.bean的范围定义参考[Scope](https://docs.spring.io/spring/docs/5.0.6.RELEASE/spring-framework-reference/core.html#beans-factory-scopes).另外一种就是在需要的时候才创建bean,然后在依次创建依赖,这种机制在第一次使用的时候回表现的会慢一点. >:-: 循环依赖 > >如果你主要使用构造方法注入,可能导致无法解决的循环依赖. >比如,class A需要通过构造方法注入一个class B的实例,同时class B也需要通过构造方法注入一个class A的实例,通过配置使A和B之间相互注入,spring容器在运行时会检测到循环引用,然后抛出异常`BeanCurrentlyInCreationException` >一种解决方案就是,把构造注入方式改为setter方式,换句话说,可以用setter方式配置循环依赖 > 你要相信spring会做正确的事情.在容器加载时,校验配置问题,如引用了不存在的bean和循环依赖.bean创建时,spring会尽可能晚的设置属性解决依赖.这意味着容器正确加载之后,当你需要一个创建失败的bean或依赖也会产生异常.例如,找不到这个bean的异常或无效的属性.ApplicationContext默认实现预先实例化单例bean的机制会把配置问题的暴露向后推迟.想要在创建ApplicationContext的时候就发现配置问题,在实际需要对象之前就花费时间和内存预先把对象创建好.你还可以重写默认的行为使单例bean懒加载替换预先实例化. 如果没有循环依赖,当协作bean注入依赖bean之前,协作bean优先完全配置好.就是说A依赖B,spring会优先配置好B之后再调用set方法把B注入A 总之,bean先实例化,然后设置依赖,再调用相关生命周期方法. ## Examples of dependency injection 下面是setter方式注入 ~~~xml <bean id="exampleBean" class="examples.ExampleBean"> <!--使用子元素ref--> <property name="beanOne"> <ref bean="anotherExampleBean"/> </property> <!--使用属性ref--> <property name="beanTwo" ref="yetAnotherBean"/> <property name="integerProperty" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~java public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public void setBeanOne(AnotherBean beanOne) { this.beanOne = beanOne; } public void setBeanTwo(YetAnotherBean beanTwo) { this.beanTwo = beanTwo; } public void setIntegerProperty(int i) { this.i = i; } } ~~~ 接下来是构造方法的注入 ~~~xml <bean id="exampleBean" class="examples.ExampleBean"> <!-- 子元素ref--> <constructor-arg> <ref bean="anotherExampleBean"/> </constructor-arg> <!-- ref属性 --> <constructor-arg ref="yetAnotherBean"/> <constructor-arg type="int" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~java public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public ExampleBean( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { this.beanOne = anotherBean; this.beanTwo = yetAnotherBean; this.i = i; } } ~~~ 接下来是静态工厂方法 ~~~xml <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance"> <constructor-arg ref="anotherExampleBean"/> <constructor-arg ref="yetAnotherBean"/> <constructor-arg value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~java public class ExampleBean { // 私有的构造方法 private ExampleBean(...) { ... } //静态工厂方法,参数可 认为是这个bean的依赖 public static ExampleBean createInstance ( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { ExampleBean eb = new ExampleBean (...); // some other operations... return eb; } } ~~~ 静态工厂方法的参数使用`<constructor-arg/>`元素,等同于构造参数,但这里不一定就返回这个bean本身,也可以是其他bean.实例工厂方法(非静态)的使用和这里是一样的,就不细说了.