SpringBoot:详解Bean生命周期和作用域

🏡浩泽学编程个人主页
🔥 推荐专栏《深入浅出SpringBoot》《java项目分享》
《RabbitMQ》《Spring》《SpringMVC》

🛸学无止境,不骄不躁,知行合一

文章目录


前言

前面我们讲诉了将Bean正确地装配到IoC容器,却未讲诉IoC如何装配和销毁Bean。本篇文章主要讲诉一下Bean的生命周期和作用域。


一、生命周期

  • Bean 的生命周期的过程, 它大致分为Bean定义、Bean 的初始化、 Bean 的生存期和 Bean 的销毁4个部分。 其中 Bean 定义过程大致如下:

    • Spring 通过我们的配置,如@ComponentScan 定义的扫描路径去找到带有@Component 的类,
      这个过程就是一个资源定位的过程。
    • 一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始
      化Bean,也就没有Bean 的实例,它有的仅仅是Bean 的定义
    • 然后就会把Bean 定义发布到 Spring IoC 容器中。 此时, IoC 容器也只有Bean 的定义,还是
      没有Bean 的实例生成
  • 完成了这3 步只是一个资源定位并将Bean 的定义发布到IoC容器的过程,还没有Bean实例的生成,更没有完成依赖注入。在默认的情况下, Spring会继续去完成Bean 的实例化和依赖注入 ,这样从IoC 容器中就可以得到一个依赖注入完成的Bean。 但是,有些Bean会受到变化因素的影响,这时我们倒希望是取出 Bean 的时候完成初始化和依赖注入换句话说就是让那些 Bean 只是将定义发布到IoC 容器而不做实例化和依赖注入, 当我们取出来的时候才做初始化和依赖注入等操作。

Spring Bean的初始化过程:

ComponentScan 中还有一个配置项 lazyI nit,只可以配置 Boolean 值,且默认值为 false,也就是默认不进行延迟初始化,因此在默认的情况下Spring会对Bean进行实例化和依赖注入对应的属性值

引入例子:人类(Person)有时候利用一些动物(Animal)去完成一些事情,比方说狗(Dog)是用来看门的,猫(Cat)是用来抓老鼠的.。

代码如下:

java 复制代码
//定义人类接口
public interface Person {
    void service();

    void setAnimal(Animal animal);
}
//定义动物接口
public interface Animal {
    void user();
}
//定义狗
@Component
public class Dog implements Animal {
    @Override
    public void user() {
        System.out.println("狗【" + Dog.class.getSimpleName() + "】是用来看门的");
    }
}
//定义年轻人
@Component
public class YoungPerson implements Person {
    @Autowired
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.user();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}
//定义猫
@Component
public class Cat implements Animal{
    @Override
    public void user() {
        System.out.println("猫【" + Cat.class.getSimpleName() + "】是抓老鼠的");
    }
}

//定义配置类
@Configuration
@ComponentScan("com.dragon.restart")//所有的包和类都在restart下
public class AppConfig {
}

此时没有配置lazyInit的情况进行断点测试如下:

可以看到在断点处,我们并没有获取Bean 的实例,而日志就已经打出了,可见它是在SpringIoC容器初

始化时就执行了实例化和依赖注入。为了改变这个情况,我们在配置类AppConfig的@ComponentScan

中加入lazylnit 配置,如下面的代码:

java 复制代码
@Configuration
@ComponentScan(value = "com.dragon.restart",lazyInit = true)
public class AppConfig {
}


就可以发现在断点处"延迟依赖注入"这行并不会出现在日志中,只有运行过断点处才会出现这行日志,这是因为我们把它修改为了延迟初始化, Spring并不会在发布Bean定义后马上为我们完成实例化和依赖注入。

如果仅仅是实例化和依赖注入还是比较简单的,还不能完成进行自定义的要求。 为了完成依赖注入的功能, Spring 在完成依赖注入之后,还提供了一系列的接口和配置来完成Bean初始化的过程,让我们学习这个过程。 Spring在完成依赖注入后,还会进行如下图所示流程来完成它的生命周期:

