标签:
传统应用程序设计中所说的依赖一般指“类之间的关系”,那先让我们复习一下类之间的关系:
泛化:表示类与类之间的继承关系、接口与接口之间的继承关系;
实现:表示类对接口的实现;
依赖:当类与类之间有使用关系时就属于依赖关系,不同于关联关系,依赖不具有“拥有关系”,而是一种“相识关系”,只在某个特定地方(比如某个方法体内)才有关系。
关联:表示类与类或类与接口之间的依赖关系,表现为“拥有关系”;具体到代码可以用实例变量来表示;
聚合:属于是关联的特殊情况,体现部分-整体关系,是一种弱拥有关系;整体和部分可以有不一样的生命周期;是一种弱关联;
组合:属于是关联的特殊情况,也体现了体现部分-整体关系,是一种强“拥有关系”;整体与部分有相同的生命周期,是一种强关联;
Spring IoC容器的依赖有两层含义:Bean依赖容器和容器注入Bean的依赖资源:
Bean依赖容器:也就是说Bean要依赖于容器,这里的依赖是指容器负责创建Bean并管理Bean的生命周期,正是由于由容器来控制创建Bean并注入依赖,也就是控制权被反转了,这也正是IoC名字的由来,此处的有依赖是指Bean和容器之间的依赖关系。
容器注入Bean的依赖资源:容器负责注入Bean的依赖资源,依赖资源可以是Bean、外部文件、常量数据等,在Java中都反映为对象,并且由容器负责组装Bean之间的依赖关系,此处的依赖是指Bean之间的依赖关系,可以认为是传统类与类之间的“关联”、“聚合”、“组合”关系。
为什么要应用依赖注入,应用依赖注入能给我们带来哪些好处呢?
动态替换Bean依赖对象,程序更灵活:替换Bean依赖对象,无需修改源文件:应用依赖注入后,由于可以采用配置文件方式实现,从而能随时动态的替换Bean的依赖对象,无需修改java源文件;
更好实践面向接口编程,代码更清晰:在Bean中只需指定依赖对象的接口,接口定义依赖对象完成的功能,通过容器注入依赖实现;
更好实践优先使用对象组合,而不是类继承:因为IoC容器采用注入依赖,也就是组合对象,从而更好的实践对象组合。
增加Bean可复用性:依赖于对象组合,Bean更可复用且复用更简单;
降低Bean之间耦合:由于我们完全采用面向接口编程,在代码中没有直接引用Bean依赖实现,全部引用接口,而且不会出现显示的创建依赖对象代码,而且这些依赖是由容器来注入,很容易替换依赖实现类,从而降低Bean与依赖之间耦合;
代码结构更清晰:要应用依赖注入,代码结构要按照规约方式进行书写,从而更好的应用一些最佳实践,因此代码结构更清晰。
从以上我们可以看出,其实依赖注入只是一种装配对象的手段,设计的类结构才是基础,如果设计的类结构不支持依赖注入,Spring IoC容器也注入不了任何东西,从而从根本上说“如何设计好类结构才是关键,依赖注入只是一种装配对象手段”。
前边IoC一章我们已经了解了Bean依赖容器,那容器如何注入Bean的依赖资源,Spring IoC容器注入依赖资源主要有以下两种基本实现方式:
构造器注入:就是容器实例化Bean时注入那些依赖,通过在在Bean定义中指定构造器参数进行注入依赖,包括实例工厂方法参数注入依赖,但静态工厂方法参数不允许注入依赖;
setter注入:通过setter方法进行注入依赖;
方法注入:能通过配置方式替换掉Bean方法,也就是通过配置改变Bean方法 功能。
我们已经知道注入实现方式了,接下来让我们来看看具体配置吧。
使用构造器注入通过配置构造器参数实现,构造器参数就是依赖。除了构造器方式,还有静态工厂、实例工厂方法可以进行构造器注入。如图3-1所示:
图3-1 实例化
构造器注入可以根据参数索引注入、参数类型注入或Spring3支持的参数名注入,但参数名注入是有限制的,需要使用在编译程序时打开调试模式(即在编译时使用“javac –g:vars”在class文件中生成变量调试信息,默认是不包含变量调试信息的,从而能获取参数名字,否则获取不到参数名字)或在构造器上使用@ConstructorProperties(java.beans.ConstructorProperties)注解来指定参数名。
首先让我们准备测试构造器类HelloImpl3.java,该类只有一个包含两个参数的构造器:
一、根据参数索引注入,使用标签“<constructor-arg index="1" value="1"/>”来指定注入的依赖,其中“index”表示索引,从0开始,即第一个参数索引为0,“value”来指定注入的常量值,配置方式如下:
二、根据参数类型进行注入,使用标签“<constructor-arg type="java.lang.String" value="Hello World!"/>”来指定注入的依赖,其中“type”表示需要匹配的参数类型,可以是基本类型也可以是其他类型,如“int”、“java.lang.String”,“value”来指定注入的常量值,配置方式如下:
三、根据参数名进行注入,使用标签“<constructor-arg name="message" value="Hello World!"/>”来指定注入的依赖,其中“name”表示需要匹配的参数名字,“value”来指定注入的常量值,配置方式如下:
四、让我们来用具体的例子来看一下构造器注入怎么使用吧。
(1)首先准备Bean类,在此我们就使用“HelloImpl3”这个类。
(2)有了Bean类,接下来要进行Bean定义配置,我们需要配置三个Bean来完成上述三种依赖注入测试,其中Bean ”byIndex”是通过索引注入依赖;Bean ”byType”是根据类型进行注入依赖;Bean ”byName”是根据参数名字进行注入依赖,具体配置文件(resources/chapter3/ constructorDependencyInject.xml)如下:
(3)配置完毕后,在测试之前,因为我们使用了通过构造器参数名字注入方式,请确保编译时class文件包含“变量信息”,具体查看编译时是否包含“变量调试信息”请右击项目,在弹出的对话框选择属性;然后在弹出的对话框选择“Java Compiler”条目,在“Class 文件 生成”框中选择“添加变量信息到Class文件(调试器使用)”,具体如图3-2:
图3-2 编译时打开“添加变量信息选项”
(4)接下来让我们测试一下配置是否工作,具体测试代码(cn.javass.spring.chapter3. DependencyInjectTest)如下:
通过以上测试我们已经会基本的构造器注入配置了,在测试通过参数名字注入时,除了可以使用以上方式,还可以通过在构造器上添加@java.beans.ConstructorProperties({"message", "index"})注解来指定参数名字,在HelloImpl3构造器上把注释掉的“ConstructorProperties”打开就可以了,这个就留给大家做练习,自己配置然后测试一下。
五、大家已经会了构造器注入,那让我们再看一下静态工厂方法注入和实例工厂注入吧,其实它们注入配置是完全一样,在此我们只示范一下静态工厂注入方式和实例工厂方式配置,测试就留给大家自己练习:
(1)静态工厂类
静态工厂类Bean定义配置文件(chapter3/staticFactoryDependencyInject.xml)
(2)实例工厂类
实例工厂类Bean定义配置文件(chapter3/instanceFactoryDependencyInject.xml)
(3)测试代码和构造器方式完全一样,只是配置文件不一样,大家只需把测试文件改一下就可以了。还有一点需要大家注意就是静态工厂方式和实例工厂方式根据参数名字注入的方式只支持通过在class文件中添加“变量调试信息”方式才能运行,ConstructorProperties注解方式不能工作,它只对构造器方式起作用,不建议使用根据参数名进行构造器注入。
setter注入,是通过在通过构造器、静态工厂或实例工厂实例好Bean后,通过调用Bean类的setter方法进行注入依赖,如图3-3所示:
图3-3 setter注入方式
setter注入方式只有一种根据setter名字进行注入:
知道配置方式了,接下来先让我们来做个简单例子吧。
(1)准备测试类HelloImpl4,需要两个setter方法“setMessage”和“setIndex”:
(2)配置Bean定义,具体配置文件(resources/chapter3/setterDependencyInject.xml)片段如下:
(3)该写测试进行测试一下是否满足能工作了,其实测试代码一点没变,变的是配置:
知道如何配置了,但Spring如何知道setter方法?如何将值注入进去的呢?其实方法名是要遵守约定的,setter注入的方法名要遵循“JavaBean getter/setter 方法命名约定”:
JavaBean:是本质就是一个POJO类,但具有一下限制:
该类必须要有公共的无参构造器,如public HelloImpl4() {};
属性为private访问级别,不建议public,如private String message;
属性必要时通过一组setter(修改器)和getter(访问器)方法来访问;
setter方法,以“set” 开头,后跟首字母大写的属性名,如“setMesssage”,简单属性一般只有一个方法参数,方法返回值通常为“void”;
getter方法,一般属性以“get”开头,对于boolean类型一般以“is”开头,后跟首字母大写的属性名,如“getMesssage”,“isOk”;
还有一些其他特殊情况,比如属性有连续两个大写字母开头,如“URL”,则setter/getter方法为:“setURL”和“getURL”,其他一些特殊情况请参看“Java Bean”命名规范。
注入常量是依赖注入中最简单的。配置方式如下所示:
以上两种方式都可以,从配置来看第一种更简洁。注意此处“value”中指定的全是字符串,由Spring容器将此字符串转换成属性所需要的类型,如果转换出错,将抛出相应的异常。
Spring容器目前能对各种基本类型把配置的String参数转换为需要的类型。
注:Spring类型转换系统对于boolean类型进行了容错处理,除了可以使用“true/false”标准的Java值进行注入,还能使用“yes/no”、“on/off”、“1/0”来代表“真/假”,所以大家在学习或工作中遇到这种类似问题不要觉得是人家配置错了,而是Spring容错做的非常好。
用于注入Bean的ID,ID是一个常量不是引用,且类似于注入常量,但提供错误验证功能,配置方式如下所示:
两种方式都可以,上述配置本质上在运行时等于如下方式
第二种方式(<idref bean="bean1"/>)可以在容器初始化时校验被引用的Bean是否存在,如果不存在将抛出异常,而第一种方式(<idref local="bean2"/>)只有在Bean实际使用时才能发现传入的Bean的ID是否正确,可能发生不可预料的错误。因此如果想注入Bean的ID,推荐使用第二种方式。
接下来学习一下如何使用吧:
首先定义测试Bean:
其次定义配置文件(chapter3/idRefInject.xml):
从配置中可以看出,注入的Bean的ID是一个java.lang.String类型,即字符串类型,因此注入的同样是常量,只是具有校验功能。
<idref bean="……"/>将在容器初始化时校验注入的ID对于的Bean是否存在,如果不存在将抛出异常。
<idref local="……"/>将在XML解析时校验注入的ID对于的Bean在当前配置文件中是否存在,如果不存在将抛出异常,它不同于<idref bean="……"/>,<idref local="……"/>是校验发生在XML解析式而非容器初始化时,且只检查当前配置文件中是否存在相应的Bean。
Spring不仅能注入简单类型数据,还能注入集合(Collection、无序集合Set、有序集合List)类型、数组(Array)类型、字典(Map)类型数据、Properties类型数据,接下来就让我们一个个看看如何注入这些数据类型的数据。
一、注入集合类型:包括Collection类型、Set类型、List类型数据:
(1)List类型:需要使用<list>标签来配置注入,其具体配置如下:
让我们来写个测试来练习一下吧:
准备测试类:
进行Bean定义,在配置文件(resources/chapter3/listInject.xml)中配置list注入:
测试代码:
(2)Set类型:需要使用<set>标签来配置注入,其配置参数及含义和<lsit>标签完全一样,在此就不阐述了:
准备测试类:
进行Bean定义,在配置文件(resources/chapter3/listInject.xml)中配置list注入:
具体测试代码就不写了,和listBean测试代码完全一样。
(2)Collection类型:因为Collection类型是Set和List类型的基类型,所以使用<set>或<list>标签都可以进行注入,配置方式完全和以上配置方式一样,只是将测试类属性改成“Collection”类型,如果配置有问题,可参考cn.javass.spring.chapter3.DependencyInjectTest测试类中的testCollectionInject测试方法中的代码。
二、注入数组类型:需要使用<array>标签来配置注入,其中标签属性“value-type”和“merge”和<list>标签含义完全一样,具体配置如下:
如果练习时遇到配置问题,可以参考“cn.javass.spring.chapter3.DependencyInjectTest”测试类中的testArrayInject测试方法中的代码。
三、注入字典(Map)类型:字典类型是包含键值对数据的数据结构,需要使用<map>标签来配置注入,其属性“key-type”和“value-type”分别指定“键”和“值”的数据类型,其含义和<list>标签的“value-type”含义一样,在此就不罗嗦了,并使用<key>子标签来指定键数据,<value>子标签来指定键对应的值数据,具体配置如下:
如果练习时遇到配置问题,可以参考“cn.javass.spring.chapter3.DependencyInjectTest”测试类中的testMapInject测试方法中的代码。
四、Properties注入:Spring能注入java.util.Properties类型数据,需要使用<props>标签来配置注入,键和值类型必须是String,不能变,子标签<prop key=”键”>值</prop>来指定键值对,具体配置如下:
如果练习时遇到配置问题,可以参考cn.javass.spring.chapter3.DependencyInjectTest测试类中的testPropertiesInject测试方法中的代码。
到此我们已经把简单类型及集合类型介绍完了,大家可能会问怎么没见注入“Bean之间关系”的例子呢?接下来就让我们来讲解配置Bean之间依赖关系,也就是注入依赖Bean。
上边章节已经介绍了注入常量、集合等基本数据类型和集合数据类型,本小节将介绍注入依赖Bean及注入内部Bean。
引用其他Bean的步骤与注入常量的步骤一样,可以通过构造器注入及setter注入引用其他Bean,只是引用其他Bean的注入配置稍微变化了一下:可以将“<constructor-arg index="0" value="Hello World!"/>”和“<property name="message" value="Hello World!"/>”中的value属性替换成bean属性,其中bean属性指定配置文件中的其他Bean的id或别名。另一种是把<value>标签替换为<.ref bean=”beanName”>,bean属性也是指定配置文件中的其他Bean的id或别名。那让我们看一下具体配置吧:
一、构造器注入方式:
(1)通过” <constructor-arg>”标签的ref属性来引用其他Bean,这是最简化的配置:
(2)通过” <constructor-arg>”标签的子<ref>标签来引用其他Bean,使用bean属性来指定引用的Bean:
二、setter注入方式:
(1)通过” <property>”标签的ref属性来引用其他Bean,这是最简化的配置:
(2)通过” <property>”标签的子<ref>标签来引用其他Bean,使用bean属性来指定引用的Bean:
三、接下来让我们用个具体例子来讲解一下具体使用吧:
(1)首先让我们定义测试引用Bean的类,在此我们可以使用原有的HelloApi实现,然后再定义一个装饰器来引用其他Bean,具体装饰类如下:
(2)定义好了测试引用Bean接下来该在配置文件(resources/chapter3/beanInject.xml)进行配置Bean定义了,在此将演示通过构造器及setter方法方式注入依赖Bean:
(3)测试一下吧,测试代码(cn.javass.spring.chapter3.DependencyInjectTest)片段如下:
四、其他引用方式:除了最基本配置方式以外,Spring还提供了另外两种更高级的配置方式,<ref local=””/>和<ref parent=””/>:
(1)<ref local=””/>配置方式:用于引用通过<bean id=”beanName”>方式中通过id属性指定的Bean,它能利用XML解析器的验证功能在读取配置文件时来验证引用的Bean是否存在。因此如果在当前配置文件中有相互引用的Bean可以采用<ref local>方式从而如果配置错误能在开发调试时就发现错误。
如果引用一个在当前配置文件中不存在的Bean将抛出如下异常:
org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line21 inXML document from class path resource [chapter3/beanInject2.xml] is invalid; nested exception is org.xml.sax.SAXParseException: cvc-id.1: There is no ID/IDREF binding for IDREF ‘helloApi‘.
<ref local>具体配置方式如下:
(2)<ref parent=””/>配置方式:用于引用父容器中的Bean,不会引用当前容器中的Bean,当然父容器中的Bean和当前容器的Bean是可以重名的,获取顺序是直接到父容器找。具体配置方式如下:
接下来让我们用个例子演示一下<ref local>和<ref parent>的配置过程:
首先还是准备测试类,在此我们就使用以前写好的HelloApiDecorator和HelloImpl4类;其次进行Bean定义,其中当前容器bean1引用本地的”helloApi”,而”bean2”将引用父容器的”helloApi”,配置如下:
(3)写测试类测试一下吧,具体代码片段如下:
“bean1”将输出“Hello Local!”表示引用当前容器的Bean,”bean2”将输出“Hello Paren!”,表示引用父容器的Bean,如配置有问题请参考cn.javass.spring.chapter3.DependencyInjectTest中的testLocalAndparentBeanInject测试方法。
内部Bean就是在<property>或<constructor-arg>内通过<bean>标签定义的Bean,该Bean不管是否指定id或name,该Bean都会有唯一的匿名标识符,而且不能指定别名,该内部Bean对其他外部Bean不可见,具体配置如下:
(1)让我们写个例子测试一下吧,具体配置文件如下:
(2)测试代码(cn.javass.spring.chapter3.DependencyInjectTest.testInnerBeanInject):
Spring通过<value>标签或value属性注入常量值,所有注入的数据都是字符串,那如何注入null值呢?通过“null”值吗?当然不是因为如果注入“null”则认为是字符串。Spring通过<null/>标签注入null值。即可以采用如下配置方式:
所谓对象图导航是指类似a.b.c这种点缀访问形式的访问或修改值。Spring支持对象图导航方式依赖注入。对象图导航依赖注入有一个限制就是比如a.b.c对象导航图注入中a和b必须为非null值才能注入c,否则将抛出空指针异常。
Spring不仅支持对象的导航,还支持数组、列表、字典、Properties数据类型的导航,对Set数据类型无法支持,因为无法导航。
数组和列表数据类型可以用array[0]、list[1]导航,注意”[]”里的必须是数字,因为是按照索引进行导航,对于数组类型注意不要数组越界错误。
字典Map数据类型可以使用map[1]、map[str]进行导航,其中“[]”里的是基本类型,无法放置引用类型。
让我们来练习一下吧。首先准备测试类,在此我们需要三个测试类,以便实现对象图导航功能演示:
NavigationC类用于打印测试代码,从而观察配置是否正确;具体类如下所示:
NavigationB类,包含对象和列表、Properties、数组字典数据类型导航,而且这些复合数据类型保存的条目都是对象,正好练习一下如何往复合数据类型中注入对象依赖。具体类如下所示:
NavigationA类是我们的前端类,通过对它的导航进行注入值,具体代码如下:
接下来该进行Bean定义配置(resources/chapter3/navigationBeanInject.xml)了,首先让我们配置一下需要被导航的数据,NavigationC和NavigationB类,其中配置NavigationB时注意要确保比如array字段不为空值,这就需要或者在代码中赋值如“NavigationC[] array = new NavigationC[1];”,或者通过配置文件注入如“<list></list>”注入一个不包含条目的列表。具体配置如下:
配置完需要被导航的Bean定义了,该来配置NavigationA导航Bean了,在此需要注意,由于“navigationB”属性为空值,在此需要首先注入“navigationB”值;还有对于数组导航不能越界否则报错;具体配置如下:
配置完毕,具体测试代码在cn.javass.spring.chapter3. DependencyInjectTest,让我们看下测试代码吧:
测试完毕,应该输出5个“===navigation c”,是不是很简单,注意这种方式是不推荐使用的,了解一下就够了,最好使用3.1.5一节使用的配置方式。
让我们来总结一下依赖注入配置及简写形式,其实我们已经在以上部分穿插着进行简化配置了:
一、构造器注入:
1)常量值
简写:<constructor-arg index="0" value="常量"/>
全写:<constructor-arg index="0"><value>常量</value></constructor-arg>
2)引用
简写:<constructor-arg index="0" ref="引用"/>
全写:<constructor-arg index="0"><ref bean="引用"/></constructor-arg>
二、setter注入:
1)常量值
简写:<property name="message" value="常量"/>
全写:<property name="message"><value>常量</value></ property>
2)引用
简写:<property name="message" ref="引用"/>
全写:<property name="message"><ref bean="引用"/></ property>
3)数组:<array>没有简写形式
4)列表:<list>没有简写形式
5)集合:<set>没有简写形式
6)字典
简写:<map>
<entry key="键常量" value="值常量"/>
<entry key-ref="键引用" value-ref="值引用"/>
</map>
全写:<map>
<entry><key><value>键常量</value></key><value>值常量</value></entry>
<entry><key><ref bean="键引用"/></key><ref bean="值引用"/></entry>
</map>
7)Properties:没有简写形式
三、使用p命名空间简化setter注入:
使用p命名空间来简化setter注入,具体使用如下:
循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用。如图3-5所示:
图3-5 循环引用
循环调用是无法解决的,除非有终结条件,否则就是死循环,最终导致内存溢出错误。
Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:
一、构造器循环依赖:表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。
如在创建CircleA类时,构造器需要CircleB类,那将去创建CircleB,在创建CircleB类时又发现需要CircleC类,则又去创建CircleC,最终在创建CircleC时发现又需要CircleA;从而形成一个环,没办法创建。
Spring容器将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。
1)首先让我们看一下配置文件(chapter3/circleInjectByConstructor.xml):
2)写段测试代码(cn.javass.spring.chapter3.CircleTest)测试一下吧:
让我们分析一下吧:
1、Spring容器创建“circleA” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleB”,并将“circleA” 标识符放到“当前创建Bean池”;
2、Spring容器创建“circleB” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleC”,并将“circleB” 标识符放到“当前创建Bean池”;
3、Spring容器创建“circleC” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleA”,并将“circleC” 标识符放到“当前创建Bean池”;
4、到此为止Spring容器要去创建“circleA”Bean,发现该Bean 标识符在“当前创建Bean池”中,因为表示循环依赖,抛出BeanCurrentlyInCreationException。
二、setter循环依赖:表示通过setter注入方式构成的循环依赖。
对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。
如下代码所示,通过提前暴露一个单例工厂方法,从而使其他Bean能引用到该Bean。
具体步骤如下:
1、Spring容器创建单例“circleA” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleA” 标识符放到“当前创建Bean池”;然后进行setter注入“circleB”;
2、Spring容器创建单例“circleB” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory”用于返回一个提前暴露一个创建中的Bean,并将“circleB” 标识符放到“当前创建Bean池”,然后进行setter注入“circleC”;
3、Spring容器创建单例“circleC” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleC” 标识符放到“当前创建Bean池”,然后进行setter注入“circleA”;进行注入“circleA”时由于提前暴露了“ObjectFactory”工厂从而使用它返回提前暴露一个创建中的Bean;
4、最后在依赖注入“circleB”和“circleA”,完成setter注入。
对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。
对于“singleton”作用域Bean,可以通过“setAllowCircularReferences(false);”来禁用循环引用:
补充:出现循环依赖是设计上的问题,一定要避免!
请参考《敏捷软件开发:原则、模式与实践》中的“无环依赖”原则
延迟初始化也叫做惰性初始化,指不提前初始化Bean,而是只有在真正使用时才创建及初始化Bean。
配置方式很简单只需在<bean>标签上指定 “lazy-init” 属性值为“true”即可延迟初始化Bean。
Spring容器会在创建容器时提前初始化“singleton”作用域的Bean,“singleton”就是单例的意思即整个容器每个Bean只有一个实例,后边会详细介绍。Spring容器预先初始化Bean通常能帮助我们提前发现配置错误,所以如果没有什么情况建议开启,除非有某个Bean可能需要加载很大资源,而且很可能在整个应用程序生命周期中很可能使用不到,可以设置为延迟初始化。
延迟初始化的Bean通常会在第一次使用时被初始化;或者在被非延迟初始化Bean作为依赖对象注入时在会随着初始化该Bean时被初始化,因为在这时使用了延迟初始化Bean。
容器管理初始化Bean消除了编程实现延迟初始化,完全由容器控制,只需在需要延迟初始化的Bean定义上配置即可,比编程方式更简单,而且是无侵入代码的。
具体配置如下:
depends-on是指指定Bean初始化及销毁时的顺序,使用depends-on属性指定的Bean要先初始化完毕后才初始化当前Bean,由于只有“singleton”Bean能被Spring管理销毁,所以当指定的Bean都是“singleton”时,使用depends-on属性指定的Bean要在指定的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用来定义一些初始化操作并向文件中输出文件头信息;最后定义销毁方法用于在关闭应用程序时想文件中输出文件尾信息。
具体代码如下:
2)类定义好了,让我们来进行Bean定义吧,具体配置文件如下:
<property name="file" value="D:/test.txt"/>配置:Spring容器能自动把字符串转换为java.io.File。
init-method="init" :指定初始化方法,在构造器注入和setter注入完毕后执行。
destroy-method="destroy":指定销毁方法,只有“singleton”作用域能销毁,“prototype”作用域的一定不能,其他作用域不一定能;后边再介绍。
在此配置中,resourceBean初始化在dependentBean之前被初始化,resourceBean销毁会在dependentBean销毁之后执行。
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定义配置:
测试代码如下:
是不是不要配置<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方法):
2)根据类型找到多个Bean时,对于集合类型(如List、Set)将注入所有匹配的候选者,而对于其他类型遇到这种情况可能需要使用“autowire-candidate”属性为false来让指定的Bean放弃作为自动装配的候选者,或使用“primary”属性为true来指定某个Bean为首选Bean:
2.1)通过设置Bean定义的“autowire-candidate”属性为false来把指定Bean后自动装配候选者中移除:
2.2)通过设置Bean定义的“primary”属性为true来把指定自动装配时候选者中首选Bean:
具体测试请参考AutowireBeanTest类的testAutowireByType***方法。
五、“constructor”:通过设置Bean定义属性autowire="constructor",功能和“byType”功能一样,根据类型注入构造器参数,只是用于构造器注入方式,直接看例子吧:
测试代码如下:
六、autodetect:自动检测是使用“constructor”还是“byType”自动装配方式,已不推荐使用。如果Bean有空构造器那么将采用“byType”自动装配方式,否则使用“constructor”自动装配方式。此处要把3.0的xsd替换为2.5的xsd,否则会报错。
可以采用在“<beans>”标签中通过“default-autowire”属性指定全局的自动装配方式,即如果default-autowire=”byName”,将对所有Bean进行根据名字进行自动装配。
不是所有类型都能自动装配:
数组、集合、字典类型的根据类型自动装配和普通类型的自动装配是有区别的:
自动装配我们已经介绍完了,自动装配能带给我们什么好处呢?首先,自动装配确实减少了配置文件的量;其次, “byType”自动装配能在相应的Bean更改了字段类型时自动更新,即修改Bean类不需要修改配置,确实简单了。
自动装配也是有缺点的,最重要的缺点就是没有了配置,在查找注入错误时非常麻烦,还有比如基本类型没法完成自动装配,所以可能经常发生一些莫名其妙的错误,在此我推荐大家不要使用该方式,最好是指定明确的注入方式,或者采用最新的Java5+注解注入方式。所以大家在使用自动装配时应该考虑自己负责项目的复杂度来进行衡量是否选择自动装配方式。
自动装配注入方式能和配置注入方式一同工作吗?当然可以,大家只需记住配置注入的数据会覆盖自动装配注入的数据。
大家是否注意到对于采用自动装配方式时如果没找到合适的的Bean时什么也不做,这样在程序中总会莫名其妙的发生一些空指针异常,而且是在程序运行期间才能发现,有没有办法能在提前发现这些错误呢?接下来就让我来看下依赖检查吧。
上一节介绍的自动装配,很可能发生没有匹配的Bean进行自动装配,如果此种情况发生,只有在程序运行过程中发生了空指针异常才能发现错误,如果能提前发现该多好啊,这就是依赖检查的作用。
依赖检查:用于检查Bean定义的属性都注入数据了,不管是自动装配的还是配置方式注入的都能检查,如果没有注入数据将报错,从而提前发现注入错误,只检查具有setter方法的属性。
Spring3+也不推荐配置方式依赖检查了,建议采用Java5+ @Required注解方式,测试时请将XML schema降低为2.5版本的,和自动装配中“autodetect”配置方式的xsd一样。
依赖检查有none、simple、object、all四种方式,接下来让我们详细介绍一下:
一、none:默认方式,表示不检查;
二、objects:检查除基本类型外的依赖对象,配置方式为:dependency-check="objects",此处我们为HelloApiDecorator添加一个String类型属性“message”,来测试如果有简单数据类型的属性为null,也不报错;
注意由于我们没有注入bean需要的依赖“helloApi”,所以应该抛出异常UnsatisfiedDependencyException,表示没有发现满足的依赖:
三、simple:对基本类型进行依赖检查,包括数组类型,其他依赖不报错;配置方式为:dependency-check="simple",以下配置中没有注入message属性,所以会抛出异常:
四、all:对所以类型进行依赖检查,配置方式为:dependency-check="all",如下配置方式中如果两个属性其中一个没配置将报错。
依赖检查也可以通过“<beans>”标签中default-dependency-check属性来指定全局依赖检查配置。
所谓方法注入其实就是通过配置方式覆盖或拦截指定的方法,通常通过代理模式实现。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字段记录访问次数:
HelloImpl5类用于打印欢迎信息,其中包括setter注入和方法注入,此处特别需要注意的是该类是抽象的,充分说明了需要容器对其进行子类化处理,还定义了一个抽象方法createPrototypePrinter用于创建“prototype”Bean,createSingletonPrinter方法用于创建“singleton”Bean,此处注意方法会被Spring拦截,不会执行方法体代码:
2) 开始配置了,配置文件在(resources/chapter3/lookupMethodInject.xml),其中“prototypePrinter”是“prototype”Printer,“singletonPrinter”是“singleton”Printer,“helloApi1”是“singleton”Bean,而“helloApi2”注入了“prototype”Bean:
3)测试代码如下:
其中“helloApi1”测试中,其输出结果如下:
首先“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”测试中,其输出结果如下:
首先“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接口实现,然后如下配置来实现:
1)首先定义MethodReplacer实现,完全替换掉被替换方法的方法体及返回值,其中reimplement方法重定义方法 功能,参数obj为被替换方法的对象,method为被替换方法,args为方法参数;最需要注意的是不能再 通过“method.invoke(obj, new String[]{"hehe"});” 反射形式再去调用原来方法,这样会产生循环调用;如果返回值类型为Void,请在实现中返回null:
2)配置如下,首先定义MethodReplacer实现,使用< replaced-method >标签来指定要进行替换方法,属性name指定替换的方法名字,replacer指定该方法的重新实现者,子标签< arg-type >用来指定原来方法参数的类型,必须指定否则找不到原方法:
3)测试代码将输出“Print Replacer ”,说明方法体确实被替换了:
什么是作用域呢?即“scope”,在面向对象程序设计中一般指对象或变量之间的可见范围。而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围。
Spring提供“singleton”和“prototype”两种基本作用域,另外提供“request”、“session”、“global session”三种web作用域;Spring还允许用户定制自己的作用域。
一、singleton:指“singleton”作用域的Bean只会在每个Spring IoC容器中存在一个实例,而且其完整生命周期完全由Spring容器管理。对于所有获取该Bean的操作Spring容器将只返回同一个Bean。
GoF单例设计模式指“保证一个类仅有一个实例,并提供一个访问它的全局访问点”,介绍了两种实现:通过在类上定义静态属性保持该实例和通过注册表方式。
1)通过在类上定义静态属性保持该实例:一般指一个Java虚拟机 ClassLoader装载的类只有一个实例,一般通过类静态属性保持该实例,这样就造成需要单例的类都需要按照单例设计模式进行编码;Spring没采用这种方式,因为该方式属于侵入式设计;代码样例如下:
以上定义个了个单例类,首先要私有化类构造器;其次使用InstanceHolder静态内部类持有单例对象,这样可以得到惰性初始化好处;最后提供全局访问点getInstance,使得需要该单例实例的对象能获取到;我们在此还提供了一个counter计数器来验证一个ClassLoader一个实例。具体一个ClassLoader有一个单例实例测试请参考代码“cn.javass.spring.chapter3. SingletonTest”中的“testSingleton”测试方法,里边详细演示了一个ClassLoader有一个单例实例。
1) 通过注册表方式: 首先将需要单例的实例通过唯一键注册到注册表,然后通过键来获取单例,让我们直接看实现吧,注意本注册表实现了Spring接口“SingletonBeanRegistry”,该接口定义了操作共享的单例对象,Spring容器实现将实现此接口;所以共享单例对象通过“registerSingleton”方法注册,通过“getSingleton”方法获取,消除了编程方式单例,注意在实现中不考虑并发:
Spring是注册表单例设计模式的实现,消除了编程式单例,而且对代码是非入侵式。
接下来让我们看看在Spring中如何配置单例Bean吧,在Spring容器中如果没指定作用域默认就是“singleton”,配置方式通过scope属性配置,具体配置如下:
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”表示作用域,具体如下:
2)接下来让我们看看Bean定义注册表,类似于单例注册表:
3)接下来应该来定义BeanFactory了:
其中方法getBean用于获取根据beanName对于的Bean定义创建的对象,有单例和原型两类Bean;registerBeanDefinition方法用于注册Bean定义,私有方法createBean用于根据Bean定义中的类型信息创建Bean。
3)测试一下吧,在此我们只测试原型作用域Bean,对于每次从Bean工厂中获取的Bean都是一个全新的对象,代码片段(BeanFatoryTest)如下:
最后让我们看看如何在Spring中进行配置吧,只需指定<bean>标签属性“scope”属性为“prototype”即可:
Spring管理原型对象在Spring容器中存储如图3-6所示,Spring不会缓存原型对象,而是根据Bean定义每次请求返回一个全新的Bean:
图3-6 原型处理
单例和原型作用域我们已经讲完,接下来让我们学习一些在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时讲解具体使用,大家只需要知道有这些作用域就可以了。
在日常程序开发中,几乎用不到自定义作用域,除非又必要才进行自定义作用域。
首先让我们看下Scope接口吧:
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内。
Scope已经实现了,让我们将其注册到Spring容器,使其发挥作用:
通过CustomScopeConfigurer的scopes属性注册自定义作用域实现,在此需要指定使用作用域的关键字“thread”,并指定自定义作用域实现。来让我们来定义一个“thread”作用域的Bean,配置(chapter3/threadScope.xml)如下:
最后测试(cn.javass.spring.chapter3.ThreadScopeTest)一下吧,首先在一个线程中测试,在同一线程中获取的Bean应该是一样的;再让我们开启两个线程,然后应该这两个线程创建的Bean是不一样:
自定义作用域实现其实是非常简单的,其实复杂的是如果需要销毁Bean,自定义作用域如何正确的销毁Bean。
本文借鉴:【http://sishuok.com/forum/blogPost/list/2454.html】
版权声明:欢迎转载,希望在你转载的同时,添加原文地址,谢谢配合
标签:
原文地址:http://blog.csdn.net/u011225629/article/details/47143065