一个能够发挥功能的应用免不了各个组件之间相互协作,并随着项目的复杂度变高而变得复杂,这些协作就是所谓的依赖。传统的做法是每个对象负责管理与自己相关的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。比如:
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
在这个例子中:
- DamselRescuingKnight的构造函数中构造了一个RescueDamselQuest对象,使得两个对象紧耦合。
- 单元测试困难:必须保证当embarkOnQuest()调用的时候,对象quest的方法也能被成功调用
紧耦合带来的副作用比较多,比如在更改RescueDamselQuest类中的代码的时候可能会牵扯到以之作为属性的类;当我们需要RescueDamselQuest的功能的时候用这种方式固然比较快,但是要使用另一个类似功能的时候又得去管理其他对象的引用,每增加一个功能就牵扯到多个组件的代码。
依赖注入可以从一定程度上解决这种紧耦合带来的问题
POJO
通常被称为Plain Ordinary Java Object,也就是普通的Java对象。它的内在含义是指那些从未从任何类继承、也没有实现任何接口、更没有被其他框架侵入的Java对象。这样的Java对象简单灵活,能够任意扩展。Spring的依赖注入能够发挥POJO的潜能,使得它们在没有被侵入的情况下发挥组件作用,这样既能实现功能又能保持组件之间的松耦合性。通常POJO有一些private的参数作为对象的属性。然后针对每个参数定义了get和set方法作为访问的接口。例如:
public class User {
private long id;
private String name;
public void setId(long id) {
this. id = id;
}
public void setName(String name) {
this. name=name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
}
依赖注入
所谓依赖注入,就是通过第三方组件管理协调对象之间的依赖关系,对象无需自行创建或管理它们之间的依赖关系。
依赖关系将被自动注入到需要它们的对象中去。下面通过一个例子说明一下:
public class BraveKnight() {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public embarkOnQuest() {
quest.embark();
}
}
这个例子和第一个例子对比的区别是,它并没有在构造函数中自行创建一个对象去然后去管理,而是在构造器中作为参数传入了一个对象。这就其中一种依赖注入的方式,即构造器注入(constructor injection)。
更重要的是,传入的Quest类只是一个接口,这意味着所有继承了该接口的类都可以传入并在BraveKnight中发挥作用,就不用每增加一个具体功能就要在类中重新管理一个新的对象。当然还可以传递多个参数给构造函数。
而且我们可以很方便的对该类进行测试,这里用一个mock实现就能对其进行测试。
mock可以创建模拟对象的实例,它强调业务逻辑的连通性,一般用于单例测试和集成测试。
import static org.mockito.Mockito.*; //一种导入类里的静态方法的方式
import org.junit.Test;
public class BraveKnithtTest() {
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class); //用mock静态方法创建实例
BraveKnight knight = new BraveKnight(mockQuest); //把mockQuest注入一个类中
knight.embarkOnQuest();
}
}
mock测试可以检测这个类是可用的。可是当我们有了一个继承了Quest的具体的类之后,这个类和BraveKnight之间是具体怎么协作的呢?
创建应用组件之间协作的行为通常称为装配(wiring),spring两种常用的装配方式是xml文件装配和Java语言装配。比如SlayDragonQuest类继承了Quest类,那么使用xml装配方式把它注入到BraveKnight中的方式如下:
在xml文件中这两个类都被声明为spring的bean,对于BraveKnight,它使用constructor-arg这个参数把quest这个bean作为自己的构造器参数,实现它对SlayDragonQuest的依赖。
在这个例子中,尽管BraveKnight依赖于Quest,但是它并不知道传递给它的是什么类型的Quest,这样我们可以随时通过改变传递给它的Quest实现不同的功能且不必改动其内部代码,这就是一种松耦合。
当需要启动应用的时候只需要使用spring的应用上下文(Application Context)装载bean的定义并把它们组装起来。对于上述使用xml文件配置的bean,可以用ClassPathXmlApplicationContext作为应用上下文,启动方式如下:
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");
Knight knight = context.getBean(Knight.class); //获取knight bean
knight.embarkOnQuest(); //使用knight
context.close();
}
}
控制反转(IoC)
提到了依赖注入,就不得不提一个同样很出名的术语“控制反转”。
控制反转包含了控制和反转两方面的内容。即某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。具体到例子中,这里的“某一接口”就是指的上面的Quest
接口,它在调用类BraveKnight
中使用哪个实现类的选择权不是由BraveKnight
本身决定的,而是由spring容器借由Bean配置(可以是xml也可以是java config)来进行控制的。由于控制反转这个术语不够直观,所以Martin Fowler使用了依赖注入(Dependency Injection)来解释这种模式。
BeanFactory和ApplicationContext
IoC容器底层是通过Java语言的反射机制实例化bean并建立Bean之前的依赖关系。Spring的IOC容器在实现这些基础功能的基础上,还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布,资源装载等高级服务。
Bean工厂(com.springframework.beans.factory.BeanFactory)是Spring框架最核心的接口,它提供了高级IoC的配置机制。BeanFactory使得管理不同类型的Java对象成为可能,应用上下文(com.springframework.beans.factory.ApplicationContext)建立的BeanFactory基础之上,提供了更多面向应用的功能,它提供了国际化支持和框架事件体系,更易于创建实际应用。我们一般称BeanFactory为Ioc容器,而称ApplicationContext为应用上下文。在某些场景下,也将ApplicationContext成为Spring容器。简单来说,BeanFactory是Spring框架的基础设施,面向Spring本身; ApplicationContext面向Spring框架的开发者,几乎所有的应用场合都可以直接使用ApplicationContext而非底层的BeanFactory。
ApplicationContext的初始化和BeanFactory有一个重大的区别:BeanFactory在初始化的时候,并未实例化Bean,直到第一次访问某个Bean时才实例化目标Bean;而前者则在初始化应用上下文时就实例化所有单实例的Bean。因此ApplicationContext的初始化时间要比BeanFactory稍长。