Spring Framwork 官方文档5.0.8翻译 第一章:The IoC Container 1.5.-1.16

1.5. Bean的作用域

bean 定义就是创建类实例的模板,也就是说通过这个 bean 定义(recipe)可以创建出很多对象。

通过配置 bean 定义不仅可以配置依赖,还可以设置作用域。这种方式非常强大和灵活,因为你可以通过配置设置 bean 的作用域,而不是在 java 类层面设置。Spring Framework 支持六种作用域,其中四个只能在 web-ware Application 中使用(request session application websocket)。当然,你也可以自定义作用域。

下面的表格描述了(Spring Framework)支持的作用域:

Table 3.Bean scopes

Scope Description
singleton 每个容器中只能有一个该 bean 的实例(作用域默认值)
prototype bean 的实例可以有任意多个
request 每个 http request 都会创建该 bean 的实例
session 每个 http session 都会创建该 bean 的实例
application 每个 ServletContext 都会创建该 bean 的实例
websocket 每个 WebSocket 都会创建该 bean 的实例

Spring 3.0 开始,提供了另一个作用域 thread scope ,但是默认没有注册,详情请看 SimpleThreadScope,关于如何注册作用域,请看 Using a Custom Scope.

1.5.1 Singleton Scope

容器中只有一个 single bean 的实例,所有需要这个 single bean 实例的地方,都会返回这个实例。

Spring 概念中的单例和设计模式(GOF)中不同,设计模式中的单例指的是每一个 ClassLoader 中只会创建一个单例类的实例,而 Spring 中的单例指的是每个容器中只会创建一个特定 bean 的实例。

singleton

你可以通过下面的XML定义一个单例 bean

<bean id="accountService" class="com.something.DefaultAccountService" />

<!-- the following is equivalent, though redundant (singleton scope is the default ) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton" />
1.5.2. Prototype Scope

scopeprototypebean 在每次注入到其他 bean 中或者通过 getBean 方法调用时都会创建一个新实例。通常,将有状态的 beanscope 设置为 prototype,将无状态的 beanscope 设置为 singleton

下面的图阐明了 prototype scope

prototype

DAO 对象的 scope 并不适合设置为 prototype ,因为一般 DAO 都是无状态的,这里只是复用的前面的单例图)

下面通过XML 定义了 prototype bean

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype" />

与其他作用域不同的是,Spring 不会管理 prototype bean 的所有 lifecycle 。容器初始化、配置和装载 prototype bean ,然后将其交给客户端,之后不会再记录这个 prototype instance。因此,虽然 initialization lifecycle 会在所有的对象上面调用不管 scope 是什么,但是 destruction lifecycle 不会在 prototype bean 上面调用,清理和释放资源的工作由客户端完成。如果想让容器释放 prototype bean 拥有的资源,可以使用 bean post-processor,这样容器会维护一个 prototype instance 的引用。

另一方面,可以认为容器在 prototype bean 的角色等同于 Java new operator。所有生命周期的管理都由客户端完成。

1.5.3. Singleton Beans 依赖 Prototype-bean

如果 singleton bean 依赖于 prototype bean 时,其依赖关系是在实例化时解析的,也就是说 singleton bean 初始化完成以后就会有一个不变的 prototype bean 引用 ,为了在 singleton bean 上面每次获取新的 prototype bean , 可以通过 Method Injection 实现。

1.5.4. Request,Session,Application,and WebSocket Scopes

request,session,applicationwebsocket 作用域只能在实现了 ApplicationContextweb 环境中使用(比如:XmlWebApplicationContext)。如果在常规的容器中使用,比如 ClassPathXmlApplicationContext,会抛出 IllegalStateException 未知作用域异常。

初始化Web配置
为了支持 request,session,application 以及 webSocketweb 相关的作用域,在定义 bean 之前,需要做一些初始化的配置。

如何完成初始化设置依赖于所使用的 Servlet 环境。

如果是在 Spring Web MVC 中访问有作用域的 bean,在由 Spring DispatcherServlet 处理的请求中,不需要做特殊的设置,DispatcherServlet 已经暴露了所有的相关状态。

