没明白过的Spring Bean 生命周期
前言
在前端的一些框架例如Vue,React中,如果你不了解生命周期这个概念和它的使用方式,你的日常工作大概很难展开,因为你往往需要做很多页面变量的初始化工作。
但是在使用Spring时,我们并不了解这个概念但似乎也没有出现什么问题,因为我们大多数时候需要Spring管理的对象都是一些无状态的服务型的对象,无状态的意思就是对象中没有什么数据,只是单纯的提供服务逻辑,所以说如果不是我们要对Bean做出什么定制化的操作,我们一般是不会用到相关的方法和注解的。
但是Spring就是一个创建对象,管理对象的容器,了解Spring的生命周期,也就是它创建对象的过程仍然是必要的,特别是在我们想要对Bean进行一些修改的时候。
参考链接:Spring官方文档 (1.6小节)
钩子方法
我们知道Spring最主要的工作就是帮我们管理对象,包括对象的创建,依赖的注入,对象的销毁等等,在整个过程之中,Spring为我们提供了一些钩子方法,利用这些方法我们就可以在不同的阶段对Bean进行一些初始化的处理或者销毁前的资源释放处理或是其他任何你想要干的事情,如下图红色的XXX方法就是我们可以实现的方法,实现之后就可以在创建后,销毁前做一些操作了
Spring的生命周期方法并不是只有这么几个,特别是还有一个非常重要的接口BeanPostProcessor
这里也还没提到,不过更详细,更复杂的过程就先不放出来了,等大家对生命周期有一个基本的理解之后,再看这些会容易一些。
初始化方法
初始化方法就是在对象创建完成之后我们可以实现一些方法,通过这些方法,我们可以对Bean进行一些操作,比如初始化或者是修改一些对象的属性等等。
实现方法一共有三种 (以下代码均来自官方文档):
- XML实现
在XML中定义init-method
方法:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
- 接口实现
public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// do some initialization work
}
}
- 注解实现
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
}
代码demo
为了方便大家理解,这里就举一个简单的例子:
现在有一个Request
类,成员属性有type,head,url
如果type是test,就使用测试环境的head:localhost:8080/test
如果type是prod,就使用生产环境的head:localhost:8080/prod
最后拼接URL,得到完整的地址
ps : 其实一般不会这么做,关于这种分环境使用不同的值,Spring中可以通过profile来实现,所以这里的例子仅用于理解
三种方式实现
- XML实现
- 引入依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
- 写好Request类
public class Request {
private String type = "test";
private String head;
private String url = "/service/goods/1";
public String getUrl() {
return head + url;
}
}
- 定义好bean,并添加init-method方法
<bean id="request" class="com.thoughtliu.Request" init-method="initUrl"/>
- 实现初始化方法
在Request类中添加initUrl方法
private void initUrl() {
if (type.equals("test")) {
head = "http:localhost/test";
}else {
head = "http:localhost/prod";
}
}
- 测试
public static void main(String[] args) {
String xml = "classpath:/springconfig.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(xml);
Request request = context.getBean("request",Request.class);
System.out.println(request.getUrl());
}
测试结果:
http:localhost/test/service/goods/1
- 接口实现
和上面基本是一样的,但关键的地方是,bean定义中不用写init-method
方法了,实现初始化的方式改为实现InitializingBean
接口 (但正因如此,方法名称也不能随便取了)
<bean id="requestWithInitBean" class="com.thoughtliu.RequestWithInitBean"/>
public class RequestWithInitBean implements InitializingBean {
private String type = "prod";
private String head;
private String url = "/service/goods/1";
public String getUrl() {
return head + url;
}
@Override
public void afterPropertiesSet() throws Exception {
if (type.equals("test")) {
head = "http:localhost/test";
}else {
head = "http:localhost/prod";
}
}
}
测试结果
http:localhost/prod/service/goods/1 (注意type是prod,所以请求头也变了)
- 注解实现
<bean id="requestWithPostConstruct" class="com.thoughtliu.RequestWithPostConstruct"/>
public class RequestWithPostContruct {
private String type = "prod";
private String head;
private String url = "/service/goods/1";
public String getUrl() {
return head + url;
}
@PostConstruct
private void initUrl() {
if (type.equals("test")) {
head = "http:localhost/test";
}else {
head = "http:localhost/prod";
}
}
}
测试结果:
http:null/service/goods/1 (head竟然是null !!!!!)
这里head是null说明注解没有生效,该注解对应的文档是1.9 小结,通过查阅文档1.9小节得知 要使用这些注解需要开启注解配置:
<context:annotation-config/>
再次测试就成功了:
http:localhost/prod/service/goods/1
三种方式谁先谁后
这里肯定有人好奇要问了,如果我同时执行了三种方法,那么最终会执行哪个呢?通过测试发现,三种都会执行,执行顺序为:
注解 -> 接口 -> xml定义 ,所以如果你同时使用三种方法设置head,XML定义的方法所设置的head会是最终结果。
对象销毁钩子方法
这一部分实际上和初始化方法非常相像,这里就不再重复写了
-
xml方式
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
-
接口方式
public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
- 注解方式
使用@PostDestroy
注解
BeanPostProcessor
在上面的基础上我们可以再进一步的来看一个非常重要的接口它就是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;
}
}
从接口方法的名称我们就可以看出来,这个方法的执行的顺序就是在init方法前后,结合上面的init和destroy 整个过程就变成了(你可以通过打印语句验证顺序):
- 创建对象
- 依赖注入
postProcessBeforeInitialization
init
方法(三种init,顺序上面讲过)postProcessAfterInitialization
- destroy
作用
那么这个接口和上面的接口有什么不一样呢?通过下面的例子你就可以发现初始化方法还需要我们在每一个类中都写好方法才行,是对每一个Bean
的定制化操作,而BeanPostProcessor
接口一旦实现之后就会对所有Bean
生效
代码demo
还是拿刚才那个Request
为例,我们只是单纯的打印一下Bean
的一些相关信息:
public class CustomBean implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之前" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之后" + bean.getClass());
return bean;
}
}
然后注册一下这个Bean:
<bean id="customBean" class="com.thoughtliu.CustomBean"/>
然后拿之前的Request
类来测试一下(我在Request
的initUrl
方法中加了打印语句):
测试结果:
可以看出来我们只是注册了这个Bean
,其他的啥也没干,它就自动打印出Request的相关信息了,因为这是在每个Bean创建时都会执行的的一个接口,默认是什么也不做,直接返回原有Bean
多个BeanPostProcessor实现顺序
从上面的例子可以看出来我们只需要定义一个Bean
然后注册到Spring
中去就可以实现对每个Bean
的定制化操作了,那么我们完全有能力定义很多个Bean,那他们的执行顺序是怎样的呢?
我们这里可以猜一下,总共只有两个步骤:
- 完成实现类
- 注册Bean
首先我们在书写实现类的时候,不同的类之间完全没有顺序可言,那么就还剩注册的时候,注册的顺序决定了方法执行的顺序,比如我们新建一个和CustomBean
一模一样的类CustomBean2
<bean id="customBean2" class="com.thoughtliu.beanpostprocessor.CustomBean2"/>
<bean id="customBean" class="com.thoughtliu.beanpostprocessor.CustomBean"/>
当我们这样注册的时候CustoBean2的方法就会先执行
Ordered
但我们如果对Spring稍微有一点了解就会知道,在我们真正使用Spring的时候,往往会使用注解的方式来注册Bean,那么此时他们的顺序又如何呢?
<context:component-scan base-package="com.thoughtliu.beanpostprocessor"/> // 上面的custombean的定义就不要了
查看了一下文档似乎没有找到,不过通过我的试验,当使用自动检测时,它是根据名字来排序的,其实也就是按照注册的顺序来执行的。
这样的顺序当然不是我们想要的。所以Spring提供了一种方法让我们去定制它的顺序。
代码demo
最关键的就是这里要实现Ordered
接口,值越小,优先级越高
@Component
public class CustomBean implements BeanPostProcessor, Ordered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之前" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之后" + bean.getClass());
return bean;
}
@Override
public int getOrder() {
return 1;
}
}
再新建一个和CustomBean基本类似的类:
@Component
public class CustomBean2 implements BeanPostProcessor, Ordered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之前2" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之后2" + bean.getClass());
return bean;
}
@Override
public int getOrder() {
return 0;
}
}
如果不实现Ordered接口执行的话,Custom2的方法应该是后执行的,但现在它是先执行的,你也可以自己改变顺序尝试一下。
ps : 这里看到网上有人说@Order
可以起到同样的作用,但是实际执行之后发现并不起作用,然后在知乎上看到一个回答,放在这里给大家参考一下:
https://zhuanlan.zhihu.com/p/91783100
今天这篇文章就先介绍到这里,其实Spring的生命周期中还有很多的内容,比如*Aware
接口,比如默认初始化方法,比如BeanPostProcessor
和AOP的关系,就算是上面已经说过的内容可能也有一些缺漏,比如没有提到Java方式注册Bean然后实现初始化方法等,但这篇文章的目的主要是对Spring生命周期有一个基本的了解,其他的内容有时间再专门来写