内容过长,core部分分开发布,core章节第1部分点击:Spring Framework 官方文档中文版—Core_part_1
主目录或专题地址可以点击:主目录, 专题地址
自动装(Autowiring)配协作者
Spring容器可以自动装配协作bean之间的关系。你可以通过检查ApplicationContext的内容,让Spring自动为你的bean解析协作者(即其他bean)。自动装配有以下优点:
自动装配可以显著减少指定属性或构造方法的场景。
自动装配可以随着你的对象改变而改变。例如,你需要往一个类中增加依赖,那么无需修改配置就可以自动满足这个需求。因此自动装配在开发过程中非常有用,当代码库趋于稳定时,不需要否定显示的装配。(最后一句原文:Thus autowiring can be especially useful during development, without negating the option of switching to explicit wiring when the code base becomes more stable.)
当使用基于XML的配置时,你需要在<bean/>
标签内指定属性autowire
。自动装配功能有4种模式。你可以为每个bean单独指定自动装配,所以也就可以选择其中一个进行自动装配。
下面是自动装配的4种模式:
模式 | 说明 |
---|---|
no | (默认)不自动装配。bean的引用必须通过ref 属性标签来指定。对于较大型的项目,并不推荐修改此默认配置,因为明确的指定bean可以更易于管理和更可控。在某种意义上,这相当于是记录了系统的结构 |
byName | 根据名称自动装配。Spring自动寻找同名的bean作为需要装配的属性。例如,如果设置了一个bean定义为byName自动装配,并且含有一个master属性(也就是说它有一个setMaster(..)方法) ,Spring寻找到名称为master的bean定义,并设置到其属性中 |
byType | 如果容器中恰好和属性类型相同的bean,那么允许将这个bean自动装配到属性。如果这种bean的数量为多个则会抛出异常,表明你并不适合用词类型的自动装配,如果没有此类型的bean匹配,则不会发生什么异常,属性也就有可能没有被设置 |
constructor | 和bytype类似,但是是用于构造函数参数,如果容器中没有一个和构造函数参数类型一样的bean,则会引发致命异常 |
使用byType 或 constructor 自动装配模式,你可以装配数组和集合类型。这种情况下,容器内所有与预期类型匹配的bean都会被装配至此数据或集合。你可以自动装配强类型的map,如果key类型正好的String
。自动装配的map的value是由和预期类型一样的bean组成,key的值会包含bean的名称。
你可以将自动装配的行为与依赖检查相结合,依赖检查是在自动装配结束后开始执行的。
自动装配的局限性和缺点*
自动装配在项目中应用时,要么全部使用,不要部分使用。如果一般不使用自动装配,只是在少数的一两个bean定义中使用它,自动装配可能会让开发者产生混淆。
考虑自动装配的局限性和缺点:
property 和 constructor-arg中明确的依赖会覆盖自动装配。你不能自动装配简单类型的属性,如原始类型,String,Class(以及这些简单类型组成的数组)。这个局限是因为就是这么设计的
明确的装配要比自动装配准确。尽管如上面的表所示,Spring小心的避免猜测所导致预期外的结果,但是项目中被Spring管理的对象关系并不会被明确记录。
可能无法从Spring容器生成文档的工具获得使用连接信息。
setter方法或构造函数所指的的类型,容器中可能会存在多个bean匹配上。对于数组,集合或map来说这并不是一个问题。然而对依赖来说只需要一个结果的话,这并不会被有效的解决。如果非唯一值存在,则会抛出致命的异常。
在下面,你可以有以下几个选择:
放弃自动装配,全部使用显示的(常规)装配
将其
autowire-candidate
属性设置为false,避免bean定义进行自动装配,如下一节所示。指派一个单独的bean定义作为主要对象,需要将
<bean/>
标签的属性primary
属性设置为true。使用基于注解的容器配置,实现更细粒度的配置。
自动装配中排除一个bean
基于每个bean的配置,你可以将一个bean从自动装配中排除。在Spring XML格式中,设置<bean/>
标签中的属性autowire-candidate
为false;容器会让此bean定义无法进行自动装配(包括注解风格的配置例如@Autowired
)。
属性
autowire-candidate
只对基于类型的自动装配有效。它不对明确的装配例如byName类型的自动装配有效,即使某bean没有被标记为autowire选项,它也会被解析。因此,只要名称匹配,自动装配总会注入一个bean。
你也可以利用表达式来限制自动装配候选者的名称。最顶层标签<beans/>
的default-autowire-candidates
属性接受一个或多个表达式。例如,限制候选bean的名称是以 Repository 结尾,只需要将表达式写为 *Repository 。要写入多个表达式,则表达式之间用逗号分隔。对于一个bean定义来说,明确指定autowire-candidate
属性的值为true或false总是优先的,对于这个bean定义来说,表达式匹配规则并不生效。
这些技术,对于那些你不希望通过自动装配注入到其他bean的bean十分有用。这并不意味着被排除的bean,本身不能配置自动装配。相反的,而是bean本身不是其他自动装配bean的候选者。
方法注入(Method injection)
大多数应用场景中,容器中大部分bean都是单例的。当一个单例的bean,需要另一个单例bean协作,或者一个非单例bean需要另一个非单例bean协作,你通常通过定义一个bean作为另一个bean的属性来处理这种依赖关系。当bean的生命周期不同时问题就出现了。假设一个单例bean A需要一个非单例的bean B,也许A的每个方法都调用了。容器只创建bean A一次,因此也就只有一次机会来设置A的属性。当需要时,容器不一定能为bean A提供一个新的bean B的实例。
一个解决办法是放弃一些控制反转。你可以通过实现ApplicationContextAware接口,创建一个bean A aware(此单词不翻译,直译为:知道的,了解的),当bean A需要beanB的时候,容器调用getBean("B")方法来获得bean B。下面是相关的例子:
// 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) {
// 创建一个 Command实例
Command command = createCommand();
// 将状态设置为命令实例
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// 注意引用的spring依赖
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
上面的这个例子,在实际应用中是不可取的,因为你的业务代码耦合到了Spring代码中!方法注入是Spring IoC容器的一个高级特性,它能够以一种干净的方式来处理这个例子!
基于查找的方法注入(Lookup method injection)
Lookup方法注入,是指容器在其管理的bean上重写方法,将查找结果返回给容器中定义好的bean。查找通常涉及前面所讲的prototype类型的bean。Spring Framework实现这中方法注入主要使用了CGLIB的字节码生成技术去动态生成子类去覆盖方法。
- 为了成功生成和使用这个动态子类,Spring要去继承的类不能为final,同样的,要被覆盖的方法也不能是final的。
- 单元测试时,拥有抽象方法的类需要你自己去继承这个类并且实现它的抽象方法。
- 组件扫描也需要非抽象方法,这需要非抽象类来提取。
- 进一步的限制就是,方法查找注入不能用于工厂方法,尤其是不能在配置类中使用@Bean注解,因为这个情况下容器不负责创建实例。所以在运行时创建子类。
在前面的CommandManager
类中,你可以看见,Spring容器会动态的覆盖createCommand()
方法的实现。CommandManager类不会有任何Spring依赖项,我们可以看其重写的例子:
package fiona.apple;
// 没有任何Spring的依赖!
public abstract class CommandManager {
public Object process(Object commandState) {
// 获取Command接口的新实例
Command command = createCommand();
command.setState(commandState);
// 为新Command实例设置状态
return command.execute();
}
// ok!但是,这个抽象方法在哪里来实现呢?
protected abstract Command createCommand();
}
包含被注入方法的类(本例中的CommandManager),被注入的方法需要满足下面的格式:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是抽象的,会用动态生成子类来实现这个方法。动态生成子类会覆盖那个方法。例如:
<!-- bean的scope被设置成prototype(非单例的) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
不管什么时候,只要commandManager bean调用自己的createCommand()方法它都需要一个myCommand bean的新实例。你必须要注意将myCommand bean定义设置为prototype
,如果实际需求是这样。如果他是单例的,则每次myCommand bean都会返回同一个实例。
作为选择,也可以使用基于注解的配置,你可以通过@Lookup
注解来声明:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
或者,更通俗的,你可以依赖目标bean的类型,将抽象方法的返回类型修改为目标bean的类型:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
注意,你会声明这样一个注解来查找一个一般方法的实现,以便让它们能够让Spring的组件去扫描。这种方式不适用于显式的注册或者显式导入bean class。
接受不同scope目标bean的另一个方法是ObjectFactory/ Provider
注入点。具体办法在bean scope章节给出。
感兴趣的读者可以去研究一下ServiceLocatorFactoryBean
(在org.springframework.beans.factory.config
包下面)。
替换任意方法
比方法查找注入应用还少的一种方法注入是,可以在管理的bean中用另一个方法实现来替换任意方法。如果你在实际中没有此需求,那么可以完全跳过本节内容。
基于XML的配置中,你可以使用replaced-method
标签去替换已经实现的方法实现。考虑一下下面的类,有一个我们要覆盖的方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
一个实现了org.springframework.beans.factory.support.MethodReplacer
接口的类,定义了一个新方法:
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
定义原始类的bean,并且指定覆盖方法:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- 任意方法替换-->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
你可以看到,<replaced-method/>
标签内包含一个或多个<arg-type/>
标签,来表示被覆盖的方法签名。只有类中方法是重载的,这个参数声明才是必须的。方便起见,字符串类型的参数可以是完全限定的类型名字的一部分,例如,下面的类型都表示的是java.lang.String
:
java.lang.String
String
Str
因为参数数量,通常足够去彼此区分,这种简写可以节省很多拼写。
bean的作用域
当你定义了一个bean,你就创建了一个bean实例化的规则。这是非常重要的,因为这意味着,使用一个类,你可以从一个bean定义来创建多个实例。
你不光可以控制各种各样的的依赖,将配置的值注入到由bean的配置创建的对象,也可以控制由bean定义创建的对象的范围。这种方法非常强大和有效,因为你通过配置文件创建的对象,是可以选择它的范围的,这样避免了你在Java类级别去设置它的范围。bena可以被部署在以下几个范围:Spring支持6个范围,其中4个是只有在web项目中管用的。
下面的范围都是开箱即用的,你也可以定制自己的范围。
范围 | 描述 |
---|---|
singleton | (默认)整个Spring IoC容器中只有一个实例 |
prototype | 每次使用都会创建一个新的实例 |
request | 在一个JTTP请求生命周期内创建一个单实例;就是,每个HTTP请求有它自己的单例的bean,只有在web项目的Spring ApplicationContext的上下文中有效 |
session | 在HTTP session的生命周期内有效。只有在web项目的Spring ApplicationContext的上下文中有效 |
application | 在ServletContext的生命周期内有效。只有在web项目的Spring ApplicationContext的上下文中有效 |
webSocket | 在webSocket的生命周期内有效。 只有在web项目的Spring ApplicationContext的上下文中有效 |
Spring 3.0中,是有线程范围的,但是默认是不注册的。更多信息,请参考<font style=text-decoration:underline color=#548e2e>SimpleThreadScope</font>。像获得如何注册它和其他的自定义scope,可以查看<font style=text-decoration:underline color=#548e2e>使用自定义scope</font>
单例(singleton)作用域
单例的bean,只存在一个被管理的共享实例,对于所有对这个bean实例的请求,例如用id去匹配bean定义,Spring容器都会返回一个特定的bean实例。
换句话说,当你在bean定义中把bean的范围设置成单例的时候,Spring Ioc容器会根据bean的定义只创建一个实例。此单个实例会被存在一个单例bean的缓存中,后面的所有请求和对这个bean的指向,都会返回缓存中的bean实例。
Spring的单例bean概念,不同于Gang of Four (GoF)设计模式书籍中的单例模式。GoF的单例是硬编码对象的范围,对于每个类加载器来说,类的对象有且只有一个实例。Spring的单例范围最好的理解是每一个容器和每一个bean内,有且只有一个实例。这意味着,在一个Spring IoC容器中,你为一个类定义了一个bean定义,Spring容器会为这个bean定义创建一个且只创建一个实例。单例范围是Spring中的默认范围。在xml中创建一个单例的bean,你可以利用下面的方式:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- 这个是与上一个等价的, 通过多余的指定 (singleton的配置是默认的) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
prototype作用域
非单例的,prototype范围的bean,在每次对bean实例的请求都会创建一个新的bean的实例。就是说,bean被注入到另一个bean中或通过getBean()
调用bean的实例。一般来说,使用prototype范围的bean来创建有状态的bean,使用singleton(单例)范围来创建无状态的bean。下面的图表阐明了Spring的prototype范围的bean。比较典型的,一个数据访问对象(DAO)并不会被配置成prototype的bean,因为典型的DAO不具有任何会话状态;对作者来说只是简单的重复使用而已,就像上一节图表中展示的那样。
下面的例子是如何在xml创建prototype的bean:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
对比于其他的范围,Spring并不会完整的管理prototype范围bean的生命周期:容器的实例化,配置,其他方式组装对象,和将其交由客户端,整过程并没有对prototype进行进一步记录。因此,尽管初始化生命周期的回调方法不管是什么范围,在所有对象上都会被调用,但在prototype范围情况下,为其配置生命周期的回调并不会被调用。客户端代码必须要清理prototype对象,并释放prototype bean所占用的珍贵的资源。为了让Spring容器释放prototype bean占用的资源,可以使用BeanPostProcessor进行自定义扩展,这里面包含了需要清理的bean的引用。
在某些方面,Spring容器在prototype域中的角色,就相当于Java中new
操作。所有生命周期的操作都需要由客户端自己来处理。
单例bean依赖于prototype bean
当你使用单例域的bean依赖于prototype bean的时候,要注意,依赖是在实例化的时候才解析。所以如果你将一个peototype域的bean注入到单例域的bean,一个新的prototype bean实例化并且被注入到单例域的bean中。prototype域的实例是提供给单例域的bean的唯一实例。
然而,假设你想单例域的bean在运行时反复获取一个prototype的bean的新实例。你不能将一个prototype的bean依赖注入到单例域的bean中,因为注入只发生一次,是在Spring容器实例化单例域的bean并解析它的依赖时。如果你需要在运行时获得一次或多次prototype bean的实例时,可以参考前面的方法注入章节。
Request,session,application,和WebSocket作用域
只有你在使用一个web方面的Spring ApplicationContext
(例如XmlWebApplicationContext
)实现时,Request,session,application,和WebSocket作用域才会起作用。如果你将这些作用域用在一个常规的Spring IoC容器中例如ClassPathXmlApplicationContext
,则会抛出一个IllegalStateException
异常,告诉你这是一个未知的bean作用域。
初始化web配置
为了支持request
,session
,application
和websocket
等域,在你进行bean定义之前,需要做一些小的配置(这些小的配置在标准域中不需要配置,singleton和 prototype)。
怎么样完成这些初始化步骤,这取决于你的具体Servlet
环境。
如果你使用Spring Web MVC来访问这些域的bean,实际上,就是通过DispatcherServlet
来调用, 根本不需要其他的步骤。
如果你使用Servlet 2.5
容器时,也就是说不使用DispatcherServlet
时(例如使用JSF,Struts等),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener
。对于Servlet 3.5+
,可以实现WebApplicationInitializer
接口,或者对于老版本容器可以在web.xml
中做如下的配置:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果你的listener启动有了问题,可以考虑使用Spring的RequestContextFilter
。这个过滤器的配置依赖与web配置文件,可以参考如下配置:
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
其实DispatcherServlet
, RequestContextListener
, 和 RequestContextFilter
,它们都在做一件事,就是将HTTP request对象绑定到处理请求的线程上,这就可以让request或session域加入到请求链中了。
Request scope
请先看一下下面的XML配置:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
Spring容器会创建一个beanLoginAction
的实例,这样它就可以处理各个HTTP请求。也就是说,LoginAction
是作用在了Http请求级别。你可以随便改变这个实例的内部状态,因为LoginAction
是多实例的,所以实例之间不会受影响;每个请求都会有它自己的实例。当请求结束的时候,实例也会跟着销毁。
可以使用注解的方式来配置请求域,即@RequestScope
,如下所示:
@RequestScope
@Component
public class LoginAction {
// ...
}
Session作用域
看一下下面xml配置的bean:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
对于每个HTTP Session
,Spring容器都会创建一个UserPreferences
的实例。也就是说UserPreferences
是作用范围是Session级别的。和上面讲的request作用域类似,你也可以随便改变实例的内部状态而其他的实例则不受影响。当HTTP Session被销毁的时候,这个实例也就随之销毁。
当基于注解来配置的时候,可以使用@SessionScope
来配置:
@SessionScope
@Component
public class UserPreferences {
// ...
}
Application作用域
先看一下下面的xml配置:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
上面配置的含义是,Spring容器会为整个web应用而创建appPreferences
bean实例,并且只创建一次。也就是说,appPreferences
的作用级别是ServletContext
级别的,是作为ServletContext
的属性来保存的。这与Spring 单例bean有些类似,但是有两点不同:它是对于每个ServletContext
来说的,不是针对ApplicationContext
来说的,还有就是,它是彻底暴露出来的,是作为ServletContext
属性来存储和使用的。
基于注解的配置,可以参考以下内容:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
非单例的bean作为被依赖
Spring IoC容器不光实例化你的bean,同时也会实例化你的bean的依赖。如果你想将一个HTTP请求域的bean注入到另一个bean,并且这个bean将会长期存在,那么你可以注入一个aop代理来替代这个bean。也就是说,你需要注入一个代理对象,并且暴露出一样的public接口,也可以从相关域(例如HTTP request域)来检索实际的目标对象,并将该方法委托给实际对象。
使用单例bena的过程里,可以使用<aop:scoped-proxy/>标签,然后可以引用一个可以序列话的中间代理,从而在反序列化时候的时候重新获得这个单例的bean。
当对一个有具体域范围的bean(例如session域的bean)使用<aop:scoped-proxy/> 标签时,共享代理的的每一次调用都会导致重新创建一次目标实例,然后将该实例转发给调用方。
另外,代理并不是从短生命周期(相对而言)bean获取bean的唯一方式。你也可以简单的声明你的注入点(例如构造/setter方法参数或成员变量)来作为ObjectFactory,同时提供getObject()方法来每次检索按需调用--无需保留实例或单独存储实例。
考虑扩展性,你可以声明一个ObjectProvider<MyTargetBean>,额外提供几个访问方法,例如getIfAvailable 和 getIfUnique。
Provider是JSR-330变体,同时为每次检索和尝试来声明Provider<MyTargetBean>和对应的get()。有关JSR-330的相信信息可以参阅这里
下配置的配置文件很简单,但是足够让你明白“为什么”和“如何”去明白上面讲的是什么意思:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 一个 HTTP Session-scoped bean 是作为一个代理被暴露出来 -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- 一个 singleton-scoped bean 作为代理注入上面的bean -->
<bean id="userService" class="com.foo.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
要创建这样一个代理,你需要在有作用域的bean定义中插入一个子元素<aop:scoped-proxy/>
,那么为什么需要在request, session 和 custom-scope级别中定义一个<aop:scoped-proxy/>
元素?让门来观察一下下面的单例bean,并与上面的例子做对比:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
深入bean的本质
生命周期中的回调
如果想深入到容器对bean生命周期的管理, 你可以实现Spring的InitializingBean
和 DisposableBean
两个接口. 容器在初始化时调用afterPropertiesSet()
方法, 在销毁时调用destroy()
方法.
在spring内部是使用BeanPostProcessor
接口去执行它所能发现的所有回调接口或方法. 如果你想定制一些spring没有提供的功能, 那么你可以自己去实现BeanPostProcessor
接口.
除了初始化回调, 销毁回调以外, spring所管理的对象也可以去实现生命周期接口, 这样的话, 这些对象可以随着容器的生命周期来初始化和停止了.
这些生命周期的回调接口被在此章节来说明.
初始化的回调
实现org.springframework.beans.factory.InitializingBean
接口, 让bean在加载完必要的属性之后, 执行自己所需的初始化工作, 这个接口只指定了一个方法:
void afterPropertiesSet() throws Exception;
其实并不一定推荐你去使用这个接口, 因为这相当于和Spring耦合到了一起. 你也可以使用@PostConstruct
注解或指定一个POJO初始化方法. 使用xml配置的时候, 使用init-method
属性去指定没有返回值的, 没有参数的方法. 使用Java配置的话, 使用@bean
注解的initMethod
属性, 如下面的例子:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
bean的java类:
public class ExampleBean {
public void init() {
// do some initialization work
}
}
实现InitializingBean
:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
java类:
public class AnotherExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// 做一些初始化工作
}
}
销毁的回调
当容器关闭的时候,如果你想让你的bean在这时候执行一些操作,那么可以实现接口org.springframework.beans.factory.DisposableBean
,这个接口只有一个方法:
void destroy() throws Exception;
并不建议以上面这种方式去处理,因为这相当于和spring代码相耦合了。你可以使用@PreDestroy
注解或指定一个普通方法。基于xml的配置,使用<bean/>
标签下的destroy-method
属性。基于Java的配置,可以使用@Bean
注解的destroyMethod
属性. 如下面得例子所示:
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
ExampleBean得Java代码:
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
上面的例子和下面的事一样的, 但是没有和Spring解耦:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
对应的java代码:
public class AnotherExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
标签
<bean/>
下的destroy-method
属性, 可以指定一个值, 然后通过Spring会去自动发现这个bean上的此方法(一些实现了java.lang.AutoCloseable
或java.io.Closeable
接口的累也会因此而匹配上). 这个值也可以设置再default-destroy-method
中,下一节内容会详解介绍.
默认的初始化和销毁方法
当你编写初始化和销毁回调时, 可以不使用使用Spring指定的InitializingBean
和DisposableBean
接口. 通常你应该写类似init(), initialize(), dispose()
的方法, 此时应该尽可能的让这些生命周期方法保持一致, 以便让项目中所有的开发者一目了然.
可以通过配置Spring容器, 让他可以去每个bean中寻找已经命名好的生命周期方法. 这意味着, 作为一个应用开发者, 你可以在你自己的类中定义叫init()
的初始化回调方法, 并且不用在bean的定义中配置init-method="init"
. Spring IoC容器在bean创建时调用此方法. 这种特性会强制执行初始化或回调方法.
可以参考以下例子:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// 这是初始化方法
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
xml:
<beans default-init-method="init">
<bean id="blogService" class="com.foo.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>
bean的配置中, 顶级标签为beans
, 在这个标签中配置default-init-method="init"
, 就可以让Spring容器创建beans
下所有的bean
时, 都去执行init
方法.
同样的, 在配置销毁回调方法时, 在beans
标签中配置default-destroy-method
属性.
如果有的bean的命名和beans
标签重命名不一致, 可以在bean
标签中配置init-method
和 destroy-method
, 这样就可以覆盖默认值了.
多种方式的结合使用
在Spring2.5 中, 你有三种方式来控制bean的生命周期, 他们分别是: 实现 InitializingBean
和 DisposableBean
回调接口; 自定义init()
和 destroy()
方法; 还有使用@PostConstruct
和 @PreDestroy
注解. 对于某一个bean, 你可以结合上面三种方法, 来控制它的生命周期.
如果对一个bean配置了多种生命周期, 并且每种配置的方法名都不一样, 这些配置会按照下面所讲的顺序来执行. 然而, 如果配置了多个一样的方法名的话, 例如初始化回调的
init()
方法, 如果有像上节所讲的内容一样, 它只会执行一次.
在一个bean上使用多种初始化方式, 调用优先级:
- 加了
@PostConstruct
注解的方法 - 实现了
InitializingBean
接口的afterPropertiesSet()
方法 - 自己实现的
init()
方法
对于销毁的回调方法, 他们会同时执行
- 加了
@PreDestroy
注解的方法 - 实现了
DisposableBean
接口的destroy()
方法 - 自定义的
destroy()
方法
启动和停止回调
任何有自己生命周期需求的对象都可以实现接口Lifecycle
接口(例如开始或结束后台进程).
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
任何由Spring管理的对象都可以实现Lifecycle
接口. 当ApplicationContext
在运行时接收到停止或重启的信号时, 它将这些实现了Lifecycle
接口的对象作为一个级联的上下文对象. 它将这些操作委派给了LifecycleProcessor
接口:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
需要注意的是, LifecycleProcessor
是Lifecycle
的子接口. 它增加了两个方法来支持contenx的刷新和关闭.
请注意,
org.springframework.context.Lifecycle
接口只是简单的规定了 开始/结束通知, 在context刷新时并不会自动启动. 可以考虑实现org.springframework.context.SmartLifecycle
接口以来替代某个bean的自动启动的精确控制. 同样的, 请注意停止信号并不是稳定的: 在正常停止下, 所有的生命周期bean都会第一时间接收到停止信号, 然而, 在热刷新的时候, 只有销毁方法会被回调, start方法是不会被回调的.
启动和停止的调用顺序是很重要的. 如果两个对象之间是有依赖的, 依赖方会在其依赖启动后启动, 在依赖停止前停止. 但是有时候依赖关系是未知的.你可能只是知道某一对象可能在另一个对象启动之前启动. 在这个场景下, SmartLifecycle
接口定义了另一种方法叫做getPhase()
, 这个方法实际上是在其父接口Phased
中定义的.
public interface Phased {
int getPhase();
}
接口SmartLifecycle
:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
启动的时候, 最底层的对象首先启动, 关闭的时候, 顺序则正好相反. 所以实现了SmartLifecycle
接口, 并且在getPhase()
方法中返回Integer.MIN_VALUE
的话, 它就会最先启动并且最后停止. 没有实现SmartLifecycle
接口的话则为"0".
SmartLifecycle
的停止方法会接收一个回调. 实现它的对象, 会在其停止前调用调用run()
. 所以停止就实现了异步操作.
在非web应用中优雅的关闭Spring容器
此章节仅针对非web应用。基于Spring的web容器的关闭已经说得很详细了。
如果你再非web项目中使用了Spring IoC容器,例如桌面程序中,你在其中注册了一个JVM关闭的hook。这样通过调用单例上的销毁方法来实现优雅的关闭,可以释放掉相关的资源。当然,你必须做正确的配置和实现。
想要注册一个停止钩子,你需要调用ConfigurableApplicationContext
的registerShutdownHook()
方法:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 为上面的 context 增加一个hook
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}
ApplicationContextAware 和 BeanNameAware
当ApplicationContext
创建了一个实现了org.springframework.context.ApplicationContextAware
的对象时,此时对象和容器之间就存在某种关联了。
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
这样就可以让bean通过编程的方式来操作ApplicationContext
, 也可以使用它的子类, 例如ConfigurableApplicationContext
, 它也提供了更多的功能. 可以通过编程的方式来检索其他的bean. 一般这是比较实用的; 但是一般你需要避免实用它, 因为在一般的业务代码中, 这样做会将Spring的代码耦合近业务代码, 这并不符合Spring的控制反转的风格.
当ApplicationContext
创建了一实现了org.springframework.beans.factory.BeanNameAware
接口的类, 这个类就被赋予了定义它的地方的引用.
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}
这个回调会在一般的的bean之后被调用, 但是会在初始化回调之前被调用, 例如InitializingBean
或自定义初始化方法.
其他的Aware接口
除了上面提到的ApplicationContextAware
和 BeanNameAware
以外, Spring还提供了一些Aware
接口, 也允许bean指向创建它的容器, 但是需要一些依赖. 大部分重要的Aware
接口可以总结如下: 一般可以通过名字来看他的依赖类型:
表: Aware相关接口
名称 | 需要的依赖 | 解释说明 |
---|---|---|
ApplicationContextAware | 声明ApplicationContext | 前面章节中 |
ApplicationEventPublisherAware | 围绕ApplicationContext发布时间 | Additional capabilities of the ApplicationContext |
BeanClassLoaderAware | 加载bean的class的类加载器 | 实例化bean |
BeanFactoryAware | 声明BeanFactory | 前面章节 |
BeanNameAware | 声明bean的命名 | 前面章节 |
BootstrapContext | BootstrapContext的资源装饰器 | JCA CCI |
LoadTimeWeaverAware | 在加载时定义执行类的操作 | Load-time weaving with AspectJ in the Spring Framework |
MessageSourceAware | 配置解析消息的策略 | ApplicationContext |
NotificationPublisherAware | Spring JMX信号发布 | 信号 |
ResourceLoaderAware | 配置底层资源的加载器 | 资源 |
ServletConfigAware | 当前运行中容器的servletConfig,只在ApplicationContext加载的web应用有效 | Spring MVC |
ServletContextAware | 同上 | Spring MVC |
需要再次注意, 使用上述这些接口同时也意味着不再遵循控制反转的style. 只有再你构建项目中比较基础的部分时, 才推荐使用上面的接口(普通业务代码不推荐).
bean定义的继承
一个bean定义会包含很多配置信息, 包括构造方法参数, 属性值 以及容器指定的信息(初始化方法, 静态工厂名称等等). 子bena定义可以从父bean定义继承配置信息. 子定义可以根据自身所需来覆盖一些值, 也可以增加一些配置. 使用父子bean定义可以让配置更加简洁.
如果你的项目使用的是 ApplicationContext
, 子bean定义可以由ChildBeanDefinition
定义. 很多用户并不在这个层面上使用它们, 而是使用了像ClassPathXmlApplicationContext
的方式. 当你使用基于XML的配置, 你需要使用parent
属性来生命当前配置是一个子bean.
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- age属性值为1, 是从父bean继承的 -->
</bean>
如果子bean没有指定bean的class, 那么他就会从父bean继承过来, 当然也是可以覆盖父的class的. 稍后的例子中, 子bean必须要兼容父类的class, 也就是说它必须要接受父bena的属性值.
子bean定义会从父bean继承和覆盖scope, 构造方法参数, 属性值等, 也可以根据自己所需添加新值. 所有scope, 初始化方法, 销毁方法, 或静态工厂方法等其他你在子bean指定的, 会覆盖父bean的设置.
除上面提到的设置外, 其他的设置都会在子bean上定义: 依赖检查, 装配模式, 依赖检查, 单例, 延迟初始化等等
上一个例子中, 通过使用abstract
属性来显式标记父bean定义是抽象的. 如果父定义没有指定一个类, 那么必须要使用abstract
属性来生命这是一个抽象配置, 如下所示:
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
因为父bean是抽象且不完整的, 所以是不可以被实例化的, 当一个bean定义被生命成抽象的, 那么他就是只能是一个模板, 被子bean使用.
ApplicationContext
默认是提前实例化所有的单例. 所以, 如果你只是想让一个bean定义当成模板来使用, 那么就必须要在bean定义上设置抽象属性为True. 否则容器就会去提前实例化(尝试)这个抽象bean.