如果使用 Servlet 2.5 web 容器,在 Spring DispatcherServlet 之外处理请求(如:JSFStructs),你需要注册 org.springframework.web.context.request.RequestContextListener ServletRequestListener。在 Servlet 3.0 以上,可以通过实现 WebApplicationInitializer 接口完成。或者,对于旧容器,可以在 web.xml 文件中添加如下声明。

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>

或者,如果使用监听器设置有问题,可以考虑使用 RequestContextFilter。过滤器映射依赖于所在的 web 应用配置,可以根据需要修改。下面展示了 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,RequestContextListenerRequestContextFilter 都做了相同的事情,即将请求对象绑定到服务该请求的线程。这使得作用域为 requestsessionbean 可以在调用链的下游使用。

Request Scope
考虑下面的 bean 定义:

<bean id="loginAction" class="com.something.LoginAction" scope="request" />

对于每一个请求,容器都会通过 loginAction bean 定义创建 loginAction 的实例,也就是说,loginAction bean 的作用域是 HTTP request 级别。你可以改变这个实例的内部状态,不会影响其他请求中创建的该实例。当请求完成以后,这些 bean 也会被销毁。

当使用 java annotation 配置时,@RequestScope 注解可以指定一个组件的作用域是 request。下面的实例展示了如何使用:

@RequestScope
@Component
public class LoginAction{
    //....
}

Session Scope
考虑下面的 bean 定义:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session" />

对于每一个 HTTP Session ,容器都会创建该 bean 的 实例,你可以像 request scopebean 一样,修改该实例的内部状态,这些修改对于其他 HTTP Session 是透明的。当 HTTP Session 销毁时,属于该它的作用域为 session 的实例也会被销毁。

当使用 java annoation 配置时,也可以使用 @SessionScope 注解指定一个组件的作用域为 session

@SessionScope
@Component
public class UserPreferences{
    //....
}

Application Scope
考虑下面的 bean 定义:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application " />

容器对整个 web 应用一次性使用 appPreferences bean 定义创建 appPreferences 实例,也就是说 appPreferences bean 的作用域是 ServletContext 级别,并且作为 ServletContext 的常规属性。这有些类似于单例作用域,但是有两个区别,首先,它是在每一个 ServletContext 中是单例的,其次,它是作为 ServletContext 属性公开可见的。

当使用 Java annoation 配置时,也可以使用 @ApplicationScope 注解指定一个组件的作用域是 application

@ApplicationScope
@Component
public class AppPreferences{
    //...
}

Scoped Beans Dependencies
容器不仅管理 bean 的初始化,而且还会装载 bean 的协作者。如果你想把一个(比如)request-scoped bean 注入到一个作用域更长的 bean,你可以选择将 AOP proxy 注入到作用域 bean 中。也就是说,你需要注入一个暴露了和作用域 bean 相同的公共接口的代理对象,但是可以从相关的作用域(比如:HTTP request)中获取真实的目标对象,将方法调用委托到真实的目标对象上。

下面的配置只用一行设置了 AOP proxy ,但是理解为什么以及如何使用特别重要:

<?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">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> 
    </bean>

    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

给作用域 bean 中插入一个子元素 <aop:scoped-proxy /> 来创建代理。为什么 requestsession 以及自定义作用域需要设置代理元素 <aop:scoped-proxy> ?考虑下面的单例 bean 定义来和上面的配置比较。

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

在上面的例子中, singleton bean userManager 引用了一个 session bean userPreferences ,但是 userManager 只会在每个容器中初始化一次,也就是它的依赖(userPreferences)只会注入一次。

这应该不是你想要的,你需要的是在每个 HTTP Session 中,userManager 都引用属于这个 SessionuserPreferences,所以需要代理,对于 userManager 来说,别不知道它代用的是代理还是真实的目标对象,代理也会将方法调用委托到真实的目标对象上面。

因此,当需要将一个 request 或者 session-scoped bean 注入到其他协作者中时,需要做如下的配置:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

选择创建代理方式
实现动态代理有两种方式:CGLIB 基于继承类的代理,JDK 基于接口的代理,CGLIB 只能代理公有方法且不能是 finalfinal 方法不能被重写),JDK 只能代理实现了接口的类。

默认,<aop:scoped-proxy /> 使用的是 CGLIB 代理。
如果想使用 JDK 代理,可以使用下面的配置,将元素 <aop:scope-proxy /> 的属性 proxy-target-class 设置为 false

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

如果想了解更多关于基于类代理和基于接口代理,请查看 Proxying Mechanisms.

1.5.5. 自定义作用域

bean 的作用域机制是可扩展的,你也定义自己的作用域,也可以重新定义已经存在的作用域(这不是一种好的方式)。

创建自定义作用域
为了将自定义作用域整合到容器中,需要实现接口 org.springframework.beans.factory.config.Scope
Scope 接口有四个方法需要实现:

Object get(String name, ObjectFactory objectFactory)

Object remove(String name)

void registerDestructionCallback(String name, Runnable destructionCallback)

String getConversationId()

使用自定义作用域
定义了作用域以后,如果希望容器知道新的作用域,需要将其注册到容器中:

void registerScope(String scopeName, Scope scope);

这个方法是在 ConfigurableBeanFactory 接口中定义的,可以通过实现了 ApplicationContext 接口的实例上面的 BeanFatory 属性获取。

容器中还有一个作用域是 SimpleThreadScope,但是默认没有注册。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("scope",threadScope);

现在就可以使用这个新注册的作用域了。

<bean id="..." class="..." scope="thread" >

也可以使用 CustomScopeConfigurer 类通过配置声明一个新的作用域,而不是编程的方式。

<?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">

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>
1.6. 自定义Bean 的特性

Spring Framework 提供了一些列接口来自定义 bean 的特性

1.6.1. Lifecycle Callbacks

为了和 bean 的生命周期交互,需要实现 InitializingBeanDisposableBean 接口,容器会首先调用 afterPropertiesSet() ,然后调用 destory() 来执行 bean 的初始化和销毁操作。

JSR-250 中的 @PostConstruct@PreDestroy 注解也可以实现相同的功能,更详细的信息可以查看:Using @PostConstruct and @PreDestroy.
如果不想使用这两个注解,还想与 Spring Framework 的接口解耦,也可以使用 init-methoddestroy-method 定义 bean

在内部,Spring Framework 使用 BeanPostProcessor 接口的实现处理所有它能发现的回调接口,调用合适的方法。如果你需要自定义或者使用 Spring 不支持的生命周期行为,你也可以实现 BeanPostProcessor 接口,更多信息请看 Container Extension Points.

除了初始化和销毁回调之外,Spring 管理的对象也可以实现 Lifecycle 接口,使对象参与容器的启动和关闭的过程。

Initialization Callbacks
容器设置了 bean 所需属性之后,接口 org.springframework.beans.factory.InitializingBean 执行初始化操作。接口 InitializingBean 接口只有一个方法。

void afterPropertiesSet() throws Exception;

我们推荐使用 @PostConstruct 注解或者 <bean> 元素的属性 init-method (或 @Bean 的属性 initMethod) 来定义初始化,因为使用接口 InitizlizingBean 会与 Spring Framework 有耦合。更多信息请看 Receiving Lifecycle Callbacks。请看下面的例子:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init" />
public class ExampleBean{
    public void init(){
        // do some initialization work
    }
}

上面的例子和下面做了相同的事情

<bean id="exampleInitBean" class="examples.AnotherExampleBean" />
public class AnotherExampleBean implements InitializingBean{
    public void afterPropertiesSet(){
        // do some initialization work
    }
}

Destruction Callbacks
如果 bean 实现了接口 org.springframework.beans.factory.DisposableBean ,当容器销毁时,这个 bean 或获得一个回调。DisposableBean 接口只有一个方法:

void destroy() throws Exception;

推荐使用 @PreDestroy 注解或者 <bean /> 元素的 destroy-method 属性(或 @Bean 注解的 destroyMethod 属性)。

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

上面的例子和下面的例子实现了相同的功能:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {

    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

然而,第一个定义并没有与 Spring 代码耦合。

Default Initialization and Destroy Methods
如果 bean 不实现 InitializingBeanDisposableBean 回调接口,一般我们会使用这些名称表示初始化和销毁的方法名称, init(),initialize(),dispose() 等等。在整个项目中应该保持这些名称一致。

也可以配置 <beans /> 元素的 default-init-methoddefault-destroy-method 属性,来指定所有 bean 的默认初始化和销毁方法名称。如果个别 bean 的初始化和销毁方法不一致,可以在 <bean /> 元素上面指定,会覆盖 <beans /> 中的设定。

假设初始化回调叫 init(),销毁回调叫 destroy(),你的类应该和下面的类类似。

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }

    // this is (unsurprisingly) the initialization callback method
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }
}
<beans default-init-method="init">

    <bean id="blogService" class="com.foo.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>

