3.3.1 延迟初始化Bean
延迟初始化也叫做惰性初始化,指不提前初始化Bean,而是只有在真正使用时才创建及初始化Bean。
配置方式很简单只需在<bean>标签上指定 “lazy-init” 属性值为“true”即可延迟初始化Bean。
Spring容器会在创建容器时提前初始化“singleton”作用域的Bean,“singleton”就是单例的意思即整个容器每个Bean只有一个实例,后边会详细介绍。Spring容器预先初始化Bean通常能帮助我们提前发现配置错误,所以如果没有什么情况建议开启,除非有某个Bean可能需要加载很大资源,而且很可能在整个应用程序生命周期中很可能使用不到,可以设置为延迟初始化。
延迟初始化的Bean通常会在第一次使用时被初始化;或者在被非延迟初始化Bean作为依赖对象注入时在会随着初始化该Bean时被初始化,因为在这时使用了延迟初始化Bean。
容器管理初始化Bean消除了编程实现延迟初始化,完全由容器控制,只需在需要延迟初始化的Bean定义上配置即可,比编程方式更简单,而且是无侵入代码的。
具体配置如下:
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl" lazy-init="true"/>
3.3.2 使用depends-on
depends-on是指指定Bean初始化及销毁时的顺序,使用depends-on属性指定的Bean要先初始化完毕后才初始化当前Bean,由于只有“singleton”Bean能被Spring管理销毁,所以当指定的Bean都是“singleton”时,使用depends-on属性指定的目标Bean要在原来Bean之后才销毁。也就是说,定义“depends-on”属性的Bean会首先被销毁,然后才是“depends-on”指定的Bean被销毁。
配置方式如下:
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<bean id="decorator"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
depends-on="helloApi">
<property name="helloApi"><ref bean="helloApi"/></property>
</bean>
“decorator”指定了“depends-on”属性为“helloApi”,所以在“decorator” Bean初始化之前要先初始化“helloApi”,而在销毁“helloApi”之前先要销毁“decorator”,大家注意一下销毁顺序。
“depends-on”属性可以指定多个Bean,若指定多个Bean可以用“;”、“,”、空格分割。
那“depends-on”有什么好处呢?主要是给出明确的初始化及销毁顺序,比如要初始化“decorator”时要确保“helloApi” Bean的资源准备好了,否则使用“decorator”时会看不到准备的资源;而在销毁时要先把“decorator” Bean对“helloApi”资源的引用释放掉才能销毁“helloApi”,否则可能销毁 “helloApi”时而“decorator”还保持着资源访问,造成资源不能释放或释放错误。
让我们看个例子吧,在平常开发中我们可能需要访问文件系统,而文件打开、关闭是必须配对的,不能打开后不关闭,从而造成其他程序不能访问该文件。让我们来看具体配置吧。1)准备测试类:
ResourceBean从配置文件中获取文件位置,然后定义初始化方法init中打开指定的文件,然后获取文件流;最后定义销毁方法destroy用于在应用程序关闭时调用该方法关闭掉文件流。
DependentBean中会注入ResourceBean,并从ResourceBean中获取文件流写入内容;定义初始化方法init用来定义一些初始化操作并向文件中输出文件头信息;最后定义销毁方法用于在关闭应用程序时想文件中输出文件尾信息。
具体代码如下:
package cn.javass.spring.chapter3.bean;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ResourceBean {
private FileOutputStream fos;
private File file;
//初始化方法
public void init() {
System.out.println("ResourceBean:========初始化");
//加载资源,在此只是演示
System.out.println("ResourceBean:========加载资源,执行一些预操作");
try {
this.fos = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//销毁资源方法
public void destroy() {
System.out.println("ResourceBean:========销毁");
//释放资源
System.out.println("ResourceBean:========释放资源,执行一些清理操作");
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public FileOutputStream getFos() {
return fos;
}
public void setFile(File file) {
this.file = file;
}
}
package cn.javass.spring.chapter3.bean;
import java.io.IOException;
public class DependentBean {
ResourceBean resourceBean;
public void write(String ss) throws IOException {
System.out.println("DependentBean:=======写资源");
resourceBean.getFos().write(ss.getBytes());
}
//初始化方法
public void init() throws IOException {
System.out.println("DependentBean:=======初始化");
resourceBean.getFos().write("DependentBean:=======初始化=====".getBytes());
}
//销毁方法
public void destroy() throws IOException {
System.out.println("DependentBean:=======销毁");
//在销毁之前需要往文件中写销毁内容
resourceBean.getFos().write("DependentBean:=======销毁=====".getBytes());
}
public void setResourceBean(ResourceBean resourceBean) {
this.resourceBean = resourceBean;
}
}
2)类定义好了,让我们来进行Bean定义吧,具体配置文件如下:
<bean id="resourceBean"
class="cn.javass.spring.chapter3.bean.ResourceBean"
init-method="init" destroy-method="destroy">
<property name="file" value="D:/test.txt"/>
</bean>
<bean id="dependentBean"
class="cn.javass.spring.chapter3.bean.DependentBean"
init-method="init" destroy-method="destroy" depends-on="resourceBean">
<property name="resourceBean" ref="resourceBean"/>
</bean>
<property name="file" value="D:/test.txt"/>配置:Spring容器能自动把字符串转换为java.io.File。
init-method="init" :指定初始化方法,在构造器注入和setter注入完毕后执行。
destroy-method="destroy":指定销毁方法,只有“singleton”作用域能销毁,“prototype”作用域的一定不能,其他作用域不一定能;后边再介绍。
在此配置中,dependentBean初始化在resourceBean之前被初始化,resourceBean销毁会在dependentBean销毁之后执行。
3)配置完毕,测试一下吧:
package cn.javass.spring.chapter3;
import java.io.IOException;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import cn.javass.spring.chapter3.bean.DependentBean;
public class MoreDependencyInjectTest {
@Test
public void testDependOn() throws IOException {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("chapter3/depends-on.xml");
//一定要注册销毁回调,否则我们定义的销毁方法不执行
context.registerShutdownHook();
DependentBean dependentBean =
context.getBean("dependentBean", DependentBean.class);
dependentBean.write("aaa");
}
}
测试跟其他测试完全一样,只是在此
我们一定要注册销毁方法回调,否则销毁方法不会执行。
如果配置没问题会有如下输出:
ResourceBean:========初始化
ResourceBean:========加载资源,执行一些预操作
DependentBean:=========初始化
DependentBean:=========写资源
DependentBean:=========销毁
ResourceBean:========销毁
ResourceBean:========释放资源,执行一些清理操作
3.3.3 自动装配
自动装配就是指由Spring来自动地注入依赖对象,无需人工参与。
目前Spring3.0支持“no”、“byName ”、“byType”、“constructor”四种自动装配,默认是“no”指不支持自动装配的,其中Spring3.0已不推荐使用之前版本的“autodetect”自动装配,推荐使用Java 5+支持的(@Autowired)注解方式代替;如果想支持“autodetect”自动装配,请将schema改为“spring-beans-2.5.xsd”或去掉。
自动装配的好处是减少构造器注入和setter注入配置,减少配置文件的长度。自动装配通过配置<bean>标签的“autowire”属性来改变自动装配方式。接下来让我们挨着看下配置的含义。
一、default:表示使用默认的自动装配,默认的自动装配需要在<beans>标签中使用default-autowire属性指定,其支持“no”、“byName ”、“byType”、“constructor”四种自动装配,如果需要覆盖默认自动装配,请继续往下看;二、no:意思是不支持自动装配,必须明确指定依赖。
三、byName:通过设置Bean定义属性autowire="byName",意思是根据名字进行自动装配,只能用于setter注入。比如我们有方法“setHelloApi”,则“byName”方式Spring容器将查找名字为helloApi的Bean并注入,如果找不到指定的Bean,将什么也不注入。例如如下Bean定义配置:
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<bean id="bean" class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="byName"/>
测试代码如下:
package cn.javass.spring.chapter3;
import java.io.IOException;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import cn.javass.spring.chapter2.helloworld.HelloApi;
public class AutowireBeanTest {
@Test
public void testAutowireByName() throws IOException {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("chapter3/autowire-byName.xml");
HelloApi helloApi = context.getBean("bean", HelloApi.class);
helloApi.sayHello();
}
}
是不是不要配置<property>了,如果一个bean有很多setter注入,通过“byName”方式是不是能减少很多<property>配置。此处注意了,在根据名字注入时,将把当前Bean自己排除在外:比如“hello” Bean类定义了“setHello”方法,则hello是不能注入到“setHello”的。
四、“byType”:通过设置Bean定义属性autowire="byType",意思是指根据类型注入,用于setter注入,比如如果指定自动装配方式为“byType”,而“setHelloApi”方法需要注入HelloApi类型数据,则Spring容器将查找HelloApi类型数据,如果找到一个则注入该Bean,如果找不到将什么也不注入,如果找到多个Bean将优先注入<bean>标签“primary”属性为true的Bean,否则抛出异常来表明有个多个Bean发现但不知道使用哪个。让我们用例子来讲解一下这几种情况吧。
1)根据类型只找到一个Bean,此处注意了,在根据类型注入时,将把当前Bean自己排除在外,即如下配置中helloApi和bean都是HelloApi接口的实现,而“bean”通过类型进行注入“HelloApi”类型数据时自己是排除在外的,配置如下(具体测试请参考AutowireBeanTest.testAutowireByType1方法):
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<bean id="bean" class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="byType"/>
2)根据类型找到多个Bean时,对于集合类型(如List、Set)将注入所有匹配的候选者,而对于其他类型遇到这种情况可能需要使用“autowire-candidate”属性为false来让指定的Bean放弃作为自动装配的候选者,或使用“primary”属性为true来指定某个Bean为首选Bean:
2.1)通过设置Bean定义的“autowire-candidate”属性为false来把指定Bean后自动装配候选者中移除:
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 从自动装配候选者中去除 -->
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"
autowire-candidate="false"/>
<bean id="bean1" class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="byType"/>
2.2)通过设置Bean定义的“primary”属性为false来把指定自动装配时候选者中首选Bean:
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 自动装配候选者中的首选Bean-->
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl" primary="true"/>
<bean id="bean" class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="byType"/>
具体测试请参考AutowireBeanTest类的testAutowireByType***方法。
五、“constructor”:通过设置Bean定义属性autowire="constructor",功能和“byType”功能一样,根据类型注入构造器参数,只是用于构造器注入方式,直接看例子吧:
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 自动装配候选者中的首选Bean-->
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl" primary="true"/>
<bean id="bean"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="constructor"/>
测试代码如下:
@Test
public void testAutowireByConstructor() throws IOException {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("chapter3/autowire-byConstructor.xml");
HelloApi helloApi = context.getBean("bean", HelloApi.class);
helloApi.sayHello();
}
六、autodetect:自动检测是使用“constructor”还是“byType”自动装配方式,已不推荐使用。如果Bean有空构造器那么将采用“byType”自动装配方式,否则使用“constructor”自动装配方式。此处要把3.0的xsd替换为2.5的xsd,否则会报错。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 自动装配候选者中的首选Bean-->
<bean class="cn.javass.spring.chapter2.helloworld.HelloImpl" primary="true"/>
<bean id="bean"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
autowire="autodetect"/>
</beans>
可以采用在“<beans>”标签中通过“default-autowire”属性指定全局的自动装配方式,即如果default-autowire=”byName”,将对所有Bean进行根据名字进行自动装配。
不是所有类型都能自动装配:
- 不能自动装配的数据类型:Object、基本数据类型(Date、CharSequence、Number、URI、URL、Class、int)等;
- 通过“<beans>”标签default-autowire-candidates属性指定的匹配模式,不匹配的将不能作为自动装配的候选者,例如指定“*Service,*Dao”,将只把匹配这些模式的Bean作为候选者,而不匹配的不会作为候选者;
- 通过将“<bean>”标签的autowire-candidate属性可被设为false,从而该Bean将不会作为依赖注入的候选者。
数组、集合、字典类型的根据类型自动装配和普通类型的自动装配是有区别的:
- 数组类型、集合(Set、Collection、List)接口类型:将根据泛型获取匹配的所有候选者并注入到数组或集合中,如“List<HelloApi> list”将选择所有的HelloApi类型Bean并注入到list中,而对于集合的具体类型将只选择一个候选者,“如 ArrayList<HelloApi> list”将选择一个类型为ArrayList的Bean注入,而不是选择所有的HelloApi类型Bean进行注入;
- 字典(Map)接口类型:同样根据泛型信息注入,键必须为String类型的Bean名字,值根据泛型信息获取,如“Map<String, HelloApi> map” 将选择所有的HelloApi类型Bean并注入到map中,而对于具体字典类型如“HashMap<String, HelloApi> map”将只选择类型为HashMap的Bean注入,而不是选择所有的HelloApi类型Bean进行注入。
自动装配我们已经介绍完了,自动装配能带给我们什么好处呢?首先,自动装配确实减少了配置文件的量;其次, “byType”自动装配能在相应的Bean更改了字段类型时自动更新,即修改Bean类不需要修改配置,确实简单了。
自动装配也是有缺点的,最重要的缺点就是没有了配置,在查找注入错误时非常麻烦,还有比如基本类型没法完成自动装配,所以可能经常发生一些莫名其妙的错误,在此我推荐大家不要使用该方式,最好是指定明确的注入方式,或者采用最新的Java5+注解注入方式。所以大家在使用自动装配时应该考虑自己负责项目的复杂度来进行衡量是否选择自动装配方式。
自动装配注入方式能和配置注入方式一同工作吗?当然可以,大家只需记住配置注入的数据会覆盖自动装配注入的数据。
大家是否注意到对于采用自动装配方式时如果没找到合适的的Bean时什么也不做,这样在程序中总会莫名其妙的发生一些空指针异常,而且是在程序运行期间才能发现,有没有办法能在提前发现这些错误呢?接下来就让我来看下依赖检查吧。
3.3.4 依赖检查
上一节介绍的自动装配,很可能发生没有匹配的Bean进行自动装配,如果此种情况发生,只有在程序运行过程中发生了空指针异常才能发现错误,如果能提前发现该多好啊,这就是依赖检查的作用。
依赖检查:用于检查Bean定义的属性都注入数据了,不管是自动装配的还是配置方式注入的都能检查,如果没有注入数据将报错,从而提前发现注入错误,只检查具有setter方法的属性。
Spring3+也不推荐配置方式依赖检查了,建议采用Java5+ @Required注解方式,测试时请将XML schema降低为2.5版本的,和自动装配中“autodetect”配置方式的xsd一样。
<?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-2.5.xsd
</beans>
依赖检查有none、simple、object、all四种方式,接下来让我们详细介绍一下:
一、none:默认方式,表示不检查;
二、objects:检查除基本类型外的依赖对象,配置方式为:dependency-check="objects",此处我们为HelloApiDecorator添加一个String类型属性“message”,来测试如果有简单数据类型的属性为null,也不报错;
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 注意我们没有注入helloApi,所以测试时会报错 -->
<bean id="bean"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
dependency-check="objects">
<property name="message" value="Haha"/>
</bean>
注意由于我们没有注入bean需要的依赖“helloApi”,所以应该抛出异常UnsatisfiedDependencyException,表示没有发现满足的依赖:
package cn.javass.spring.chapter3;
import java.io.IOException;
import org.junit.Test;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class DependencyCheckTest {
@Test(expected = UnsatisfiedDependencyException.class)
public void testDependencyCheckByObject() throws IOException {
//将抛出异常
new ClassPathXmlApplicationContext("chapter3/dependency-check-object.xml");
}
}
三、simple:对基本类型进行依赖检查,包括数组类型,其他依赖不报错;配置方式为:dependency-check="simple",以下配置中没有注入message属性,所以会抛出异常:
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<!-- 注意我们没有注入message属性,所以测试时会报错 -->
<bean id="bean"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
dependency-check="simple">
<property name="helloApi" ref="helloApi"/>
</bean>
四、all:对所以类型进行依赖检查,配置方式为:dependency-check="all",如下配置方式中如果两个属性其中一个没配置将报错。
<bean id="helloApi" class="cn.javass.spring.chapter2.helloworld.HelloImpl"/>
<bean id="bean"
class="cn.javass.spring.chapter3.bean.HelloApiDecorator"
dependency-check="all">
<property name="helloApi" ref="helloApi"/>
<property name="message" value="Haha"/>
</bean>
依赖检查也可以通过“<beans>”标签中default-dependency-check属性来指定全局依赖检查配置。
3.3.5 方法注入
所谓方法注入其实就是通过配置方式覆盖或拦截指定的方法,通常通过代理模式实现。Spring提供两种方法注入:查找方法注入和方法替换注入。
因为Spring是通过CGLIB动态代理方式实现方法注入,也就是通过动态修改类的字节码来实现的,本质就是生成需方法注入的类的子类方式实现。
在进行测试之前,我们需要确保将“com.springsource.cn.sf.cglib-2.2.0.jar”放到lib里并添加到“Java Build Path”中的Libararies中。否则报错,异常中包含“nested exception is java.lang.NoClassDefFoundError: cn/sf/cglib/proxy/CallbackFilter”。
传统方式和Spring容器管理方式唯一不同的是不需要我们手动生成子类,而是通过配置方式来实现;其中如果要替换createPrinter()方法的返回值就使用查找方法注入;如果想完全替换sayHello()方法体就使用方法替换注入。
接下来让我们看看具体实现吧。
一、查找方法注入:又称为Lookup方法注入,用于注入方法返回结果,也就是说能通过配置方式替换方法返回结果。使用<lookup-method name="方法名" bean="bean名字"/>配置;其中name属性指定方法名,bean属性指定方法需返回的Bean。
方法定义格式:访问级别必须是public或protected,保证能被子类重载,可以是抽象方法,必须有返回值,必须是无参数方法,查找方法的类和被重载的方法必须为非final:
<public | protected> [abstract] <return-type> theMethodName(no-arguments);
因为“singleton” Bean在容器中只有一个实例,而“prototype” Bean是每次获取容器都返回一个全新的实例,所以如果“singleton” Bean在使用“prototype” Bean情况时,那么“prototype” Bean由于是“singleton” Bean的一个字段属性,所以获取的这个“prototype” Bean就和它所在的“singleton” Bean具有同样的生命周期,所以不是我们所期待的结果。因此查找方法注入就是用于解决这个问题。
1) 首先定义我们需要的类,Printer类是一个有状态的类,counter字段记录访问次数:
package cn.javass.spring.chapter3.bean;
public class Printer {
private int counter = 0;
public void print(String type) {
System.out.println(type + " printer: " + counter++);
}
}
HelloImpl5类用于打印欢迎信息,其中包括setter注入和方法注入,此处特别需要注意的是该类是抽象的,充分说明了需要容器对其进行子类化处理,还定义了一个抽象方法createPrototypePrinter用于创建“prototype” Bean,createSingletonPrinter方法用于创建“singleton” Bean,此处注意方法会被Spring拦截,不会执行方法体代码:
package cn.javass.spring.chapter3;
import cn.javass.spring.chapter2.helloworld.HelloApi;
import cn.javass.spring.chapter3.bean.Printer;
public abstract class HelloImpl5 implements HelloApi {
private Printer printer;
public void sayHello() {
printer.print("setter");
createPrototypePrinter().print("prototype");
}
public abstract Printer createPrototypePrinter();
public Printer createSingletonPrinter() {
System.out.println("该方法不会被执行,如果输出就错了");
return new Printer();
}
public void setPrinter(Printer printer) {
this.printer = printer;
}
}
2) 开始配置了,配置文件在(resources/chapter3/lookupMethodInject.xml),其中“prototypePrinter”是“prototype” Printer,“singletonPrinter”是“singleton” Printer,“helloApi1”是“singleton” Bean,而“helloApi2”注入了“prototype” Bean:
<bean id="prototypePrinter"
class="cn.javass.spring.chapter3.bean.Printer" scope="prototype"/>
<bean id="singletonPrinter"
class="cn.javass.spring.chapter3.bean.Printer" scope="singleton"/>
<bean id="helloApi1" class="cn.javass.spring.chapter3.HelloImpl5" scope="singleton">
<property name="printer" ref="prototypePrinter"/>
<lookup-method name="createPrototypePrinter" bean="prototypePrinter"/>
<lookup-method name="createSingletonPrinter" bean="singletonPrinter"/>
</bean>
<bean id="helloApi2" class="cn.javass.spring.chapter3.HelloImpl5" scope="prototype">
<property name="printer" ref="prototypePrinter"/>
<lookup-method name="createPrototypePrinter" bean="prototypePrinter"/>
<lookup-method name="createSingletonPrinter" bean="singletonPrinter"/>
</bean>
3)测试代码如下:
package cn.javass.spring.chapter3;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import cn.javass.spring.chapter2.helloworld.HelloApi;
public class MethodInjectTest {
@Test
public void testLookup() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("chapter3/lookupMethodInject.xml");
System.out.println("=======singleton sayHello======");
HelloApi helloApi1 = context.getBean("helloApi1", HelloApi.class);
helloApi1.sayHello();
helloApi1 = context.getBean("helloApi1", HelloApi.class);
helloApi1.sayHello();
System.out.println("=======prototype sayHello======");
HelloApi helloApi2 = context.getBean("helloApi2", HelloApi.class);
helloApi2.sayHello();
helloApi2 = context.getBean("helloApi2", HelloApi.class);
helloApi2.sayHello();
}
}
其中“helloApi1”测试中,其输出结果如下:
=======singleton sayHello======
setter printer: 0
prototype printer: 0
singleton printer: 0
setter printer: 1
prototype printer: 0
singleton printer: 1
首先“helloApi1”是“singleton”,通过setter注入的“printer”是“prototypePrinter”,所以它应该输出“setter printer:0”和“setter printer:1”;而“createPrototypePrinter”方法注入了“prototypePrinter”,所以应该输出两次“prototype printer:0”;而“createSingletonPrinter”注入了“singletonPrinter”,所以应该输出“singleton printer:0”和“singleton printer:1”。
而“helloApi2”测试中,其输出结果如下:
=======prototype sayHello======
setter printer: 0
prototype printer: 0
singleton printer: 2
setter printer: 0
prototype printer: 0
singleton printer: 3
首先“helloApi2”是“prototype”,通过setter注入的“printer”是“prototypePrinter”,所以它应该输出两次“setter printer:0”;而“createPrototypePrinter”方法注入了“prototypePrinter”,所以应该输出两次“prototype printer:0”;而“createSingletonPrinter”注入了“singletonPrinter”,所以应该输出“singleton printer:2”和“singleton printer:3”。
大家是否注意到“createSingletonPrinter”方法应该输出“该方法不会被执行,如果输出就错了”,而实际是没输出的,这说明Spring拦截了该方法并使用注入的Bean替换了返回结果。
方法注入主要用于处理“singleton”作用域的Bean需要其他作用域的Bean时,采用Spring查找方法注入方式无需修改任何代码即能获取需要的其他作用域的Bean。
二、替换方法注入:也叫“MethodReplacer”注入,和查找注入方法不一样的是,他主要用来替换方法体。通过首先定义一个MethodReplacer接口实现,然后如下配置来实现:
<replaced-method name="方法名" replacer="MethodReplacer实现">
<arg-type>参数类型</arg-type>
</replaced-method>
1)首先定义MethodReplacer实现,完全替换掉被替换方法的方法体及返回值,其中reimplement方法重定义方法功能,参数obj为被替换方法的对象,method为被替换方法,args为方法参数;最需要注意的是不能再 通过“method.invoke(obj, new String[]{"hehe"});” 反射形式再去调用原来方法,这样会产生循环调用;如果返回值类型为Void,请在实现中返回null:
package cn.javass.spring.chapter3.bean;
import java.lang.reflect.Method;
import org.springframework.beans.factory.support.MethodReplacer;
public class PrinterReplacer implements MethodReplacer {
@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("Print Replacer");
//注意此处不能再通过反射调用了,否则会产生循环调用,知道内存溢出
//method.invoke(obj, new String[]{"hehe"});
return null;
}
}
2)配置如下,首先定义MethodReplacer实现,使用< replaced-method >标签来指定要进行替换方法,属性name指定替换的方法名字,replacer指定该方法的重新实现者,子标签< arg-type >用来指定原来方法参数的类型,必须指定否则找不到原方法:
<bean id="replacer" class="cn.javass.spring.chapter3.bean.PrinterReplacer"/>
<bean id="printer" class="cn.javass.spring.chapter3.bean.Printer">
<replaced-method name="print" replacer="replacer">
<arg-type>java.lang.String</arg-type>
</replaced-method>
</bean>
3)测试代码将输出“Print Replacer ”,说明方法体确实被替换了:
@Test
public void testMethodReplacer() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("chapter3/methodReplacerInject.xml");
Printer printer = context.getBean("printer", Printer.class);
printer.print("我将被替换");
}
3.4 Bean的作用域
什么是作用域呢?即“scope”,在面向对象程序设计中一般指对象或变量之间的可见范围。而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围。
Spring提供“singleton”和“prototype”两种基本作用域,另外提供“request”、“session”、“global session”三种web作用域;Spring还允许用户定制自己的作用域。
3.4.1 基本的作用域
一、singleton:指“singleton”作用域的Bean只会在每个Spring IoC容器中存在一个实例,而且其完整生命周期完全由Spring容器管理。对于所有获取该Bean的操作Spring容器将只返回同一个Bean。
GoF单例设计模式指“保证一个类仅有一个实例,并提供一个访问它的全局访问点”,介绍了两种实现:通过在类上定义静态属性保持该实例和通过注册表方式。
1)通过在类上定义静态属性保持该实例:一般指一个Java虚拟机 ClassLoader装载的类只有一个实例,一般通过类静态属性保持该实例,这样就造成需要单例的类都需要按照单例设计模式进行编码;Spring没采用这种方式,因为该方式属于侵入式设计;代码样例如下:
package cn.javass.spring.chapter3.bean;
public class Singleton {
//1.私有化构造器
private Singleton() {}
//2.单例缓存者,惰性初始化,第一次使用时初始化
private static class InstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.提供全局访问点
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
//4.提供一个计数器来验证一个ClassLoader一个实例
private int counter=0;
}
以上定义个了个单例类,首先要私有化类构造器;其次使用InstanceHolder静态内部类持有单例对象,这样可以得到惰性初始化好处;最后提供全局访问点getInstance,使得需要该单例实例的对象能获取到;我们在此还提供了一个counter计数器来验证一个ClassLoader一个实例。具体一个ClassLoader有一个单例实例测试请参考代码“cn.javass.spring.chapter3. SingletonTest”中的“testSingleton”测试方法,里边详细演示了一个ClassLoader有一个单例实例。
2)通过注册表方式: 首先将需要单例的实例通过唯一键注册到注册表,然后通过键来获取单例,让我们直接看实现吧,注意本注册表实现了Spring接口“SingletonBeanRegistry”,该接口定义了操作共享的单例对象,Spring容器实现将实现此接口;所以共享单例对象通过“registerSingleton”方法注册,通过“getSingleton”方法获取,消除了编程方式单例,注意在实现中不考虑并发:
package cn.javass.spring.chapter3;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
public class SingletonBeanRegister implements SingletonBeanRegistry {
//单例Bean缓存池,此处不考虑并发
private final Map<String, Object> BEANS = new HashMap<String, Object>();
public boolean containsSingleton(String beanName) {
return BEANS.containsKey(beanName);
}
public Object getSingleton(String beanName) {
return BEANS.get(beanName);
}
@Override
public int getSingletonCount() {
return BEANS.size();
}
@Override
public String[] getSingletonNames() {
return BEANS.keySet().toArray(new String[0]);
}
@Override
public void registerSingleton(String beanName, Object bean) {
if(BEANS.containsKey(beanName)) {
throw new RuntimeException("[" + beanName + "] 已存在");
}
BEANS.put(beanName, bean);
}
}
Spring是注册表单例设计模式的实现,消除了编程式单例,而且对代码是非入侵式。
接下来让我们看看在Spring中如何配置单例Bean吧,在Spring容器中如果没指定作用域默认就是“singleton”,配置方式通过scope属性配置,具体配置如下:
<bean class="cn.javass.spring.chapter3.bean.Printer" scope="singleton"/>
Spring管理单例对象在Spring容器中存储如图3-5所示,Spring不仅会缓存单例对象,Bean定义也是会缓存的,对于惰性初始化的对象是在首次使用时根据Bean定义创建并存放于单例缓存池。
图3-5 单例处理
二、prototype:即原型,指每次向Spring容器请求获取Bean都返回一个全新的Bean,相对于“singleton”来说就是不缓存Bean,每次都是一个根据Bean定义创建的全新Bean。
GoF原型设计模式,指用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
Spring中的原型和GoF中介绍的原型含义是不一样的:
GoF通过用原型实例指定创建对象的种类,而Spring容器用Bean定义指定创建对象的种类;
GoF通过拷贝这些原型创建新的对象,而Spring容器根据Bean定义创建新对象。
其相同地方都是根据某些东西创建新东西,而且GoF原型必须显示实现克隆操作,属于侵入式,而Spring容器只需配置即可,属于非侵入式。
接下来让我们看看Spring如何实现原型呢?
1)首先让我们来定义Bean“原型”:Bean定义,所有对象将根据Bean定义创建;在此我们只是简单示例一下,不会涉及依赖注入等复杂实现:BeanDefinition类定义属性“class”表示原型类,“id”表示唯一标识,“scope”表示作用域,具体如下:
package cn.javass.spring.chapter3;
public class BeanDefinition {
//单例
public static final int SCOPE_SINGLETON = 0;
//原型
public static final int SCOPE_PROTOTYPE = 1;
//唯一标识
private String id;
//class全限定名
private String clazz;
//作用域
private int scope = SCOPE_SINGLETON;
//鉴于篇幅,省略setter和getter方法;
}
2)接下来让我们看看Bean定义注册表,类似于单例注册表:
package cn.javass.spring.chapter3;
import java.util.HashMap;
import java.util.Map;
public class BeanDifinitionRegister {
//bean定义缓存,此处不考虑并发问题
private final Map<String, BeanDefinition> DEFINITIONS =
new HashMap<String, BeanDefinition>();
public void registerBeanDefinition(String beanName, BeanDefinition bd) {
//1.本实现不允许覆盖Bean定义
if(DEFINITIONS.containsKey(bd.getId())) {
throw new RuntimeException("已存在Bean定义,此实现不允许覆盖");
}
//2.将Bean定义放入Bean定义缓存池
DEFINITIONS.put(bd.getId(), bd);
}
public BeanDefinition getBeanDefinition(String beanName) {
return DEFINITIONS.get(beanName);
}
public boolean containsBeanDefinition(String beanName) {
return DEFINITIONS.containsKey(beanName);
}
}
3)接下来应该来定义BeanFactory了:
package cn.javass.spring.chapter3;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
public class DefaultBeanFactory {
//Bean定义注册表
private BeanDifinitionRegister DEFINITIONS = new BeanDifinitionRegister();
//单例注册表
private final SingletonBeanRegistry SINGLETONS = new SingletonBeanRegister();
public Object getBean(String beanName) {
//1.验证Bean定义是否存在
if(!DEFINITIONS.containsBeanDefinition(beanName)) {
throw new RuntimeException("不存在[" + beanName + "]Bean定义");
}
//2.获取Bean定义
BeanDefinition bd = DEFINITIONS.getBeanDefinition(beanName);
//3.是否该Bean定义是单例作用域
if(bd.getScope() == BeanDefinition.SCOPE_SINGLETON) {
//3.1 如果单例注册表包含Bean,则直接返回该Bean
if(SINGLETONS.containsSingleton(beanName)) {
return SINGLETONS.getSingleton(beanName);
}
//3.2单例注册表不包含该Bean,则创建并注册到单例注册表,从而缓存
SINGLETONS.registerSingleton(beanName, createBean(bd));
return SINGLETONS.getSingleton(beanName);
}
//4.如果是原型Bean定义,则直接返回根据Bean定义创建的新Bean,
//每次都是新的,无缓存
if(bd.getScope() == BeanDefinition.SCOPE_PROTOTYPE) {
return createBean(bd);
}
//5.其他情况错误的Bean定义
throw new RuntimeException("错误的Bean定义");
}
public void registerBeanDefinition(BeanDefinition bd) {
DEFINITIONS.registerBeanDefinition(bd.getId(), bd);
}
private Object createBean(BeanDefinition bd) {
//根据Bean定义创建Bean
try {
Class clazz = Class.forName(bd.getClazz());
//通过反射使用无参数构造器创建Bean
return clazz.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("没有找到Bean[" + bd.getId() + "]类");
} catch (Exception e) {
throw new RuntimeException("创建Bean[" + bd.getId() + "]失败");
}
}
其中方法getBean用于获取根据beanName对于的Bean定义创建的对象,有单例和原型两类Bean;registerBeanDefinition方法用于注册Bean定义,私有方法createBean用于根据Bean定义中的类型信息创建Bean。
3)测试一下吧,在此我们只测试原型作用域Bean,对于每次从Bean工厂中获取的Bean都是一个全新的对象,代码片段(BeanFatoryTest)如下:
@Test
public void testPrototype () throws Exception {
//1.创建Bean工厂
DefaultBeanFactory bf = new DefaultBeanFactory();
//2.创建原型 Bean定义
BeanDefinition bd = new BeanDefinition();
bd.setId("bean");
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bd.setClazz(HelloImpl2.class.getName());
bf.registerBeanDefinition(bd);
//对于原型Bean每次应该返回一个全新的Bean
System.out.println(bf.getBean("bean") != bf.getBean("bean"));
}
最后让我们看看如何在Spring中进行配置吧,只需指定<bean>标签属性“scope”属性为“prototype”即可:
<bean class="cn.javass.spring.chapter3.bean.Printer" />
Spring管理原型对象在Spring容器中存储如图3-6所示,Spring不会缓存原型对象,而是根据Bean定义每次请求返回一个全新的Bean:
单例和原型作用域我们已经讲完,接下来让我们学习一些在Web应用中有哪些作用域:
3.4.2 Web应用中的作用域
在Web应用中,我们可能需要将数据存储到request、session、global session。因此Spring提供了三种Web作用域:request、session、globalSession。
一、request作用域:表示每个请求需要容器创建一个全新Bean。比如提交表单的数据必须是对每次请求新建一个Bean来保持这些表单数据,请求结束释放这些数据。
二、session作用域:表示每个会话需要容器创建一个全新Bean。比如对于每个用户一般会有一个会话,该用户的用户信息需要存储到会话中,此时可以将该Bean配置为web作用域。
三、globalSession:类似于session作用域,只是其用于portlet环境的web应用。如果在非portlet环境将视为session作用域。
配置方式和基本的作用域相同,只是必须要有web环境支持,并配置相应的容器监听器或拦截器从而能应用这些作用域,我们会在集成web时讲解具体使用,大家只需要知道有这些作用域就可以了。
3.4.4 自定义作用域
在日常程序开发中,几乎用不到自定义作用域,除非又必要才进行自定义作用域。
首先让我们看下Scope接口吧:
package org.springframework.beans.factory.config;
import org.springframework.beans.factory.ObjectFactory;
public interface Scope {
Object get(String name, ObjectFactory<?> objectFactory);
Object remove(String name);
void registerDestructionCallback(String name, Runnable callback);
Object resolveContextualObject(String key);
String getConversationId();
}
1)
Object get(String name, ObjectFactory<?> objectFactory):用于从作用域中获取Bean,其中参数objectFactory是当在当前作用域没找到合适Bean时使用它创建一个新的Bean;
2)void registerDestructionCallback(String name, Runnable callback):用于注册销毁回调,如果想要销毁相应的对象则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象;
3)Object resolveContextualObject(String key):用于解析相应的上下文数据,比如request作用域将返回request中的属性。
4)String getConversationId():作用域的会话标识,比如session作用域将是sessionId。
让我们来实现个简单的thread作用域,该作用域内创建的对象将绑定到ThreadLocal内。package cn.javass.spring.chapter3;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> THREAD_SCOPE =
new ThreadLocal<Map<String, Object>>() {
protected Map<String, Object> initialValue() {
//用于存放线程相关Bean
return new HashMap<String, Object>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
//如果当前线程已经绑定了相应Bean,直接返回
if(THREAD_SCOPE.get().containsKey(name)) {
return THREAD_SCOPE.get().get(name);
}
//使用objectFactory创建Bean并绑定到当前线程上
THREAD_SCOPE.get().put(name, objectFactory.getObject());
return THREAD_SCOPE.get().get(name);
}
@Override
public String getConversationId() {
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
//此处不实现就代表类似proytotype,容器返回给用户后就不管了
}
@Override
public Object remove(String name) {
return THREAD_SCOPE.get().remove(name);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
}
Scope已经实现了,让我们将其注册到Spring容器,使其发挥作用:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map><entry>
<!-- 指定scope关键字 -->
<key><value>thread</value></key>
<!-- scope实现 -->
<bean class="cn.javass.spring.chapter3.ThreadScope"/>
</entry></map>
</property>
</bean>
通过CustomScopeConfigurer的scopes属性注册自定义作用域实现,在此需要指定使用作用域的关键字“thread”,并指定自定义作用域实现。来让我们来定义一个“thread”作用域的Bean,配置(chapter3/threadScope.xml)如下:
<bean id="helloApi"
class="cn.javass.spring.chapter2.helloworld.HelloImpl"
scope="thread"/>
最后测试(cn.javass.spring.chapter3.ThreadScopeTest)一下吧,首先在一个线程中测试,在同一线程中获取的Bean应该是一样的;再让我们开启两个线程,然后应该这两个线程创建的Bean是不一样:
@Test
public void testSingleThread() {
BeanFactory beanFactory =
new ClassPathXmlApplicationContext("chapter3/threadScope.xml");
HelloApi bean1 = beanFactory.getBean("helloApi", HelloApi.class);
HelloApi bean2 = beanFactory.getBean("helloApi", HelloApi.class);
//在同一线程中两次获取的Bean应该是相等的
Assert.assertEquals(bean1, bean2);
}
@Test
public void testTwoThread() throws InterruptedException {
final BeanFactory beanFactory =
new ClassPathXmlApplicationContext("chapter3/threadScope.xml");
final HelloApi[] beans = new HelloApi[2];
Thread thread1 = new Thread() {
public void run() {
beans[0] = beanFactory.getBean("helloApi", HelloApi.class);
}};
Thread thread2 = new Thread() {
public void run() {
beans[1] = beanFactory.getBean("helloApi", HelloApi.class);
}};
thread1.start();thread1.sleep(1000);
thread2.start();thread2.sleep(1000);
//在两个线程中两次获取的Bean应该是不相等的
Assert.assertEquals(beans[0], beans[1]);
}
自定义作用域实现其实是非常简单的,其实复杂的是如果需要销毁Bean,自定义作用域如何正确的销毁Bean。