Spring丨Bean的生命周期

前言

Spring 容器可以管理 Singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁。了解 Spring 生命周期的意义就在于,可以利用 Bean 在其存活期间的指定时刻完成一些相关操作。这种时刻可能有很多,但一般情况下,会在 Bean 被初始化后和被销毁前执行一些相关操作。

可以干预的阶段

Bean 的生命周期
为什么说在“被初始化后”和“被销毁前”干预呢?请想想,SpringFramework 作为一个框架,它能干预的是哪几个阶段呢?很明显,对象的回收阶段不行。
再想想,在使用 SpringFramework 来获取 Bean 的前提下,我们又能干预哪几个阶段呢?那就只剩下“初始化”和“销毁”这两个阶段可以干预了。
那 SpringFramework 如何能让我们干预 Bean 的初始化和销毁呢?

Servlet 的生命周期

现在回想一下 Servlet 的生命周期,是不是存在 init() 方法与 destroy() 方法?Servlet 初始化后调用 init() 方法,在销毁前调用 destroy() 方法。

@WebServlet(name = "AServlet", value = "/AServlet")
public class AServlet implements Servlet {
    // 模仿 GenericServlet,获取 ServletConfig 对象
    private ServletConfig config;

    /**
     * 生命周期方法,会在 servlet 对象创建之后马上执行
     * 该函数用于初始化 servlet,类似于类的构造函数,该函数只会被调用一次
     * @param servletConfig
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("init()...");
        this.config = servletConfig;

        // 获取 <servlet-name> 中的名字
        String servletName = servletConfig.getServletName();
        System.out.println("servlet名称:" + servletName);
        // 获取上下文对象
        ServletContext sc =  servletConfig.getServletContext();
        // 获取全部初始化参数的名称,返回的结果是一个迭代器
        Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
        while(initParameterNames.hasMoreElements()){
            // 输出初始化参数的名称
            System.out.println("参数名称:" + initParameterNames.nextElement());
        }
        // 通过参数的名称 获取参数的初始值
        String initParameter1 = servletConfig.getInitParameter("p1");
        System.out.println("指定参数名称获取的初始值:" + initParameter1);
        // 自定义方法,在生命周期方法中执行
        this.init();
    }

    /**
     * 模拟 GenericServlet ,一个自定义的方法,非生命周期方法
     * 这样子类继承父类,可以增强拓展性,也不需要
     * @throws ServletException
     */
    public void init() throws ServletException{

    }

    /**
     * 用来获取 servlet 的配置信息
     * @return
     */
    @Override
    public ServletConfig getServletConfig() {
        System.out.println("getServletConfig()...");
        return this.config;
    }

    /**
     * 生命周期方法
     * 这个函数用于处理业务逻辑。当用户每访问 servlet时,都会调用
     * @param servletRequest 用于获取客户端(浏览器)请求
     * @param servletResponse 用于向客户端(浏览器)返回响应
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("service()...");
    }

    /**
     * 用来获取 servlet 的信息
     * @return
     */
    @Override
    public String getServletInfo() {
        System.out.println("getServletInfo()...");
        return "【信息】——什么是快乐星球?我来带你研究";
    }

    /**
     * 生命周期方法,会在 servlet 被销毁之前被调用,也是只会被调用一次
     * 需要就写,不需要就不写
     * 销毁,释放内存,三种方式会触发destroy
     * 1. reload该 servlet(webApps) 2.关闭 tomcat 3.关机
     */
    @Override
    public void destroy() {
        System.out.println("destroy()...");
    }
}

这两个方法都是被 Web 容器调用的,用来初始化和销毁 Servlet 的。这种方法的设计思想其实就是“回调机制”,它都不是自己设计的,而是由父类 / 接口定义好的,由第三者(框架、容器等)来调用。

init-method 与 destroy-method

与 Servlet 的 init()destroy() 方法类似,Spring 也有相应的方法用于自定义初始化与销毁。
为了方便演示 XML 与注解的方式,创建两个类分别演示,创建两个普通类:CatDog

创建 Bean1

为了节省时间(偷懒),两个普通类的代码基本一致,只写一个:

public class Cat {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void init() {
        System.out.println(name + "被初始化了...");
    }

    public void destroy() {
        System.out.println(name + "被销毁了...");
    }
}

配置1

Cat 类对应的 XML 配置文件:

<!-- XML 演示 Bean 的生命周期 -->
<bean id="cat2" class="com.study.lifecycle.Cat" init-method="init" destroy-method="destroy">
    <property name="name" value="阿猫"/>
</bean>

<bean> 标签中,有两个属性,就是上面的 init-methoddestroy-method ,属性值对应类中的 inint() 方法与 destroy() 方法。

bean 配置文件属性 init-method 用于在 bean 初始化时指定执行方法,用来替代继承 InitializingBean接口。而 destroy-method 则相反,是在 bean 销毁时执行的方法。


Dog 类对应的注解配置:类似于 XML ,@Bean 注解中也有类似的属性,只不过 Java 中的属性名不能带短横线,所以就改用驼峰命名:

package com.study.lifecycle;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author yyt
 * 注解配置初始化方法与注销方法
 */
@Configuration
public class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Dog dog() {
        Dog dog = new Dog();
        dog.setName("旺财");
        return dog;
    }
}

测试运行1

分别初始化 XML 和注解驱动的容器,添加一些输出语句以便观察。

package com.study.lifecycle;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author yyt
 * @date 2021年12月07日 21:27
 */
public class InitMethodXmlApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器...");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 注解配置 —— LifeCycleConfig.class
        // AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        System.out.println("IOC容器初始化完成...");
        System.out.println();
        System.out.println("准备销毁IOC容器...");
        // 调用 close() 方法对容器进行关闭,以触发 Bean 的销毁动作
        ctx.close();
        System.out.println("IOC容器销毁完成...");
    }
}

运行 main 方法,控制台会输出以下内容:

准备初始化IOC容器...
阿猫被初始化了...
IOC容器初始化完成...

准备销毁IOC容器...
阿猫被销毁了...
IOC容器销毁完成...

那么可知:在 IOC 容器初始化之前,默认情况下 Bean 已经创建好了,而且完成了初始化动作;容器调用销毁动作时,先销毁所有 Bean ,最后 IOC 容器全部销毁完成。

验证流程

修改一下 Cat 类,分别在构造方法和 setName() 方法中加入控制台打印,这样在触发这些方法时,会在控制台上得以体现:

public class Cat {

    private String name;

    public Cat() {
        System.out.println("Cat的构造方法执行了...");
    }

    public void setName(String name) {
        System.out.println("Cat的setName方法执行了...");
        this.name = name;
    }

    public void init() {
        System.out.println(name + "被初始化了...");
    }

    public void destroy() {
        System.out.println(name + "被销毁了...");
    }
}

重新运行启动类的 main 方法,控制台会打印如下内容:

准备初始化IOC容器...
Cat的构造方法执行了...
Cat的setName方法执行了...
阿猫被初始化了...
IOC容器初始化完成...

准备销毁IOC容器...
阿猫被销毁了...
IOC容器销毁完成...

由此可知:Bean 的生命周期中,是先对属性赋值,后执行 init-method 标记的方法。

JSR250 规范

JSR 250 规范包含用于将资源注入到端点实现类的注释和用于管理应用程序生命周期的注释,除了有 @Resource 这样的自动注入注解,还有负责生命周期的注解,包括 @PostConstruct@PreDestroy 这两个注解,分别对应 init-methoddestroy-method

创建 Bean2

假设一种场景:钢笔与墨水,刚买来的动作代表实例化,加墨水的动作代表初始化,倒掉所有墨水的动作代表销毁,于是这个 Pen 类可以这样设计:

package com.study.jsr250specification;

import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * @author yyt
 */
@Component
public class Pen {

    private Integer ink;

   public Pen(){
        System.out.println("买来了钢笔和墨水");
    }

    @PostConstruct
    public void addInk() {
        System.out.println("已经将墨水注入到钢笔中...");
        this.ink = 100;
    }

    @PreDestroy
    public void outwellInk() {
        System.out.println("将钢笔中的墨水都倒掉了...");
        this.ink = 0;
    }

    @Override
    public String toString() {
        return "Pen{" + "ink=" + ink + '}';
    }
}

对于 JSR250 规范的这两个注解的使用,直接标注在 Bean 的方法上即可:被 @PostConstruct@PreDestroy 注解标注的方法,与 init-methoddestroy-method 方法的声明要求是一样的,访问修饰符也可以是 private

测试运行2

编写启动类,直接扫描这个 Pen 类:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * @author yyt
 */
public class JSR250AnnoApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器...");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.study.jsr250specification");
        System.out.println("IOC容器初始化完成...");
        System.out.println();
        System.out.println("准备销毁IOC容器...");
        // 关闭容器
        ctx.close();
        System.out.println("IOC容器销毁完成...");
    }
}

运行 main 方法,控制台中打印如下内容:

准备初始化IOC容器...
买来了钢笔和墨水
已经将墨水注入到钢笔中...
IOC容器初始化完成...

准备销毁IOC容器...
将钢笔中的墨水都倒掉了...
IOC容器销毁完成...

