Spring Framework 官方文档中文版—Core_part_2

内容过长,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实例。

从始至终只创建一个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不具有任何会话状态;对作者来说只是简单的重复使用而已,就像上一节图表中展示的那样。

每次创建一个新的bean实例

下面的例子是如何在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配置

为了支持requestsessionapplicationwebsocket等域,在你进行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应用而创建appPreferencesbean实例,并且只创建一次。也就是说,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的InitializingBeanDisposableBean两个接口. 容器在初始化时调用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.AutoCloseablejava.io.Closeable接口的累也会因此而匹配上). 这个值也可以设置再default-destroy-method中,下一节内容会详解介绍.

默认的初始化和销毁方法
当你编写初始化和销毁回调时, 可以不使用使用Spring指定的InitializingBeanDisposableBean接口. 通常你应该写类似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-methoddestroy-method, 这样就可以覆盖默认值了.

多种方式的结合使用
在Spring2.5 中, 你有三种方式来控制bean的生命周期, 他们分别是: 实现 InitializingBeanDisposableBean 回调接口; 自定义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();
}

需要注意的是, LifecycleProcessorLifecycle的子接口. 它增加了两个方法来支持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。这样通过调用单例上的销毁方法来实现优雅的关闭,可以释放掉相关的资源。当然,你必须做正确的配置和实现。

想要注册一个停止钩子,你需要调用ConfigurableApplicationContextregisterShutdownHook()方法:

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接口

除了上面提到的ApplicationContextAwareBeanNameAware以外, 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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容