Spring之IoC容器
RT,断断续续写……
Spirng
插件:http://spring.io/tools/sts/all
You can use start.spring.io to generate a basic project or follow one of the “Getting Started” guides
日志:http://blog.csdn.net/yycdaizi/article/details/8276265
控制反转
任何一个有实际意义的应用都是由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。如果相互协作的对象(即它所依赖的对象)的引用要由每个对象自己负责管理,将会导致高度耦合和难以测试的代码。
耦合具有两面性,一方面紧密耦合的代码难以测试、复用和理解,另一方面一定程度的耦合又是必须的。
如果从具体对象手中交出控制,即把对象的依赖关系交给负责协调系统各个对象的第三方组件管理,就可以解耦代码、提高可测试性——这就是控制反转(Inversion of Control ,IoC)。控制反转主要有两种方式来实现:一是依赖查找(Dependency Lookup或Service Locator),二是依赖注入(dependency injection,DI),将对象自己去“取”依赖的职责反转为框架或IoC容器主动向对象“给”依赖。在Spring中,使用的是依赖注入,即在创建对象时设定依赖,注入到它们的对象中去,具体指的是对象实例通过构造器参数、工厂方法参数、(由构造器或工厂方法得到对象后的)属性设置来获得依赖对象的过程。
通过下例来感受几种管理依赖关系的方式:
(1)原始的,Human的构造器中自行创建了Eyes接口的一种实现BigBlueEyes,这使得Human与BigBlueEyes紧耦合在了一起,极大地限制了Human。更为糟糕的是难以对Human编写单元测试,当Human的see()方法被调用时,无法测试BigBlueEyes的see()方法是否被调用。1
2
3
4
5
6
7
8
9public class Human implements Life{
private Eyes eyes;
public Human(){
eyes = new BigBlueEyes();
}
public void see(){
eyes.see();
}
}
(2)通过依赖查找,Human一定程度上解耦了,然而与BigBlueEyes的关系过于隐式,需要通过文字来查找,而且依然不方便测试。此处化简了JNDI代码。1
2
3
4
5
6
7
8
9public class Human implements Life{
private Eyes eyes;
public Human(){
eyes = new ServiceLocator().getService("BigBlueEyes");
}
public void see(){
eyes.see();
}
}
(3)用构造器注入的形式(依赖注入的方式还有 Interface注入和setter注入),我们在构造Human时才把Eyes作为构造器参数传入,Human也没有与任何特定的Eyes实现发生耦合,这个Human长了什么样的Eyes,要等到Eyes的某种实现被注入时才知道。通过依赖注入和面向接口,Human与Eyes变成松耦合的关系。在测试时,用mock对象框架可以方便地进行单元测试,验证Eyes的eye()方法是否只被调用了一次。1
2
3
4
5
6
7
8
9public class Human implements Life{
private Eyes eyes;
public Human(Eyes eyes){
this.eyes = eyes;
}
public void see(){
eyes.see();
}
}
——参考自Inversion of Control Containers and the Dependency Injection pattern和这篇。
Bean 概述
在Spring中,由Spring IoC容器管理的对象称为Bean
。容器通过XML、Java annotations或者Java code形式的configuration metadata
来获得bean和它们之间的关系。
以下都先以XML为例。先做一个简单的示例。
在Maven的pom.xml中加入对log4j(log可以给我们提供很多的信息)和Spring的依赖。1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
添加x/y/beans.xml,默认bean的singleton-scoped(一个容器只维护一个单例)和pre-instantiated(ApplicationContext容器初始化启动后立刻创建配置bean,并完成依赖注入)。1
2
3
4
5
6
7<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
<bean id="foo" class="x.y.Foo"/>
</beans>
1 | package x.y; |
1 | BasicConfigurator.configure(); //配置log4j |
执行程序,正常,还可以看到一行行的log输出。
Bean的命名
Bean的创建方式
(1) 构造器
这是最普遍的一种方式。1
<bean id="foo" class="x.y.Foo"/>
1 | public class Foo { |
(2)静态工厂方法
有时候静态工厂方法是实例化对象的唯一方法。1
<bean id="bar" class="x.y.Foo" factory-method="createBar"/>
1 | public class Foo { |
更常见的是GoF中的单例模式。1
<bean id="foo" class="x.y.Foo" factory-method="createInstance"/>
1 | public class Foo { |
(3)非静态工厂方法
容器通过一个现有已实例化的bean的非静态工厂方法创建一系列的产品,类似于抽象工厂方法设计模式。1
2
3<bean id="foo" class="x.y.Foo"/>
<bean id="bar" factory-bean="foo" factory-method="createBar"/>
<bean id="baz" factory-bean="foo" factory-method="createBaz"/>
1 | public class Foo { |
注意区别,class + factory-method 是用class的静态factory-method创建bean,而 factory-bean + factory-method 是用factory-bean的非静态factory-method创建bean,创建的bean都不一定与class、factory-bean同类。
在Spring文档中,通过静态或非静态工厂方法创建对象,由IoC容器配置管理的bean称为factory bean
。区别于Spring中的FactoryBean
。
FactoryBean 专指实现了FactoryBean接口的bean,是一个能够产生或者修饰对象生成的工厂bean。通过定制自己的FactoryBean,可以用Java code代替冗长的XML更好地表示bean复杂的初始化过程。当调用应用上下文的getBean(…)方法时实际得到的是FactoryBean的产品,即调用了bean实现的FactoryBean接口提供的getObject(…)方法。IoC容器的初始化逻辑本身支持FactoryBean的插入,FactoryBean接口对于Spring框架来说有重要的地位,Spring自身就提供了50多个FactoryBean的实现,它们隐藏了一些复杂bean的实例化细节,给上层应用带来便利。
Bean的依赖注入
Bean的装配方式
(1)基于创建的依赖注入
用< constructor-arg >元素表示。在很多地方都叫做 Constructor-based dependency injection,未免令人容易误会,因为不只构造器,创建方式中的静态和非静态工厂方法当然也可以进行依赖注入。1
2
3
4
5
6
7
8
9<bean id="bar" class="x.y.Bar"> <!--构造器-->
<constructor-arg value="15"/>
<constructor-arg>
<bean class="x.y.Baz"/> <!--内部bean-->
</constructor-arg>
</bean>
<bean id="foo" class="x.y.Foo" factory-method="createInstance"> <!--工厂-->
<constructor-arg ref="bar"/> <!--引用bean-->
</bean>
1 | public Bar{ |
1 | public class Foo { |
Bar的构造参数num和baz在XML中默认按顺序匹配对应。另外还可以利用< constructor-arg >元素的type、index、name等属性显示地去匹配构造器、工厂方法所需的参数。
(2)基于setter的依赖注入
用< property >元素表示。在调用构造器或静态工厂方法实例化bean后,容器使用setter方法完成依赖注入。1
2
3
4
5
6<bean id="foo" class="x.y.Foo">
<property name="num" value="15"/>
<property name="bar">
<bean class="x.y.Bar"/>
</property>
</bean>
1 | public class Foo{ |
一般,构造器用于注入必须的依赖,setter用于注入可选的依赖。另外,对setter方法声明@Required可令属性成为必选的。
Bean的装配类型
简单值:由< property > and < constructor-arg >的value属性指定,再从文字String转换成bean中真正对应的类型。 还可以用内嵌的< value >元素替换value属性。有时候还可以作为java.util.Properties使用,与后面的集合< props >元素是等价的。
1
2
3
4
5
6<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>< idref >元素:获得bean的id值(一个字符串),而不是bean的实例。bean属性指定引用的bean的id,比直接用value属性的方式,容器会在部署时验证所引用的bean是否存在。注:< bean >元素的name属性可被视为bean的别称id,广义上也是id。
- 引用bean,即< ref >元素:< property > and < constructor-arg >的ref属性实际是内部< ref bean=”…” />的简写,bean属性指定引用的bean的id。
- 内部bean,即< bean >元素:容器自动忽视id、name和scope等属性,随着外部bean产生。
集合
- < array >:对应数组
- < list >:对应List
- < set >:对应Set
- < map >:对应Map,名称和值都可是任意类型。< enrty >元素的key属性和value属性是字符串形式,key-ref属性是子元素< key >的子元素 < ref bean=”…” /> 的简写,value-ref属性是子元素 < ref bean=”…” /> 的简写(就是key所对应的value了,不要再写< value >元素了),这样对于只由字符串或bean组成的map,只要一行< entry >就可以写好了。
- < props >:对应Properties,名称和值都必须是String类型。
1
2
3
4
5
6<property name="properties">
<props>
<prop key=" jdbc.driver.className">com.mysql.jdbc.Driver</prop>
<prop key="jdbc.url">jdbc:mysql://localhost:3306/mydb</prop>
</props>
</property>
空值,< null >元素对应null关键字,类似setText(null),而value=””实际是setText(“”)。
更多可参看XML的Elment和Content Model给出的说明。
奇淫巧技:
引入c-命名空间xmlns:p="http://www.springframework.org/schema/c"
后,简化< constructor-arg >元素。引入p-命名空间xmlns:p="http://www.springframework.org/schema/p"
后,简化< property >元素。1
2
3
4
5
6
7
8
9<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<bean id="foo" class="x.y.Foo">
<constructor-arg name="first" value="123"/>
<constructor-arg name="myBar" ref="bar"/>
<property name="second" value="456"/>
<property name="myBaz" ref="baz"/>
</bean>
1 | <bean id="foo" class="x.y.Foo" |
除了可以对应参数或者属性的name,也可以按序等。
Bean的装载模式
默认情况下,ApplictaionContext容器(BeanFactory总是懒汉)动就会创建、配置所有的singleton型bean,这就是pre-instantiation模式(饿汉),有助于尽早发现配置或环境的问题。另一种相反的模式是lazy-initialization(懒汉),在向容器索取bean时对bean进行创建。装载模式在XML中由< bean >元素的lazy-init属性控制,若为true则是懒汉模式。1
<bean id="foo" class="x.y.Foo" lazy-init="true"/>
如果某个饿汉对懒汉有依赖关系,懒汉也就不能懒了,必须创建、配置后被注入饿汉bean。
容器级别(应该是配置文件级别,因为容器可以有多个配置文件?)的装载模式由< beans >元素的< default-lazy-init >属性控制。
Bean的自动装配
引用bean时,总是要去找对应的id,而且重复写大量的相似代码,好累。为bean再增加依赖时,还要给XML再增加代码,好累。看看我上面的代码,似乎总习惯把bean名称和要注入的属性名称取同一个,那容器能不能聪明点,就按这种习惯帮我装配?这就是自动装配的一种,byName,按名称装配。
自动装配(aotowiring)就是Spring自动识别并装配Bean的依赖关系。分为4种类型,可通过< bean >元素的autowire属性指定:
- no:默认,不自动装配。
- byName:根据属性名称(或ID)自动装配,减少< property >元素,基于setter方法的依赖注入。如有属性名称为bar,则自动寻找名为bar的bean,若有则调用setBar(Bar)方法进行注入。
- byType:根据属性类型自动装配,减少< property >元素,基于setter方法的依赖注入。如有属性类型为Bar,则自动寻找类型为Bar的bean,若有则调用setBar(Bar)方法进行注入。
- constructor:根据属性类型装配,减少< constructor-arg >元素,基于构造器的依赖注入。如有构造器的参数类型为Bar和Baz,则自动寻找类型为Bar和Baz的bean,若有则调用构造器进行注入。
怎么决定用哪种装配类型? < beans >元素的default-autowire属性可设置全局,< bean >元素的autowire属性可以覆盖全局,< bean >元素的子元素< constructor-arg >或< property >又可以再覆盖bean上的作用。即XML一般以内为准。如下所示,如果foo以全局byType找,就会报错,如果以< bean >上的byName找,就应是名为’bar’的bean,而实际上,< property >明确指出foo需要的是’goodBar’的bean。1
2
3
4
5
6
7
8
9
10
11
12<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd"
default-autowire="byType">
<bean id="foo" class="x.y.Foo" autowire="byName">
<property name="bar" ref="goodBar"/>
</bean>
<bean id="goodBar" class="x.y.Foo"/>
<bean id="bar" class="x.y.Foo"/>
</beans>
匹配到的bean不是唯一的? 除了已举例说明的恰好找到唯一的完美情况,当然还有:
- 零:如果byName和byType找不到对应的属性名称或类型的bean,则不会进行注入,就像< property >常用于可选的属性。而如果constructor找不到对应的构造器参数类型的bean,则会提示错误,< constructor-arg >常用于必选的属性。
- 多:重复的bean名称一般IDE就先不会允许了,在初始化容器时也会早早报错,因此byName无需苦恼。但是byType和constructor这些根据类型装配的模式,在面对多种同类型的bean时就会报错,如果可以设置其中只有一个bean允许被自动装配,其他不能,那就没有问题了。
怎么设置bean允许被自动装配?
- < bean >元素的primary属性,起的是突出某一个bean的作用。默认所有候选bean的primary为false(第3版的《Spring技术内幕》时是true,应该是Spring早期版本的失误),如果恰好只有一个候选bean的primary为true,则表示突出的,被选出装配。
- < bean >元素的autowire-candidate属性,用于设置bean是否允许被自动装配。默认所有候选bean的autowire-candidate为true,表示允许被自动装配。比起primary它有更大的灵活性。(including annotation style configurations such as @Autowired). 顶层的< beans >元素还有个default-autowire-candidates属性,通过对bean的名称进行模式匹配筛选并设置哪些bean允许被自动装配。例如default-autowire-candidates=”*Dao,*Service”表示所有以Dao或Service名称结尾的bean允许被自动装配,其他bean被排除。依然是内部为王, < bean >元素的autowire-candidate属性覆盖全局的default-autowire-candidates属性。
注意区别,禁止“被”自动装配的bean表示不能被自动注入到别的bean中,不表示不可以自动装配别人,被动由上面这几个属性决定。主动是由autowire属性决定的。
自动装配的利与弊
- 自动装配不仅减少了配置< constructor-arg >或< property >元素,也有利于对象的版本升级。
- 但是显然,简单属性例如基本类型、java.lang.Class、java.lang.String类、 基本类型数组等无法自动装配。让Spring干太多猜测超出XML文档的事情,容易失控。匹配到多个bean的事情也时常发生。因此自动装配应当慎用,或者考虑用Annotation-based容器配置控制更多的自动装配细节。
Spring IoC 内幕
Spring框架中IoC容器的基础是.org.springframework.beans和org.springframework.context包,有两个主要的容器系列:一个是实现BeanFactory
接口的简单容器系列,它们提供了一种能够管理任何类型对象的配置机制;另一个是实现 BeanFactory 的子接口ApplicationContext
应用上下文的高级容器系列,它们增加了Spring AOP特性简单集成、消息资源处理(国际化)、事件发布和应用层具体上下文如用于Web应用的WebApplicationContext。
在Spring中,由Spring IoC容器管理的对象称为Bean
。在最早的版本中,Spring被设计用来管理JavaBean,而现在Spring已经可以管理任何对象,即使它不具备默认构造器或getter和setter方法这些JavaBean的特性,但“Spring bean”这个术语仍然被保存了下来。Spring bean可以说POJO,甚至还可以是EJB对象等。
JavaBean是符合一定规范编写的Java类,不是一种技术,而是一种规范。JavaBean规范包括:
- 所有属性为private
- 提供公共的无参构造函数
- 提供getter和setter
- 实现serializable接口
Sun最早于1996年制定了JavaBean 标准的类,最初的想法是将JavaBean打造成能够被构建工具可视化地操作的可重用的组件。
POJO是Plain Old Java Object的缩写,是除了Java语言规范外没有任何限制的Java对象。POJO不应继承任何外部预先设定的类,不应实现任何外部预先设定的接口,不应包含任何外部预先设定的注释(虽然实际上很多声称纯POJO的软件产品和框架仍然需要使用预先设定的注释,如用作持久层。因此如果移除注释后是POJO的话,则该对象就是POJO)。
早期版本的很多框架都是重量型框架,强迫应用继承它们的类或实现它们的接口,从而让应用跟框架绑死,难以编写测试代码,如EJB 2的无状态会话bean必须实现SessionBean接口。后来发展了很多轻量型框架,不强迫业务对象遵循平台特定接口,允许开发者在POJO中实现业务逻辑,使应用不依附于某种框架,虽然相对于重量级框架,轻量型框架的处理能力也有所下降。
其实JavaBean就是一个serializable、有无参构造函数的、允许用getter和setter方法访问属性的POJO。而POJO就是简单的JavaBean。如果严格遵循JavaBean规范,类必须实现serializable接口,就会略微打破POJO模型。
BeanFactory和ApplicationContext
前面介绍过,BeanFactory接口是容器始祖,DefaultListableBeanFactory是它的一个实现类,现在我们以下面的3行代码的基本容器替代上面示例ApplicationContext ctx=new ClassPathXmlApplicationContext("x/y/beans.xml")
这行代码。
1 | DefaultListableBeanFactory ctx=new DefaultListableBeanFactory(); |
程序依然正常。但是log确实不一样了,会发现BeanFactory的确比ApplicationContext简陋。来看看ApplicationContext在源码上都多了些啥功能。在ClassPathXmlApplicationContext的构造器中,实际调用了其高祖(即爹的爹的爹的爹)AbstractApplicationContext抽象类的refresh()
方法,ApplicationContext的多功能基本都体现在这了。
1 |
|
那3行代码只是做了和obtainFreshBeanFactory()
方法同样的工作。1
2
3
4
5
6
7
8protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
其中refreshBeanFactory()
方法是抽象方法,真正的实现由子类AbstractRefreshableApplicationContext来完成(模板方法的设计模式)同样,DefaultListableBeanFactory beanFactory = createBeanFactory()
先创建容器。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
而loadBeanDefinitions(beanFactory)
也是抽象方法,真正的实现由子孙AbstractXmlApplicationContext完成,代码类似,至此可以确定,基于BeanFactory的那3行代码的工作被包含在了ApplicationContext的obtainFreshBeanFactory()
方法中完成。
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { |
IoC容器初始化
从上面基于BeanFactory和基于ApplicationContext的两种方式看出,loadBeanDefinitions(...)
的重载方法们作为主力承担了IoC容器的狭义初始化的任务。其中的主要工作,由XmlBeanDefinitionReader的doLoadBeanDefinitions(...)
方法进行体现:1
2Document doc = doLoadDocument(inputSource, resource); //读入并解析XML文档
return registerBeanDefinitions(doc, resource); //将XML中定义好的bean转换成BeanDefinition,向容器注册
继续深入,发现交由BeanDefinitionDocumentReader接口的实现类DefaultBeanDefinitionDocumentReader来负责解析和注册bean。1
2
3
4
5
6public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
DefaultBeanDefinitionDocumentReader类处理多种元素,其中< bean >元素由processBeanDefinition(...)
方法负责。最终,由BeanDefinitionParserDelegate代理类的parseBeanDefinitionElement(...)
实现解析,DefaultListableBeanFactory类的registerBeanDefinition(...)
方法实现注册。详细可看《Spring技术内幕》一书。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// 把< bean >解析成相应的BeanDefinition
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// 向容器注册BeanDefinition
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
总结起来,IoC容器的狭义初始化工作内容包括:
- 对configuration metadata进行资源定位、载入
- 把用户定义好的bean解析成容器内部的数据结构,即BeanDefinition
- 向容器注册这些BeanDefinition
BeanFactory和ApplicationContext容器的初始化都视为广义初始化,但实际后者包含了前者的工作,即狭义初始化部分。
BeanDefinition中包含了关于bean的创建方式、依赖注入方式、装配类型、装配模式、范围scope等信息。以后,容器就可以通过BeanDefinition来创建、注入和管理Bean。
依赖注入
IoC容器的初始化过程不包括依赖注入。依赖注入通常发生在应用第一次通过getBean(...)
向容器索取Bean的时候。
在ApplicationContext应用上下文容器中,finishBeanFactoryInitialization(beanFactory)
会预实例化(pre-instantiation)所有no-lazy-init的singleton型Bean,而no-lazy-init的singleton型也是Bean的默认形态,预实例化会完成依赖注入。虽然这种容器的使用方式会对容器一开始的性能有一些影响,但却能够提高应用第一次取得Bean的性能,因为依赖注入早已完成。通过< bean >元素的属性lazy-init=”true”,开发者显示地向容器索取Bean时才进行实例化和依赖注入。BeanFactory容器则完全无视lazy-init属性设置,永远是懒汉模式。