由此可见,这两个注解也完成了像 init-methoddestroy-method 一样的效果。

InitializingBean 与 DisposableBean

这两个实际上是两个接口,而且是 SpringFramework 内部预先定义好的两个关于生命周期的接口。他们的触发时机与上面的 init-methoddestroy-method 以及 JSR250 规范的两个注解一样,都是在 Bean 的初始化和销毁阶段要回调的。

创建 Bean3

使用 Pen3 类作为演示对象,让 Pen3 实现这两个接口:

package com.study.lifecycle.initializingbean;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

/**
 * @author yyt
 * 演示 InitializingBean 与 DisposableBean 接口的使用
 */
@Component
public class Pen3 implements InitializingBean, DisposableBean {

    private Integer ink;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("钢笔3中已加满墨水了...");
        this.ink = 100;
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("钢笔3中的墨水完全清理了...");
        this.ink = 0;
    }

    @Override
    public String toString() {
        return "Pen{" + "ink=" + ink + '}';
    }
}

测试运行3

直接使用注解驱动 IOC 容器扫描这个 Pen3 ,其余编写内容与上面一致:

package com.study.lifecycle.initializingbean;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * @author yyt
 */
public class InitializingDisposableAnnoApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器...");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.study.lifecycle.initializingbean");
        System.out.println("IOC容器初始化完成...");
        System.out.println();
        System.out.println("准备销毁IOC容器...");
        ctx.close();
        System.out.println("IOC容器销毁完成...");
    }
}

运行 main 方法,控制台同样打印了触发生命周期的内容:

准备初始化IOC容器...
钢笔3中已加满墨水了...
IOC容器初始化完成...

准备销毁IOC容器...
钢笔3中的墨水完全清理了...
IOC容器销毁完成...

三种生命周期方法并存

既然有三种生命周期方法,那么一个 Bean 同时使用这三种生命周期共同控制时,执行顺序是怎样的?

创建 Bean4

再复制出一个 Pen 类,命名为 Pen4 ,并同时实现三种生命周期的控制:

package com.study.lifecycle.allin;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * @author yyt
 * @date 2021年12月08日 20:05
 * 演示三种生命周期共存
 */
@Component
public class Pen4 implements InitializingBean, DisposableBean {

    private Integer ink;

    public void open() {
        System.out.println("init-method - 打开钢笔4...");
    }

    public void close() {
        System.out.println("destroy-method - 合上钢笔4...");
    }

    @PostConstruct
    public void addInk() {
        System.out.println("@PostConstruct注解 - 钢笔4中已加满墨水...");
        this.ink = 100;
    }

    @PreDestroy
    public void outwellInk() {
        System.out.println("@PreDestroy注解 - 钢笔4中的墨水都放干净了...");
        this.ink = 0;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean - 准备写字...");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean - 写完字了...");
    }
}

配置4

使用注解驱动方式注册这个 Pen4

@Bean(initMethod = "open", destroyMethod = "close")
public Pen4 pen() {
    return new Pen4();
}

测试运行4

使用注解 IOC 容器驱动这个配置类,运行 main 方法,观察控制台的打印:

package com.study.lifecycle.allin;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * @author yyt
 * 测试三种生命周期并存的优先级
 */
public class InitializingAllMethodApplication {
    public static void main(String[] args) {
        System.out.println("准备初始化IOC容器...");
        // 驱动配置类
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Pen4Configuration.class);
        System.out.println("IOC容器初始化完成...");
        System.out.println();
        System.out.println("准备销毁IOC容器...");
        ctx.close();
        System.out.println("IOC容器销毁完成...");
    }
}

输出结果:

准备初始化IOC容器...
@PostConstruct注解 - 钢笔4中已加满墨水...
InitializingBean - 准备写字...
init-method - 打开钢笔4...
IOC容器初始化完成...

准备销毁IOC容器...
@PreDestroy注解- 钢笔4中的墨水都放干净了...
DisposableBean - 写完字了...
destroy-method - 合上钢笔4...
IOC容器销毁完成...

由此可知,执行顺序:@PostConstructInitializingBeaninit-method

原型 Bean 的生命周期

单实例 Bean 的生命周期是陪着 IOC 容器一起的,容器初始化,单实例 Bean 也跟着初始化(不绝对);容器销毁,单实例 Bean 也跟着销毁。而原型 Bean 由于每次都是取的时候才产生一个,所以它的生命周期与 IOC 容器无关。

参考

W3Cschool- Spring Bean的生命周期
稀土掘金 - 如何记忆 Spring Bean 的生命周期
稀土掘金 - IOC基础-Bean的生命周期-初始化与销毁
打赏
评论区
头像
文章目录