本文连接:https://www.jianshu.com/p/df19a9ec8268
本文作者:gks09@qq.com
什么是依赖注入?
依赖注入(DI)是指对象定义它们自身依赖的过程。此过程可以通过配置构造方法的参数来定义依赖,也可以通过配置工厂方法的参数来获得依赖,还可以在对象实例化后配置属性来定义依赖。
Dependency injection (DI) is a process whereby objects define their dependencies, that is, the other objects they work with, only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean.
——Spring官方文档7.4.1 Dependency Injection
一般来说我们所需要的对象(可以是POJO,也可以不是POJO)会有诸多依赖(例如一个User对象有name、sex等属性,一个query服务对象依赖于数据库连接管理对象等),不使用Spring框架的时候,我们会在代码中使用new关键字来产生新对象;使用Spring框架后,可以通过xml配置、Java代码配置等方式,直接产生新对象并交由Spring框架(IoC container)统一管理。
原先对所有对象的控制权在于我们,我们在代码逻辑中控制对象;现在所有的对象配置好后,在项目执行阶段就完全交由Spring框架管理,包括对象的实例化、对象销毁、多个对象之间相互依赖等。这就是控制反转(Inversion of Control,IOC)。
参考:1、Spring官方文档(7. The IoC container)
依赖注入的优缺点?
优点
DI是一种编程思想,其目的是统一的对象和依赖管理。DI的优点是会更易于管理和解耦对象之间的依赖,使得代码更加的简单。对象不再关注依赖,也不需要知道依赖类的位置。这样的话,开发者的类更加易于测试,尤其是当开发者的依赖是接口或者抽象类的情况,开发者可以轻易在单元测试中mock对象。
Code is cleaner with the DI principle and decoupling is more effective when objects are provided with their dependencies. The object does not look up its dependencies, and does not know the location or class of the dependencies. As such, your classes become easier to test, in particular when the dependencies are on interfaces or abstract base classes, which allow for stub or mock implementations to be used in unit tests.
——Spring官方文档7.4.1 Dependency Injection
缺点
从Stack Overflow上,看到一些讨论,获得认同最多的回答如下:
A couple of points:
DI increases complexity, usually by increasing the number of classes since responsibilities are separated more, which is not always beneficial
Your code will be (somewhat) coupled to the dependency injection framework you use (or more generally how you decide to implement the DI pattern)
DI containers or approaches that perform type resolving generally incur a slight runtime penalty (very negligible, but it's there)
Generally, the benefit of decoupling makes each task simpler to read and understand, but increases the complexity of orchestrating the more complex tasks.
——What are the downsides to using Dependency Injection? [closed]
Stack Overflow的答者认为:
依赖注入增加了复杂度。有时候为了将不同对象的责任完全划分,会增加对象(类)的数量,并非总是有益。
代码可能会和依赖注入框架耦合。
依赖注入容器或实现方法需要进行类型解析,会带来轻微的不足。
目前笔者个人不太能深入理解依赖注入的缺点。个人认为,依赖注入会增加代码量(但好的DI框架可以帮助我们减少代码),在项目开始前需要对不同对象、不同业务进行划分,对开发者有一定的要求。
当然依赖注入的缺点,本身就是一个有争议的话题。没有一种pattern是完美的。
参考:1、What are the downsides to using Dependency Injection? [closed]
结合Spring框架谈DI
结合Spring Framework谈一谈依赖注入,Spring Framework做了很多工作,避免依赖注入和业务代码耦合,有助于我们专注于对象、依赖、业务的设计。Spring帮助我们产生、管理、使用、销毁bean,不需要我们手动去操作bean。不过带来的缺点就是Spring框架较为复杂,上手慢,用得不好会适得其反。
Spring的依赖注入方式
本节主要引用Spring官方文档。
基于构造方法的注入
基于构造函数的依赖注入是由IoC容器来调用类的构造函数,构造函数的参数代表这个Bean所依赖的对象。跟调用带参数的静态工厂方法基本一样。下面的例子展示了一个类通过构造函数来实现依赖注入的。
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
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的参数为简单类型时(如Java基本类型、String等),可以使用type
字段进行配置:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
也可以使用index
字段进行配置,指明构造函数的参数index:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
同样,也可以使用参数名进行配置:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
需要注意的是,做这项工作的代码必须启用了调试标记编译,这样Spring才可以从构造函数查找参数名称。开发者也可以使用@ConstructorProperties注解来显式声明构造函数的名称,比如如下代码:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于setter方法的注入
基于setter函数的依赖注入则是容器会调用Bean的无参构造方法,或者无参数的工厂方法,然后再来调用setter方法来实现的依赖注入。有的时候,会有对象相互依赖的情况,如A依赖B、B也依赖A。这时候无法通过含参的构造方法来注入A和B,需要先调用无参构造方法,然后用过Setter设置属性。
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;
}
}
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
ApplicationContext所管理Bean对于基于构造函数的依赖注入,或者基于Setter方式的依赖注入都是支持的。同时也支持使用setter方式在通过构造函数注入依赖之后再次注入依赖。开发者在BeanDefinition中可以使用PropertyEditor实例来自由选择注入的方式。然而,大多数的开发者并不直接使用这些类,而是跟喜欢XML形式的bean定义,或者基于注解的组件(比如使用
@Component
,@Controller
等)或者在配置了@Configuration
的类上面使用@Bean
的方法。
本质上,使用annotation进行也是使用基于构造方法的注入或基于setter的注入。
如何选择注入方式?
一般来说,我们可以混用基于构造方法的注入和基于setter的注入。Spring建议我们,使用constructor-based dependency injection进行强制依赖的注入,使用setter-based dependency injection 进行可选依赖的注入。当然,可以在Setter方法上面的@Required
注解可用来构造必要的依赖。
Constructor-based dependency injection有几点好处:它可以保证注入的依赖是非null
的,并且该组件被他人调用时一定是完好的。此外要注意,构造方法不应有过多参数,参数过多说明该对象的功能过复杂,应考虑拆分。
Setter-based dependency injection的好处是,可以进行重配置和重新注入。Setter-based dependency injection应给对象的属性附初始值,否则在使用时需要进行非null
检查。
依赖注入的解析过程
Spring容器首先会实例化ApplicationContext,然后依据xml、annotation或java代码进行bean的加载。如果bean依赖于其他bean,则依赖的bean会先加载。
在容器创建后,容器会验证bean的配置,但是bean的属性没有被赋值。单例类型的bean会先预实例化(pre-instantiated)。bean只有被请求时,才会真正创建。
创建bean时,实际上是创建了一个依赖图,容器按此顺序进行bean的创建。
依赖注入的实现方法
待补充