图中描述的是整个IoC容器初始化Bean 的流程,作为开发者,需要注意这些流程。除此之外,还需要注意以下两点:

  • 这些接口和方法是针对什么而言的。 对于上图, 在没有注释的情况下的流程节点都是针对单个Bean 而言的,但是BeanPostProcessor 是针对所有 Bean 而言的,这是我们需要注意的地方。
  • 即使你定义了 ApplicationContextAware 接口,但是有时候并不会调用,这要根据你的 IoC 容器来决定。 我们知道, Spring IoC 容器最低的要求是实现 BeanFactory 接口,而不是实现ApplicationContext 接口 。 对于那些没有实现 ApplicationContext 接口的容器,在生命周期对应的ApplicationContextAware 定义的方法也是不会被调用的,只有实现了 ApplicationContext 接口的容器,才会在生命周期调用 ApplicationContextAware 所定义的 setApplicationContext方法。

现在改造一下YoungPerson类:

java 复制代码
@Component
public class YoungPerson implements Person, BeanNameAware , BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.user();
    }
    @Autowired
    @Qualifier("dog")
    @Override
    public void setAnimal(Animal animal) {
        System.out.println("延迟依赖注入");
        this.animal = animal;
    }

    @Override
    public void setBeanName(String name) {
        System.out.println ("【" + this.getClass().getSimpleName() + "】调用BeanNameAware的setBeanName");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println ("【" + this.getClass().getSimpleName() + "】调用BeanFactoryAware的setBeanFactory");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println ("【" + this.getClass().getSimpleName() + "】调用DisposableBean方法");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println ("【" + this.getClass().getSimpleName() + "】调用InitializingBean方法的afterPropertiesSet方法");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println ("【" + this.getClass().getSimpleName() + "】调用ApplicationContextAware方法的setApplicationContext方法");
    }
    @PostConstruct
    public void init () {
        System.out.println("【" + this.getClass().getSimpleName() + "】注解@PostConstruct定义的自定义初始化方法");
    }
    @PreDestroy
    public void destroyl () {
        System.out.println("【" + this.getClass().getSimpleName() + "】注解@PreDestroy定义的自定义销毁方法");
    }
}

这样,这个 B巳an 就实现了生命周期中单个 Bean 可以实现的所有接口, 并且通过注解@PostConstruct 定义了初始化方法,通过注解@PreDestroy 定义了销毁方法。 为了测试 Bean 的后置处理器, 这里创建一个类BeanPostProcessorExampIe,如下:

java 复制代码
/**
 * @Version: 1.0.0
 * @Author: Dragon_王
 * @ClassName: BeanPostProcessorExample
 * @Description: TODO描述
 * @Date: 2024/1/20 23:34
 */
public class BeanPostProcessorExample implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor调用"+
                "postProcessBeforeinitialization方法,参数【"+
                bean.getClass().getSimpleName()+"】【"+beanName+"】");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor调用"+
                "postProcessAfterinitialization方法,参数【"+
                bean.getClass().getSimpleName()+"】【"+beanName+"】");
        return bean;
    }
}

注意,这个Bean后置处理器将对所有的Bean有效,运行测试如下:

测试类:

java 复制代码
AnnotationConfigApplicationContext ctx =new AnnotationConfigApplicationContext(AppConfig.class) ;
		ctx.close();
java 复制代码
2024-01-20T23:43:23.135+08:00  INFO 748 --- [           main] c.d.restart.RestartApplicationTests      : Starting RestartApplicationTests using Java 19 with PID 748 (started by ThundeRobot in E:\IDEA_projects\restart)
2024-01-20T23:43:23.136+08:00  INFO 748 --- [           main] c.d.restart.RestartApplicationTests      : No active profile set, falling back to 1 default profile: "default"
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【RestartApplication$$SpringCGLIB$$0】【restartApplication】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【RestartApplication$$SpringCGLIB$$0】【restartApplication】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【AppConfig$$SpringCGLIB$$0】【appConfig】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【AppConfig$$SpringCGLIB$$0】【appConfig】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【Cat】【cat】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【Cat】【cat】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【Dog】【dog】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【Dog】【dog】
延迟依赖注入
【YoungPerson】调用BeanNameAware的setBeanName
【YoungPerson】调用BeanFactoryAware的setBeanFactory
【YoungPerson】调用ApplicationContextAware方法的setApplicationContext方法
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【YoungPerson】【youngPerson】
【YoungPerson】注解@PostConstruct定义的自定义初始化方法
【YoungPerson】调用InitializingBean方法的afterPropertiesSet方法
BeanPostProcessor调用postProcessAfterinitialization方法,参数【YoungPerson】【youngPerson】
BeanPostProcessor 调用 postProcessBeforeinitialization 方法,参数 【Cat】【cat】
 BeanPostProcessor 调用 postProcessAfterinitialization 方法, 参数 【Cat】【cat】
 2024-01-20T23:43:24.044+08:00  INFO 748 --- [main] c.d.restart.RestartApplicationTests      : Started RestartApplicationTests in 1.142 seconds (process running for 1.772)
