IOC 容器
IOC容器和beans
Spring实现了IOC (Inversion of Control)(控制反转)功能,IOC的实现手段是依赖注入。
依赖注入,一个类依赖其他类的对象,依赖的对象可以通过构造函数、工厂方法、或者一个在运行时set值的属性来实例化。这个类我们叫做bean,这个bean必须要自己去new
一个它依赖的对象,或者是显式地使用一个Service Locator模式来实例化以来的对象。
Spring IOC容器的基础包是org.springframework.beans
和 org.springframework.context
。BeanFactory
提供了管理bean对象的高等机制。ApplicationContext
是BeanFactory
的子类,它对Spring的几大特性做了整合:AOP(切面编程),消息资源处理(国际化超有用),事件处理机制以及应用层的特殊上下文如WebApplicationContext
。
简而言之,BeanFactory提供了配置框架的基本能力,而ApplicationContext提供了更加高级能力。ApplicationContext是BeanFactory的超集。
Spring中,bean就是那些构成了应用程序主干,并且由Spring管理的对象。它由Spring实例化,组装(注入),管理(生命周期、范围)。Spring利用配置元数据,通过反射实例化和组装bean。
容器概览
org.springframework.context.ApplicationContext接口提供了Spring容器的基本能力——实例化、配置、组装beans。Spring提供了配置元数据让我们描述、组装应用以及beans之间丰富的相互依赖性。提供配置元数据的方式除了传统的XML配置文件方式以外,还包括注解和基于java代码两种方式。后两者能显著减少代码中xml的重复代码数量。
配置元数据
如上图所示,开发者使用配置元数据来告诉Spring如何实例化、配置和组装beans。最传统的配置元数据书写方式就是XML。
下面是一个基于XML的配置元数据
<?xml version="1.0" encoding="UTF-8"?>
<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.xsd">
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>
实例化容器
我们可使用ApplicationContext来实例化Spring容器
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
下面是services.xml配置文件的内容
<?xml version="1.0" encoding="UTF-8"?>
<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.xsd">
<!-- services -->
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
下面是daos.xml的内容
<?xml version="1.0" encoding="UTF-8"?>
<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.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for data access objects go here -->
</beans>
容器的使用
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();
beans
bean的配置元数据定义了一个bean的以下几个方面:
- bean的所属类,当然类要使用全限定名
- bean的行为元数据,例如bean的类型(单例,原型)、生命周期回掉函数等等。
- 对其他bean的依赖,这些被依赖的bean可以叫做
collaborators
或者dependencies
. - 一些基本数值型的属性设置
bean的注册方式很灵活,除了让Spring容器根据配置元数据管理以外,还可以通过BeanFactory来注册。
BeanFactory bf = ((ClassPathXmlApplicationContext) ac).getBeanFactory();
((ConfigurableListableBeanFactory) bf).registerSingleton("customHelloWorld", customHelloWorld);
HelloWorld myHelloWorld = new HelloWorld();
myHelloWorld.setHelloProcessor( (Hello) ac.getBean("customHelloWorld") );
bean的命名
bean通常都有一个或多个标识(额外的标识就是别名),bean的标识在持有这个bean的容器中必须唯一。
基于xml的配置中,标识符通过id 或者name来定义,通常使用字母数字组合的字符串(alphanumeric),如('myBean', 'fooService', etc.),官方建议使用驼峰命名法。
bean也可以没有名字,但是这样子的bean无法通过名称来被其他bean引用了,只能作为inner bean 或者是通过自动装配机制来被使用。
bean的别名
有时候,我们需要引用一个外部定义的bean的定义,这时候,往往无法修改外部系统的bean的名称。此时一种解决方案就是我们使用外部定义的bean的id来引用这个bean。Spring提供了别名让我们能够在本系统中给这个外部定义的bean一个别名:
<alias name="fromName" alias="toName"/>
例如,系统A中有一个数据源bean,我们在系统B中要给这个bean一个别名:
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
bean的实例化
Spring使用beanDefinition来创建bean实例。以基于xml的配置来说,Spring提供两种方式来实例化bean:
Spring利用反射机制调用构造函数来实例化bean,基本等同于new
Srping通过调用class中的静态工厂方法,用返回的对象来作为bean
构造函数实例化
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
工厂方法实例化
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
依赖
A对象需要使用到B对象和C对象的某些能力,我们一般利用组合来实现这样的功能,因此说,A依赖B和C。
依赖描述了多个对象之间的合作关系。很显然,几乎所有应用程序都有许多彼此依赖的对象相互合作,并提供给最终用户一个连贯的功能。
依赖注入(Dependency Injection)
对象仅仅通过构造函数的参数、工厂方法的参数、对象本身的属性来声明依赖,而容器会自动生成依赖项的实例并注入到对象中。这个过程就是依赖注入。对象没有自己在运行期间去负责实例化自己的依赖项,而是由容器来负责注入。对象将处理依赖的控制权移交给了容器,这种特性称之为控制反转。
由此可见,依赖注入是实现控制反转的一种手段,它让我们的代码更简洁而且松耦合。
依赖注入的两种主要方式:
- 基于构造函数的注入
- 基于setter的注入
基于构造函数的注入
这种注入方式在调用构造函数的时候完成,调用工厂方法的方式与此类似。
如果构造函数存在多个参数,且它们的类型没有继承关系,则它们在构造函数参数中的位置,即可作为xml定义中的位置,如下例子
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
<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>
假设Bar类与Baz类之间没有任何继承关系,在XML配置中,只要保持两个bean的引用顺序与构造函数中两个参数的顺序一致即可。
假如两个类之间有继承关系(或者可以相互转化),则需要在xml配置中显式的使用类型限定,如下例子:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
或者使用索引
<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>
如果要使用名字的话,需要编译选项中保持debug标志,否则,需要使用 @ConstructorProperties 注解:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
使用setter来注入
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...
}
依赖解析的过程
- bean定义在配置元数据中,
ApplicationContext
使用配置元数据来创建并初始化。配置元数据的来源有三种:
a. xml
b. 基于java代码
c. 基于注解 - bean声明了它的依赖(构造函数参数,工厂方法的参数,属性等)。当bean实例化的时候,他依赖的对象会被容器注入。
- 对象的依赖都是值或者是其他bean
- 值会被容器尝试转化为声明的类型,Spring支持将String转化为内置类型:
int
,long
,String
,boolean
等等。
一个比较有趣的情况是循环依赖(Circular dependencies)
如果A依赖B,B也依赖A,那么就是循环依赖,如果使用构造函数注入的方式,很显然,要注入的依赖项需要被完全初始化,因此大家都玩不起来,于是Spring抛出一个运行时异常BeanCurrentlyInCreationException
。
一种常用且有效的解决方案是使用setter注入方式来替代。换句话说,setter注入能支持循环依赖的场景。
当容器启动时,会校验bean的配置。bean的属性在创建时被设置。单例模式的bean默认在容器启动时实例化,其他类型的bean在使用的时候实例化。
预先初始化的方式能够让配置问题提前暴露,减少运行过程中出错的概率。代价是启动时间长,且很多bean在不需要的情况下已经创建好,占用内存。
lazy-initialize这时候很有用。
Bean的定义
直接赋值
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
也可以使用p命名空间来简化写法:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="masterkaoli"/>
</beans>
properties
如前文提到的,Spring内部会尝试将String转化为属性类型,一种很好用的玩法如下所示:
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>
PropertyPlaceholderConfigurer类的properties属性是java.util.Properties
,上面可以直接这样玩。
idref
idref一般用来判断某个bean的id是否真的存在
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
完全等价于下面的写法
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
不过我觉得这种写法没什么用。
对其他bean的引用
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>
ref也可以出现在<constructor-arg/>
中
内部bean(Inner bean)
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
很明显,就这一个bean使用的话,没必要命名了哈哈。
集合
集合超有用
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
集合的合并
集合合并就是子list,map,set,props 覆盖了父list,map,set,props的部分或者全部值。
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
注意,child bean的props 中有merge="true"
,因此child bean的adminEmails
从parent中继承值,并覆盖了support
的值。结果是:
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
parent上的 abstract="true"
意味着这个父型 bean 是个抽象定义,抽象的父bean 往往作为子bean的模板。甚至可以删除parent bean的class属性,这样Spring不会实例化parent bean,只将他作为模板。
集合合并的限制
- 只支持同类型的合并,如list不可以合并到map上。
- merge标签必须在子类型上。
强类型集合
java5 以后引入了自动装箱机制,因此数值1, 可以自动装箱为Integeter.valueof(1),看如下例子:
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
<beans>
<bean id="foo" class="x.y.Foo">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>
9.99 自动转化为Float。
空字符串和null
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
完全等价于
exampleBean.setEmail("");
而
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
完全等价于
exampleBean.setEmail(null);
p命名空间
通过在xml中声明
xmlns:p="http://www.springframework.org/schema/p"
即可使用p命名空间来简化property的写法:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="foo@bar.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="foo@bar.com"/>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
C命名空间
用来简化constructor-arg
元素
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<!-- traditional declaration -->
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
<constructor-arg value="foo@bar.com"/>
</bean>
<!-- c-namespace declaration -->
<bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="foo@bar.com"/>
</beans>
当然也可以使用index的方式:
<!-- c-namespace index declaration -->
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>
设置bean属性的属性的值
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
</bean>
使用 depends-on
为了处理类之间的依赖关系,我们通常使用ref元素。被依赖的bean就会先初始化好。但有时候,类之间的依赖关系没有这么直接。
比如一个dao对象使用前,需要database driver的注册,就需要database driver的static块先生效。
Spring提供了depends-on元素来处理这种情况。
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
当依赖多个bean时,可以使用逗号分隔:
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
懒初始化 (Lazy-initialized)
前面提到过,单例bean的初始化时机是容器启动时(与先初始化),这样能让配置问题提前暴露。但是有时候这并不适合(不值得),因此可以使用懒加载模式。
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
懒初始化的bean再用到的时候初始化。因此如果一个“懒”bean被一个使用默认预先初始化方式的bean依赖,则它的实例化时机仍然是容器启动时,这似乎并不难理解。
基于xml的配置中,可以用如下方式,让一个config的所有bean都变懒:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
自动装配
为了减少xml配置,也为了增加类属性时不必去修改相关的配置文件,Spring提供了自动注入。只要在bean定义中增加autowire属性即可
autowire的四个值:
- no, 不自动创配,默认配置就是不自动装配啊!!!
- byname, 使用属性名自动装配,Spring找和属性同名的bean来自动装配。
- byType, 使用类型来自动装配,如果多个类型的bean都匹配,Spring抛异常;如果没有匹配的bean,赋值null。
- constructor, 与类型装配类似,不过只对构造函数的参数生效,如果不能找到一个确定的bean,Spring给一个 错误。
自动装配的缺陷
- 值类型无法自动装配
- 不够明确
- Spring容器自动生成文档的工具可能会不可用
- 可能找到多个匹配的类型,这时候会抛异常或者错误,这时候就需要
autowire-candidate
来辅助
方法注入
大多数场景中,单例bena很有效,但有时我们需要非单例的bean。如果单例bean依赖一个非单例的bean,由于单例bean的生命周期与非单例bean的生命周期不一致,就会导致问题。
假设一个场景,单例bean 对象A依赖B(B是非单例的),A中每个方法调用的时候,都需要重新获得一个B的新的实例,可是容器只创建A一次,因此只注入B一次,这就导致B的新实例无法获得。
解决方法有两种(说明Spring真的好强):
一种方法一定程度上违背了IOC的思想,因为我们将会自己去实例化B。通过实现ApplicationContextAware
接口,Spring容器会负责自动注入一个ApplicationContext
给我们,然后我们就可以用它来获得BeanFactory
对象,从而获得任意的bean的新实例.
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
这种方法的缺陷很明显:不仅没用上IOC的能力,还要对Spring的接口有一定的了解才行(难不倒我!)。
另一种方法是使用查询方法注入。
注入的方法必须是形如下面的表示:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
然后可以直接在类中调用:
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
最后在配置文件中声明这是个查询方法。
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
当调用createCommand方法时,就会返回一个myCommand bean的实例。如果这个bean时非单例的,那么Spring会负责实例化它。
也可以使用注解@Lookup
来声明一个查询方法
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract MyCommand createCommand();
}