目录
Request, Session, Application, 和 WebSocket作用域
ApplicationContextAware和BeanNameAware
Bean Scops(Bean作用域)
当你创建bean定义时,你就是通过该定义说明了创建bean的方式,因此对于一个类而言,你可以通过该bean定义创建不同的实例对象。通过bean定义可以创建对象并设置对象的依赖,同时也可以声明bean的作用域,这种方式强大且灵活,不需要通过硬编码方式对bean定义创建的对象的作用域进行管理。
目前支持的作用域如下表所示:
Scope | Description |
---|---|
(Default) Scopes a single bean definition to a single object instance for each Spring IoC container. | |
Scopes a single bean definition to any number of object instances. | |
Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring | |
Scopes a single bean definition to the lifecycle of an HTTP | |
Scopes a single bean definition to the lifecycle of a | |
Scopes a single bean definition to the lifecycle of a |
对于Spring 3.0线程作用域也是支持的,只是没有默认注册。
Singleton
当定义一个bean为singleton时,Spring容器仅会创建该bean的一个对象实例,该对象会被缓存起来,所有后续的请求均会返回该缓存对象。
Spring中的单例概念与GoF中的单例是有区别的,在GoF中通过硬编码形式保证对于一个类加载器某个类只有一个实例存在,而在Spring中是针对每个容器而言的。通过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"/>
Prototype
对于非单例模式的bean,会在每次请求时返回一个新的该bean的实例。一般来说,对于无状态的bean采取单例模式,而对于有状态的对象采取非单例模式。
如下为配置prototype作用域的示例
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域不同,Spring容器并不负责维护prototype作用域的bean的全部生命周期,容器在初始化、配置prototype作用域的bean对象实例后就把把它交由客户端,容器并不保留关于该bean实例的信息。需要注意的是,尽管对于所有作用域的bean来说,其初始化时的回调都会被正常调用,但是对于prototype而言,在销毁时的回调并不能被调用执行。因此,客户端代码必须负责释放prototype作用域对象持有的资源。
如果想让spring负责释放prototype作用域的bean持有的资源,可以考虑用自定义的bean后后处理器。从某种意义上来说,对于prototype作用域的对象,spring容器的角色就是Java中的new操作,过了这个点,就需要由客户端负责。
Singleton依赖Prototype
当一个singleton作用域的对象依赖prototype作用域的对象时,需要考虑到对象之间的依赖是在初始化时进行注入的。当把一个prototype作用域的bean注入到singleton作用域的bean时,一个新的prototype作用域的bean对象会被创建并注入到该singleton作用域bean中,最终提供给该singleton作用域bean的对象对依赖是同一个对象实例。
如果想要在singleton作用域对象中,每次都能获取到一个新的prototype对象实例,你不能使用依赖注入,由于该注入只会在容器初始化singleton作用域bean对象时发生一次,可以采用方法注入方式完成这一目的。
Request, Session, Application, 和 WebSocket作用域
这几种作用域仅在针对web应用的ApplicationContext实现中才是可用的,如XmlWebApplicationContext。如果在通常的Spring IoC容器中使用这几种作用域,将会抛出IllegalStateException异常。
初始化web配置
为了支持request
, session
, application
, 和websocket作用域,需要先做一些基础配置,如何进行配置取决于特定的servlet环境
如果你使用了
Spring Web MVC,实际上所有的请求都会经由Spring的DispatcherServlet,DispatcherServlet已经暴露所有相关的作用域,因此并不需要进行额外的配置。
如果不使用Spring Web MVC,对于Servlet 2.5版本的web容器,需要注册org.springframework.web.context.request.RequestContextListener (这是一个ServletRequestListener);对于Servlet 3.0+,可以利用WebApplicationInitializer接口以编程的方式实现。
如下为在web.xml文件中注册RequestContextListener的方式
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果在配置监听器时遇到问题,可以考虑使用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和session作用域的对象都是可以获取的。
Request
考虑如下XML配置:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring容器会为每个HTTP请求创建一个loginAction定义的LoginAction对象实例,也就是说该对象是request级别的对象。你可以修改这个对象的状态,其他的对象实例并不受影响。当请求完成后,该对象会被丢弃掉。
当利用注解或Java配置方式时,可以通过@RequestScope注解声明一个request作用域的bean,如下:
@RequestScope
@Component
public class LoginAction {
// ...
}
Session
考虑如下XML配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring容器会为每一个Http session创建一个UserPreferences对象,也就是说该对象是session级别的对象,当Http session被销毁时,该对象也会被丢弃掉。
当通过注解或Java配置方式时,可以通过@SessionScope注解声明一个session作用域的bean,如下:
@SessionScope
@Component
public class UserPreferences {
// ...
}
Application
考虑如下XML配置:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
对于整个web应用,Spring仅会创建一个AppPreferences对象。也就是该对象是ServletContext级别的对象并作为ServletContext属性进行存储。其与spring的singleton作用域的bean是相似的,主要有两点区别:Application作用域是针对每一个web应用,而不是针对每一个Spring的ApplicationContext(对于一个web应用可能有多个);Application作用域的对象是作为ServletContext的一个属性被暴露出来。
当通过注解或Java配置方式时,可以通过@ApplicationScope注解声明一个application作用域的bean,如下:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
特定作用域bean的依赖问题
Spring容器不仅负责初始化对象,而且负责组装对象之间的依赖,如果想要把一个request作用域的bean到一个更长生命周期的作用域的bean时,可能需要注入一个AOP代理取而代之。也就是说你需要注入一个具有相同接口的代理,可以从该代理获取相应作用域(此处为request)下被代理对象的实例,并委托被代理对象中的方法。
<?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/>子元素,为何request、session和自定义的作用域的bean需要该子元素呢?考虑如下单例bean定义,并与之前提到的作用域的定义做对比
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在上一个例子中,单例的userManager注入了session作用域的userPreferences,这里的重点是:userManger是单例的,只会被初始化一次,因而其依赖userPreferences也只会被注入一次。这意味着userManager只会作用在一个userPreferences实例上。
当在一个长作用域的bean注入一个短作用域的bean时,以上的行为并不是想要的。我们想要的是,对于单一的userManager实例,在HTTP session生命周期内,需要特定于该session的一个userPreferences实例。因此,容器创建了一个与UserPreferences类具有相同公开接口的对象,并从相应的作用域内获取实例对象。容器把该代理对象注入到
userManager中,在该示例中,当userManager调用依赖的UserPreferences对象的方法时,其实调用的是对应的代理的方法,代理对象会从对应的HTTP session中获取到对应的UserPreferences对象,并委托该对象方法。
因此,当注入request、session作用域的对象时,需要使用如下配置
<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>
- 代理类型
默认情况下,包含<aop:scoped-proxy/>元素的bean会通过CGLIB方式生成对应的代理对象。
CGLIB代理只会拦截public方法,调用非public方法并不会委托给代理对象。
你也可以通过在<aop:scoped-proxy/>元素中加入proxy-target-class属性并设置为false,进而采用动态代理方式创建代理对象。利用JDK基于接口的代理,意味着被代理对象至少要实现一个接口,注入时也要使用对应接口的方式进行注入。
<!-- 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>
自定义作用域
bean作用域是可扩展的,你可以自定义作用域或修改已经存在的作用域,不过修改现有作用域的做法是不提倡的。
创建自定义作用域
如果要把自定义作用域集成到容器中,需要实现org.springframework.beans.factory.config.Scope,可以参考相关的javadoc查看具体需要实现的方法。
Scope接口定义了4中从作用域中获取对象的方法,并定义了从作用域中移除对象的方法。例如,对于session作用域的实现,会获取对应的session作用域的对象(如果不存在,则会新建一个对象,并绑定到session),如下方法返回对应作用域的对象
Object get(String name, ObjectFactory objectFactory)
如下方法为从对应作用域移除对象,要返回移除的对象,如果对应名称的对象不存在也可以返回null
Object remove(String name)
如下方法为对象销毁时的回调方法
void registerDestructionCallback(String name, Runnable destructionCallback)
如下方法为获取对应作用域的会话标识,不同作用域的会话标识是不同的,如对于session作用域的实现,该会话标识也就是对应session的id
String getConversationId()
使用自定义作用域
当创建了自定义作用域,需要让spring容器能够感知到新的作用域,其核心方法为
void registerScope(String scopeName, Scope scope);
该方法是在ConfigurableBeanFactory接口中声明的,其可以通过大多数ApplicationContext具体实现的BeanFactory属性获取到。
registerScope方法的第一个参数是作用域的唯一名称标识,该方法的第二个参数是要注册的自定义作用域的具体实现。
接下来的例子中使用SimpleThreadScope说明该注册过程,该作用域在spring中是有的,但是并没有注册,其注册方式如下
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
并不局限于采用编程式进行作用域的注册,也可以采用声明式注册方式
<?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>
定制bean特性
Spring容器提供了一些接口可以对bean的特性进行定制,大致可以包括如下三个方面:
- 生命周期回调
- ApplicationContextAware和BeanNameAware
- 其它Aware接口
生命周期回调
为了与容器对bean生命周期的管理进行交互,可以实现Spring中的InitializingBean和DisposableBean接口,容器会在对应的bean实例创建和销毁时分别调用对应的
afterPropertiesSet()和destroy()方法
使用 JSR-250中的
@PostConstruct
和@PreDestroy注解是一种更好的方式,其可以做到与spring的解耦,如果不想使用JSR-250,也可以在bean定义时采用init-method
和destroy-method进行解耦
Spring容器内部会采用BeanPostProcessor的实现处理其可以找到的回调接口并调用对应的方法,如果你想定义一下spring默认并没有提供的特性或其它的声明周期的方法,可以考虑实现
BeanPostProcessor接口。
除了初始化和销毁时的回调,spring管理的对象也可以通过实现Lifecycle接口。
初始化回调
org.springframework.beans.factory.InitializingBean接口可以在容器对bean设置了所有必需属性后调用该初始化方法,InitializingBean仅指定一个方法:
void afterPropertiesSet() throws Exception;
并不建议使用InitializingBean接口,其会与spring强耦合。最好是使用@PostConstruct注解或指定一个初始化方法,对于XML方式配置时可以使用init-method属性来指定初始化方法。对于Java配置方式,可以通过@Bean的initMethod属性指定。
<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
}
}
销毁时回调
通过实现org.springframework.beans.factory.DisposableBean接口,可以在容器销毁时调用该bean中的回调方法,DisposableBean接口仅包含如下方法:
void destroy() throws Exception;
与初始化相同,也并不建议使用该接口,而是建议使用@PreDestroy注解或在bean定义时指定一个方法。使用XML配置时可以采用destroy-method属性,使用Java配置时可以采用@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)
}
}
默认初始化和销毁方法
如果不使用InitializingBean和DisposableBean来实现初始化和销毁时的回调,通常会采用如init()
, initialize()
, dispose()这样的方法,理想情况下,对于一个应用而言,开发人员应该使用相同的名称,在整个工程中保持一致。
可以对容器配置针对每个bean初始化和销毁时需要调用的默认方法,也就是说不需要针对每个bean都通过init-method属性进行指定。假如,初始化方法设置为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.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>
<beans/>元素的default-init-method为spring容器指定了init作为bean初始化时的回调方法,当bean实例创建时,如果有该方法,则会在适当的时机被调用。可以通过default-destroy-method属性来指定默认的销毁时回调。
如果一个已经存在的类中已经声明了一个与约定的名称不同的方法名,可以通过<bean/>元素的init-method和destroy-method对默认方法进行覆盖。
Spring容器可以确保在bean实例的依赖注入之后会立刻调用配置的初始化回调,因此,初始化回调是作用在原始的bean实例上,这意味着,AOP拦截器等还没有起作用。
混用不同方式
对于Spring 2.5,有3中不同的方式控制bean的生命周期行为:
- InitializingBean和DisposableBean回调
- 自定义的init和destroy方法
- @PostConstruct和@PreDestroy注解
如果对于一个bean采用不同的方式声明声明周期回调,并采用不同的方法名称,那么会采用如下将介绍的顺序进行调用。而如果不同方式配置了同样的方法名称,如init方法,该方法仅会执行一次。
对于同一个bean,如果配置了不同的初始化方法,其执行顺序如下:
-
Methods annotated with
@PostConstruct
-
afterPropertiesSet()
as defined by theInitializingBean
callback interface -
A custom configured
init()
method
销毁时回调时的顺序是一样的
-
Methods annotated with
@PreDestroy
-
destroy()
as defined by theDisposableBean
callback interface -
A custom configured
destroy()
method
启动和关闭时回调
LifeCycle接口定义了对于任何对象都必要的方法
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
spring管理的对象都可以实现LifeCycle接口,当ApplicationContext接收到启动或关闭信号后,它会进一步委托LifecycleProcessor调用所有的容器管理的Lifecycle的实现的对应方法
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
LifecycleProcessor本身就是LifeCycle的扩展,它添加了两个方法以响应容器刷新和关闭。
LifeCycle仅明确定义了启动和关闭时的方法,并没有指明容器刷新时自动启动的行为,对于更细粒度控制特定bean的自动启动,可以考虑使用SmartLifecycle。
启动和关闭的调用顺序是很重要的,如果两个对象具有依赖关系,依赖方会在被依赖方启动之后进行启动,并在被依赖方关闭前关闭。然而,很多情况下这种依赖关系是不明确的,你可能仅仅知道某种类型的对象要在其它类型对象启动之前启动。在这种情况下,SmartLifecycle定义了另一种方式,也就是其父接口Phased中的getPhase()方法:
public interface Phased {
int getPhase();
}
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
当启动时,具有最低phase值的对象先启动,关闭时则采取相反的顺序。当考虑phase的值时,需要注意的是,对于仅实现Lifecycle接口而没有实现SmartLifecycle的对象,对应的phase值为0.
SmartLifecycle接口的stop方法接收一个回调,对于任何实现在停止过程完成后都要调用回调的run方法,这使得在必要时异步关闭成为可能,由于LifecycleProcessor的默认实现DefaultLifecycleProcessor会对每一phase等待回调完成直到超时,默认的超时时间是30s。可以在容器中定义一个名称为lifecycleProcessor的bean重写默认实现行为。如果仅修改超时时间,可以采用如下配置:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
非Web应用优雅的关闭Spring容器
如果在非Web环境使用Spring容器,需要注册一个JVM关闭钩子。这样做可以确保优雅的进行关闭,并调用单例bean的销毁方法,使得资源得以释放。可以通过调用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");
// 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...
}
}
ApplicationContextAware和BeanNameAware
当ApplicationContext实例化一个实现了org.springframework.context.ApplicationContextAware接口的对象时,会给该对象提供一个ApplicationContext的引用
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
当ApplicationContext实例化一个实现了org.springframework.beans.factory.BeanNameAware接口的对象时,会给该对象提供在bean定义时的bean名称。
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}
其它Aware接口
Name | Injected Dependency | Explained in… |
---|---|---|
| Declaring | |
| Event publisher of the enclosing | |
| Class loader used to load the bean classes. | |
| Declaring | |
| Name of the declaring bean. | |
| Resource adapter | |
| Defined weaver for processing class definition at load time. | |
| Configured strategy for resolving messages (with support for parametrization and internationalization). | |
| Spring JMX notification publisher. | |
| Configured loader for low-level access to resources. | |
| Current | |
| Current |
Bean定义继承
一个bean定义可能会包含很多配置信息,包括构造器参数、属性、容器相关信息(初始化方法、静态工厂方法等),一个子bean定义可以从父bean定义继承这些配置信息。在需要时,子定义可以覆写父定义中的值,也可以添加其它的配置信息。
如果采用编程方式使用ApplicationContext,子bean定义是通过ChildBeanDefinition类来表示。大多数用户并不会采用这种方式,而是通过ClassPathXmlApplicationContext声明式配置bean。当采用XML方式配置时,可以通过parent属性来指定该子bean对应的父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"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>
子bean定义可以使用父级定义的配置信息,也可以覆写相应的信息,在覆写情况下子级类和父级必须是兼容的。
子bean定义会继承作用域、构造器参数、属性值及从父级覆写的方法。任何对于作用域、初始化方法、销毁方法及静态工厂方法的设置都会覆盖父级中相应的配置。
对于如下配置,仅会从子定义中获取:depends on, autowire mode, dependency check, singleton, and lazy init
在前面的例子中,我们显式的指定了父级bean定义是抽象的,如果对于父级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>
父级bean定义由于并不完备,因而不能进行实例化,当一个bean被定义成抽象时,通常作为子级bean定义的模板。当试图使用这样的抽象父级bean作为其它bean的属性或通过getBean获取其时,会报错。相似的,容器内部的preInstantiateSingletons()方法也会忽略定义为抽象的bean。