bean 被创建和装载以后,如果它有这两个方法,方法将会在合适的时机调用。

容器能保证初始化回调方法将在 bean 的所有依赖提供以后调用。因此,初始化回调是在真实 bean 的引用上面调用,也是就是 AOP interceptors 之类还没有应用到 bean 上。一个目标 bean 第一次完全创建以后,AOP proxy的拦截器链将被应用。如果目标 bean 和 代理对象独立定义,我们可以绕过代理直接访问目标 bean 。因此,将拦截器应用于初始化方法将会导致不一致,因为这样会将目标 bean 的生命周期和它的代理或拦截器耦合在一起,直接访问目标 bean 会有点奇怪。

Combining lifecycle mechanisms
Spring 2.5 之后,你有三种方式控制 bean 的生命周期行为:实现InitializingBeanDisposableBean 回调接口,自定义 init()destroy() 方法以及使用 @PostConstruct@PreDestroy 注解,你可以组合使用这三种方法。

如果有多种方式指定了 bean 的生命周期行为,并且每种配置的初始化或销毁的方法名称都不一样,则配置方法将会按下面的顺序都会执行,如果配置的方法名称有相同的,相同的方法只会执行一次。

同一个 bean 应用多种生命周期机制配置,使用不同的初始化方法:

  • @PostConstruct
  • InitializingBean 接口定义的 afterPropertiesSet() 方法
  • 自定义的 init() 方法

销毁方法调用顺序一样:

  • @PreDestroy
  • DisposableBean 接口定义的 destroy() 方法
  • 自定义的 destroy() 方法

Startup and shutdown callbacks
Lifecycle 接口给任意有它自己生命周期需求的对象定义了基本的方法(比如:启动和停止一些后台进程):

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何 Spring 管理的对象都可以实现这个接口。当 ApplicationContext 收到启动和停止的信号时(比如:在运行时 stop/restart 场景),它就会委托 LifecycleProcessor 广播这些信号到所有实现了 Lifecycle 接口的 bean

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessor 也继承了 Lifecycle 接口,并且添加了两个方法。

请注意 org.springframework.context.Lifecycle 接口只是表示接受 start/stop(也就是在 context 上面调用这两个方法)信号的契约,不适用于 context refresh 时间的 auto-startup 。可以使用 org.springframework.context.SmartLifecycle 接口更细粒度控制特定 beanauto-startup 。另外,请注意容器不能保证停止信号在销毁对象之前收到,多有实现了接口 Lifecyclebean 将会在销毁对象的回调传播之前第一时间收到停止信号,但是,在 contexthot refresh 或者 aborted refresh 期间,只有销毁回调会被调用(在 context 上面首先调用 start() ,然后调用 refresh() 方法,收不到停止信号,只调用了销毁回调方法,如果不调用 refresh(),停止信号也会收到)。

启动的停止的调用顺序有时候也很重要。如果任意两个对象之间有依赖关系,依赖对象应该在被依赖对象之后启动,之前停止。然后,有时候依赖关系并不直接,可能我们只知道一种类型的对象优先于另一种类型的对象启动。在这种情况下,SmartLifecycle 接口提供了一种选择,它的父接口中有一个 getPhase() 方法,这个方法的返回值表示对象的优先级。

public interface Phased{

    int getPhase();
}
public interface SmartLifecycle extends Lifecycle,Phased{

    boolean isAutoStartup();

    void stop(Runnable callback);
}

当启动的时候, phase 值最小的对象首先启动,当停止的时候,正好相反。因此,如果一个对象实现了 SmartLifecycle 接口,并且方法 getPhase() 返回了 Integer.MIN_VALUE ,那么这个对象是第一个启动,最后一个停止的。相反,如果一个对象实现了 SmartLifecycle 接口,并且方法 getPhase() 返回了 Integer.MAX_VALUE ,则表示这个对象是最后一个启动,第一个停止的。还有一点很重要,如果一个对象实现了 Lifecycle 接口而不是 SmartLifecycle 接口,则 phase 的值为 0

SmartLifecycle 接口的 stop(Runnable callback) 方法必须调用 callbackrun() 方法,否则 DefaultLifecycleProcessor 接口认为停止没有完成,直到超时才会结束。也可以定义一个名叫 lifecycleProcessorbean 来覆盖这个默认的生命周期处理器,。

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

当调用 contextstart 方法时,如果对象实现了 Lifecycle 接口,并且 isRunning =false , 则对象的 start() 方法被调用,当调用 stop 方法,只有 isRunning=true ,对象的 stop() 方法才会被调用。如果对象实现了 SmartLifecycle 接口,context refresh(创建 context 时,会刷新)时,并且 isAutoStartup = true && isRunning=false 时,对象的 start() 方法被调用。

Shutting down the Spring IoC container gracefully in non-web applications

在基于 webApplicationContext 实现中,已有相应的实现来处理关闭 web 应用时恰当地关闭容器。 但是,如果你正在一个 no-web应用的环境下使用 Spring IoC 容器,如 dubbo 服务,你想让容器优雅的关闭,并调用 singletonbean 相应 destory 回调方法,你需要在 JVM 里注册一个“关闭钩子”(shutdown hook)。这一点非常容易做到,并且将会确保你的容器被恰当关闭,以及所有由单例持有的资源都会被释放。

为了注册“关闭钩子”,你只需要简单地调用在org.springframework.context.support.AbstractApplicationContext 实现中的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");

        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...
    }
}
1.6.2. ApplicationContextAware and BeanNameAware

ApplicationContext 创建一个实现了 org.springframework.context.ApplicationContextAware 接口的对象时,这个对象将会获得一个 ApplicationContext 的引用。

public interface ApplicationContextAware{

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此, bean 可以调用创建它的 ApplicationContext 对象上面的一些方法,也就可以转化为它的子类,调用子类上面的方法。比如:访问文件资源、发布事件以及访问 MessageSource 等等。一般不推荐这样做,因为违反的 Ioc 原则。

Spring 2.5 以后,我们也可以通过注解 @Autowire 获取创建对象的 ApplicationContext引用。

   @Autowired
    private ApplicationContext context;

ApplicationContext 创建一个实现了 org.springframework.beans.factory.BeanNameAware 接口的对象时,这个对象可以获得它自己的 bean 名称

public interface BeanNameAware{

    void setBeanName(String name) throws BeansException;
}

这些方法是在 bean 属性设置之前,初始化回调(@PostConstruct,InitializingBeaninitMethod)之后调用。

1.6.3. Other Aware interfaces

除了上面讨论的 ApplicationContextAwareBeanNameAware 接口之外,Spring 还提供了一系列的 Aware 接口,使得 bean 可以获取它需要的基础依赖。下面列举了一些重要的 Aware 接口,一般接口的名称表明了基础依赖的类型。

Table4. Aware interfaces

Name Injected Dependency Explained in...
ApplicationContextAware Declaring ApplicationContext ApplicationContextAware and BeanNameAware
ApplicationEventPublisherAware Event publisher of the enclosing ApplicationContext Additional capabilities of the ApplicationContext
BeanClassLoaderAware Class loader used to load the bean classes. Instantiating beans
BeanFactoryAware Declaring BeanFactory ApplicationContextAware and BeanNameAware
BeanNameAware Name of the declaring bean ApplicationContextAware and BeanNameAware
BootstrapContextAware Resource adapter BootstrapContext the container runs in. Typically available only in JCA aware ApplicationContexts JCA CCI
LoadTimeWeaverAware Defined weaver for processing class definition at load time Load-time weaving with AspectJ in the Spring Framework
MessageSourceAware Configured strategy for resolving messages (with support for parametrization and internationalization) Additional capabilities of the ApplicationContext
NotificationPublisherAware Spring JMX notification publisher Notifications
ResourceLoaderAware Configured loader for low-level access to resources Resources
ServletConfigAware Current ServletConfig the container runs in. Valid only in a web-aware Spring ApplicationContext Spring MVC
ServletContextAware Current ServletContext the container runs in. Valid only in a web-aware Spring ApplicationContext Spring MVC
1.7. Bean definition inheritance

bean 定义包含许多配置信息,包括构造函数参数,属性值,以及初始化方法,静态工厂方法等等,child bean 定义可以继承 parent bean 定义。child bean 可以覆盖 parent bean 定义中的配置,也可新增配置。

child bean 定义是用 ChildBeanDefinition 类表示的。

请看下面配置:

<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"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>

inheritsWithDifferentClass 通过属性 parent 来引用 inheritedTestBean
inheritedTestBean 中,如果设置 abstracttrue,则这个 bean 只能作为创建其他 bean 的模板,不能作为其他 bean 的依赖,也不能通过 getBean() 方法获取(容器内部的方法 preInstantiateSingletons() 会忽略属性 abstract=truebean 定义),如果 class 属性没有指定,那么必须将 bean 的属性 abstract 设置为 true,并且 child bean 对应的类必须有这些属性。

<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>
1.8 Container Extension Points

一般,应用开发人员并不需要实现 ApplicationContext 来扩展容器,而是通过插入特定集成接口的实现来扩展。下面几节内容将会介绍这些集成接口。

1.8.1. Customizing Beans by Using a BeanPostProcessor

BeanPostProcessor 接口有两个回调方法,你可以通过实现它们来自定义初始化逻辑,依赖解析逻辑等等。如果你想在容器实例化、配置以及初始化一个 bean 之后,给 bean 实例自定义一些逻辑,你可以插入一个或多个 BeanPostProcessor 的实现。

你可以配置多个 BeanPostProcessor 实例,设置 order 属性来控制它们的执行顺序。只有 BeanPostProcessor 实例实现了 Ordered 接口才可以设置 order 属性。如果你想创建自己的 BeanPostProcessor ,你最好也实现一下 Ordered 接口。更详细的信息请看 BeanPostProcessorOrdered 接口文档,也可以查看 programmatic registration of BeanPostProcessor instances.

BeanPostProcessor 操作对象是 bean 实例。也就是说,容器实例化一个 bean 之后, BeanPostProcessor 在这些实例上面执行其他操作。
BeanPostProcessor 的作用域是每个容器。如果你使用了容器继承,应该关注这一点。如果在一个容器中定义了一个 BeanPostProcessor ,它不会对其他容器中定义的 bean 执行后置处理,即便是在同一个继承体系中的容器。
如果想改变 bean 定义而不是 bean 实例,可以通过 BeanFactoryPostProcessor 实现,将会在下一节中介绍。

org.springframework.beans.factory.config.BeanPostProcessor 接口有两个回调方法,当这样的一个类注册为容器的后置处理器时,容器创建的 bean 实例在初始化前后都可以调用后置处理器。后置处理器可以对 bean 实例做任何操作,包括完全忽略回调。一个后置处理器典型的应用是检查回调接口或者创建 bean 的代理。Spring AOP 的一些基础类就是通过后置处理器实现的。

ApplicationContext 会自动检测实现了 BeanPostProcessor 接口的 bean ,将这些 bean 注册为后置处理器,这些后置处理器的 bean 定义和普通的 bean 定义没有什么区别。

如果 BeanPostProcessor bean 定义是通过配置类中的 @Bean 标记的工厂方法定义,则要求方法的返回值实现 org.springframework.beans.factory..config.BeanPostProcess 接口,否则 ApplicationContext 将不会检测到它是一个后置处理器。为了将后置处理器应用到容器中其他的 beanBeanPostProcessor 需要提前初始化,所以提前类型检测是关键。

编程的方式注册后置处理器
推荐使用 ApplicationContext 自动检测的方式注册后置处理器,但是可以通过 ConfigurableBeanFactoryaddBeanPostProcessor 方法注册后置处理器。如果注册后置处理器有条件,则编程的方式注册后置处理器就很有用。这些通过编程的方式注册的后置处理器不需要实现 Ordered 接口,它们是根据注册的顺序来执行的。而且这种方法注册的后置处理器比自动检测到的后置处理器先执行。

后置处理器和 AOP 自动代理
容器会特殊对待实现 BeanPostProcessor 接口的类,它们引用的其他 BeanPostProcessorbean 将会在 ApplicationContext 启动阶段实例化。因为 AOP 自动代理也是通过 BeanPostProcessor 实现的,因此 BeanPostProcessor 以及它直接引用的其他 Bean 都不适合自动代理,它们没有织入的概念。
对于这种 bean ,你将会看到这个日志信息:"Bean foo is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)"
这一段不明白:"Note that if you have beans wired into your BeanPostProcessor using autowiring or @Resource (which may fall back to autowiring), Spring might access unexpected beans when searching for type-matching dependency candidates, and therefore make them ineligible for auto-proxying or other kinds of bean post-processing. For example, if you have a dependency annotated with @Resource where the field/setter name does not directly correspond to the declared name of a bean and no name attribute is used, then Spring will access other beans for matching them by type."

下面的例子展示了在 ApplicationContext 中如何编写、注册后置处理器。

Example:Hello World,BeanPostProcessor-style
下面的例子定义了一个调用容器中每个 beantoString() 方法的 BeanPostProcessor 实现。

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}
<?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:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang
        http://www.springframework.org/schema/lang/spring-lang.xsd">

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!--
    when the above bean (messenger) is instantiated, this custom
    BeanPostProcessor implementation will output the fact to the system console
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

上面的配置中只是定义了 InstantiationTracingBeanPostProcessor ,并没有做额外的处理,甚至都没有设置 name ,这个 bean 也可以注入或依赖其他 bean

下面的的应用运行上面的代码和配置

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
        Messenger messenger = (Messenger) ctx.getBean("messenger");
        System.out.println(messenger);
    }

}

输出结果类似于:

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961

Example:The RequiredAnnotationBeanPostProcessor
使用回调接口或者注解和 BeanPostProcessor 结合使用是容器扩展的常用手段。比如:RequiredAnnotationBeanPostProcessor@Required 结合,可以确保依赖必须注入。

1.8.2. Customizing Configuration Metadata with a BeanFactoryPostProcessor

下一个我们要介绍的扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor 。这个接口的语义和 BeanPostProcessor 类似,主要的不同点是,BeanFactoryPostProcessor 的操作对象是配置元数据。因此,BeanFactoryPostProcessor 可以在容器实例化 bean 之前,除了它自己的配置元数据之外可以修改其他任何 bean 的配置元数据。

你可以配置多个 BeanFactoryPostProcessor,通过 order 属性来控制它们的执行顺序。只有实现了 Ordered 接口的 BeanFactoryPostProcessor 才有 order 属性。如果你需要自定义 BeanFactoryPostProcessor ,你也应该实现 Ordered 接口。更详细的信息可以查看 BeanFactoryPostProcessor and Ordered 的文档。

如果你想改变的是 bean 的实例,而不是 bean 的配置元数据,应该使用 BeanPostProcessor,当然,从技术来说 BeanFactoryPostProcessor 也可以通过 BeanFactory.getBean() 修改 bean 的实例,但是这样做会使 bean 提前实例化,违背了标准的容器生命周期,也可能会带来其他的负面影响,比如:绕过了后置处理器。
BeanFactoryPostProcessor 的作用域和 BeanPostProcessor 类似。

为了能修改容器中定义的配置元数据, 当在 ApplicationContext 声明了 bean factory 的后置处理器,它们会自动执行。Spring 中包含了许多预定义的 bean factory 后置处理器,比如:PropertyOverrideConfigurerPropertyPlaceholderConfigurer。你可以可以自定义 BeanFactoryPostProcessor ,比如注册一个自定义的属性编辑器。

ApplicationContext 会自动检测实现了 BeanFactoryPostProcessor 接口的 bean ,将它们作为 bean factory 的后置处理器,你可以像定义其他 bean 一样定义它们。

对于 Bean(Factory)PostProcessor,你并不想把它们配置成懒初始化。如果没有其他 bean 引用它们时,这些后置处理器将永远不会被初始化。因此,它们的 lazy 属性将会被忽略,并且会提前实例化即便是将 <beans /> 元素的 default-lazy-init 属性设置为 true

Example: The Class Name Subsitution PropertyPlaceholderConfigurer
在单独的 Java Properties 格式的文件中定义属性值来,通过 PropertyPlaceholderConfigurer 来扩展 bean 的属性值。这样可以通过修改配置文件而不是 xml 文件来切换不同环境中特定的属性值。

下面的配置片段中定义了一个 DataSourcebean ,它的属性是通过占位符定义。在运行时, PropertyPlaceholderConfigurer 将配置元数据中格式为 ${property-name} 的占位符替换为外部 Properties 文件中对应的属性值。

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

真实的值是通过 Java Properties 格式的外部配置文件中获取:

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

因此,${jdbc.username} 在运行时被替换为 sa ,其他的属性替换方式一样。也可以给占位符自定义前缀和后缀。

Spring 2.5 开始引入了 context 命名空间,可以通过声明一个配置元素来配置属性占位符。可以给 location 属性指定多个路径使用逗号分隔。

<context:property-placeholder location="classpath:com/foo/jdbc.properties" />

PropertyPlaceholderConfigurer 不仅仅在你配置的属性文件中查找属性,默认情况下,如果在属性文件中没有找到对应的属性,也会在 java System Properties 中查找。你也可以将 systemPropertiesMode 设置为下面三个整形值中的一个,来自定义这个行为。

  • nerver(0) :从不去 system properties 查找属性
  • fallback(1) : 首先从指定的属性文件中查找,如果没有找到再去系统属性中查找,这是默认配置。
  • override(2) : 首先去系统属性中查找,如果没有找到,再去指定的属性文件中查找。

查阅 PropertyPlaceholderConfigurer 文档查看更多信息。

你可以通过 PropertyPlaceholderConfigureer 来替换类名称,当在运行时指定一个特定的类。比如:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="locations">
      <value>classpath:com/foo/strategy.properties</value>
  </property>
  <property name ="properties">
      <value>custom.strategy.class=com.foo.DefaultStrategy</value>
  </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>

ApplicationContextpreInstantiateSingletons() ,对于非懒实例化 bean,如果指定的属性在运行时没有解析到有效的类,将会在创建时失败。

Example: the PropertyOverrideConfigurer
PropertyOverrideConfigurer 是另一个 bean factory 后置处理器,类似于 PropertyPlaceholderConfigurer ,但是不同于后者,原始的 bean 属性可以设置默认值,也可以不设置,如果覆盖属性文件中没有找到对应的属性值,则使用默认值。

请注意 bean 定义并不知道覆盖的存在,所以不能马上知道 XML 定义中使用的是那个覆盖配置。如果有多个 PropertyOverrideConfigurer 实例对同一属性定义了不同的值,根据覆盖机制,最后一个值将会被使用。

属性文件格式如下:

beanName.property =value

比如:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

上面的配置文件可以应用到有一个 dataSource bean ,并且该 bean 有两个属性 driverurl 的容器中。

复合属性名称也是支持的。比如

foo.fred.bob.sammy=123

foo 对象的 fred 属性的 bod属性的 sammy 属性被设置为 123

指定的覆盖值只会是字面量,不会被转化为 bean 的引用,这个约定也适用于原始的 bean 定义中的属性值是引用。

自从 Spring 2.5 引入了 context 命名空间,也可以通过下面的声明配置元素来配置属性覆盖值:

<context:property-override location="classpath:override.properties" />
1.8.3. Customizing instantiation logic with a FactoryBean

实现接口 org.springframework.beans.factory.FactoryBean ,可以让 bean 变为工厂。

对于比较复杂的初始化逻辑,使用 XML 来配置可能比较麻烦,可以实现自己的 FactoryBean 来设置初始化逻辑。

FactoryBean 接口有三个方法:

  • Object getObject() :返回工厂创建的实例,这个实例可以共享,具体取决于作用域。
  • boolean isSingleton() : 如果返回的实例是单例返回 true ,否则返回 false
  • Class getObjectType() : 返回 getObject() 方法返回的实例的类型,如果不能预先知道可以返回 null

FactoryBean 概念和接口在 Spring Framework 中很多地方在使用,Spring 自身实现了超过50 FactoryBean 接口。

如果想获取工厂创建的对象,通过 getBean("myBean") ,如果想获取工厂本身而不是工厂生成的实例,需要给 bean 名称前面添加一个 &getBean("&myBean")

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

推荐阅读更多精彩内容