【YoungPerson】注解@PreDestroy定义的自定义销毁方法
【YoungPerson】调用DisposableBean方法

从日志可以看出,对于Bean后置处理器(BeanPostProcessor)而言, 它对所有的 Bean 都起作用,而其他的接口则是对于单个Bean起作用。我们还可以注意到BussinessPerson执行的流程是上图所画出的流程。有时候Bean 的定义可能使用的是第三方的类,此时可以使用注解@Bean来配置自定义初始化和销毁方法,如下所示:

java 复制代码
@Bean(InitMethod ="Init", destroyMethod = "destroy" )

二、作用域

在介绍IoC 容器最顶级接口 BeanFactory 的时候, 可以看到 isSingleton 和 isPrototype 两个方法。其中,isSingleton 方法如果返回 true,则 Bean 在 loC 容器中以单例存在,这也是 Spring IoC 容器的默认值;如果 isPrototype 方法返回 true,则当我们每次获取 Bean 的时候, IoC 容器都会创建一个新的 Bean,这显然存在很大的不同,这便是Spring Bean 的作用域的问题。在一般的容器中, Bean都会存在单例(Singleton)和原型(Prototype)两种作用域, Java EE 广泛地使用在互联网中,而在 Web容器中, 则存在页面(page)、请求(request)、会话 (session)和应用(application) 4 种作用域对于页面(page),是针对 JSP 当前页面的作用域,所以 Spring是无法支持的。为了满足各类的作用域,在Spring 的作用域中就存在如表所示的几种类型。

作用域类型 使用范围 作用域描述
singleton 所有Spring 应用 默认值, loC 容器只存在单例
prototype 所有Spring 应用 每当从IoC 容器中取出一个 Bean,则创建一个新的Bean
session Spring Web 应用 HTTP 会话
application Spring Web 应用 Web 工程生命周期
request Spring Web 应用 Web 工程单次请求 (request)
globalSession Spring Web 应用 在一个全局的HTTPSession 中, 一个 Bean 定义对应一个实例。 实践中基本不使用
  • 前四个最常用
  • 对于application作用域,完全可以使用单例来替代。

下面我们探讨单例 (Singleton)和原型(prototype)的区别

首先定义一个类

java 复制代码
@Component 
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 
public class ScopeBean { }

这是一个简单的类, 可以看到这里声明作用域的代码已经被注释掉了, 这样就是启用默认的作用域,实际就是单例。为了证明作用域的存在,我们进行一下测试:

java 复制代码
AnnotationConfigApplicationContext ctx
				=new AnnotationConfigApplicationContext (AppConfig.class);
		ScopeBean scopeBeanl = ctx.getBean (ScopeBean.class);
		ScopeBean scopeBean2 = ctx.getBean (ScopeBean .class);
		System.out.println (scopeBeanl == scopeBean2) ;

从测试的结果来看,显然scopeBeanl 和 scopeBean2 这两个变量都指向了同一的实例,所以在IoC容器中, 只有一个ScopeBean 的实例。 然后取消代码中作用域代码的注释,进行同样的测试, 则可以看到scopeBeanl == scopeBean2 返回的将是 false,而不再是 true, 那是因为我们将Bean 的作用域修改为了 prototype,这样就能让IoC 容器在每次获取Bean 时,都新建一个Bean的实例返回给调用者。

这里的 ConfigurableBeanFactory 只能提供单例 ( SCOPE_ SINGLETON )和原型 ( SCOPE_PROTOTYPE)两种作用域供选择, 如果是在 SpringMVC环境中,还可以使用 WebApplicationContext去定义其他作用域, 如请求(SCOPE REQUEST)、 会话 (SCOPE_SESSION) 和应用 (SCOPE_APPLICATION)。 例如,下面的代码就是定义请求作用域:

java 复制代码
@Component 
@Scope(WebApplicationContext.SCOPE_REQUEST) 
public class ScopeBean { }

总结

以上就是Bean生命周期和作用域的讲解。

相关推荐
微信-since811925 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails
色空大师9 分钟前
23种设计模式
java·开发语言·设计模式
闲人一枚(学习中)10 分钟前
设计模式-创建型-建造者模式
java·设计模式·建造者模式
2202_7544215427 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
蓝染-惣右介30 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习31 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
HoneyMoose33 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热34 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余36 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp36 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea