Spring系列-02-Bean类型, 作用域, 实例化, 生命周期

Bean类型, 作用域, 实例化, 生命周期

Bean类型

在 SpringFramework 中,对于 Bean 的类型,一般有两种设计:

  • 普通Bean
  • FactoryBean

普通Bean

三种方式

  1. @Component注解
  2. 配置类+@Bean
  3. xml + <bean>
@Component
public class Child {}
@Bean
public Child child() {
    return new Child();
}
<bean class="com.linkedbear.spring.bean.a_type.bean.Child"/>

FactoryBean

出现场景: SpringFramework考虑到一些特殊的设计: Bean的创建需要指定一些策略,或者依赖特殊的场景来分别创建,也或者一个对象的创建过程太复杂,使用xml或者注解声明也比较复杂。可以借助**FactoryBean**来使用工厂方法创建对象

FactoryBean介绍

FactoryBean 本身是一个接口,它本身就是一个创建对象的工厂。如果 Bean 实现了 FactoryBean接口,则它本身将不再是一个普通的Bean ,不会在实际的业务逻辑中起作用,而是由创建的对象来起作用

FactoryBean 接口有三个方法:

  • getObject(): 返回创建的对象
  • getObjectType(): 返回创建的对象的类型(即泛型类型)
  • isSingleton(): 创建的对象是单实例Bean还是原型Bean,默认单实例
public interface FactoryBean<T> {
    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}
FactoryBean使用

场景: 小孩子要买玩具,由一个玩具生产工厂来给这个小孩子造玩具

前置准备

孩子类

public class Child {
    // 当前的小孩子想玩球
    private String wantToy = "ball";
    
    public String getWantToy() {
        return wantToy;
    }
}

一个抽象类和两个实现类

@Data
@NoArgsConstructor
public abstract class Toy { // 抽象玩具类
    /*
    玩具名
    */
    private String name;

    public Toy(String name) {
        System.out.println("生产了一个" + name);
        this.name = name;
    }
}

@Data
@EqualsAndHashCode(callSuper = true)
public class Ball extends Toy { // 球
    public Ball(String name) {
        super(name);
    }
}


@Data
@EqualsAndHashCode(callSuper = true)
public class Car extends Toy {
    public Car(String car) {
        super(name);
    }
}
运行示例

创建一个 ToyFactoryBean ,让它实现 FactoryBean 接口, 同时希望能根据小孩子想要玩的玩具来决定生产哪种玩具, 那么这里就需要注入Child 。由于咱这里面使用的不是注解式自动注入, 那咱就用setter注入

@Setter
public class ToyFactoryBean implements FactoryBean<Toy> {
    public ToyFactoryBean() {
        System.out.println("ToyFactoryBean 初始化了。。。");
    }

    /*
    Child类
    */
    private Child child;

    @Override
    public Toy getObject() throws Exception {
        // 根据Child中的wantToy属性, 来决定创建哪个玩具
        switch (child.getWantToy()) {
            case "ball":
                return new Ball("ball");
            case "car":
                return new Car("car");
            default:
                return null;
        }
    }

    /*
    表示这个工厂类返回Toy类
    */
    @Override
    public Class<Toy> getObjectType() {
        return Toy.class;
    }
}

希望能让它根据小孩子想要玩的玩具来决定生产哪种玩具,那咱就得在这里面注入Child 。由于咱这里面使用的不是注解式自动注入,那咱就用setter注入吧

注解方式注册这两个

@Configuration
public class BeanTypeConfiguration {
    @Bean
    public Child child() {
        return new Child();
    }
    
    @Bean
    public ToyFactoryBean toyFactory() {
        ToyFactoryBean toyFactory = new ToyFactoryBean();
        toyFactory.setChild(child());
        return toyFactory;
    }
}
public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
        Toy toy = ctx.getBean(Toy.class);
        // ToyFactoryBean 初始化了。。。
        // Ball()
        System.out.println(toy);
    }
}
FactoryBean与Bean同时存在

修改配置类BeanTypeConfiguration, 向IOC容器预先的创建一个Ball , 这样 FactoryBean 再创建一个, IOC容器里就会同时存在两个 Toy

@Configuration
public class BeanTypeConfiguration {
    @Bean
    public Child child() {
        return new Child();
    }

    // ---------- Start ----------
    @Bean
    public Toy ball() {
        return new Ball("ball");
    }
    // ---------- End ----------

    @Bean
    public ToyFactoryBean toyFactory() {
        ToyFactoryBean toyFactory = new ToyFactoryBean();
        toyFactory.setChild(child());
        return toyFactory;
    }
}

再次运行BeanTypeAnnoApplication类, 发现抛出NoUniqueBeanDefinitionException异常, 提示有两个 Toy

异常信息如下

Exception in thread “main” org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.linkedbear.spring.bean.a_type.bean.Toy’ available: expected single matching bean but found 2: ball,toyFactory

异常表明**FactoryBean创建的Bean是直接放在IOC容器中了**

查看IOC 容器中现有的 Toy

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);

        // 获取指定类型的所有Bean,并返回Map集合
        Map<String, Toy> toys = ctx.getBeansOfType(Toy.class);
        toys.forEach((name, toy) -> {
            System.out.println("toy name : " + name + ", " + toy.toString());
        });
        /*
			输出结果如下:
			ToyFactoryBean 初始化了。。。
			toy name : ball, Ball()
			toy name : toyFactory, Ball()
        */
    }
}
FacotryBean的加载和创建Bean的时机
FactoryBean的加载时机

之前的准备已经铺垫好了

Toy的构造方法中调用System打印

public class Toy  {
public Toy(String name) {
    System.out.println("生产了一个" + name);
    this.name = name;
}

ToyFactoryBean 的默认构造方法中也有System打印

public ToyFactoryBean() {
    System.out.println("ToyFactoryBean 初始化了。。。");
}

修改BeanTypeAnnoApplication的main方法, 只初始化IOC容器

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
    }
}

控制台输出结果如下: ToyFactoryBean 初始化了。。。, 这表示只有ToyFactoryBean被初始化, 说明**FactoryBean本身的加载是伴随IOC容器的初始化时机一起的**

Factory创建Bean的时机

经过之前的代码, 发现如果只初始化IOC容器, 那么此时是Facotry没有创建Bean的(即有了玩具工厂, 但是玩具工厂还没有生产出玩具), 说明 FactoryBean 中要创建的 Bean 还没有被加载, 也就得出: FactoryBean 生产Bean的机制是延迟生产

修改BeanTypeAnnoApplication的main方法, 添加获取玩具的方法

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
        Toy toy = ctx.getBean(Toy.class);
    }
}

控制台打印结果如下, 表明了也就是获取Bean的时候, Factory才会生产Bean

ToyFactoryBean 初始化了。。。
生产了一个ball

FactoryBean创建Bean的实例数

FactoryBean 接口中有一个默认的方法 isSingleton , 默认是 true , 代表默认是单实例的

调用两次getBean(), 判断两次对象是否一致

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
        Toy toy1 = ctx.getBean(Toy.class);
        Toy toy2 = ctx.getBean(Toy.class);
        // 打印结果: true
        System.out.println(toy1 == toy2);
    }
}
获取FactoryBean本体

Toy 本体去取, 取到的都是 FactoryBean 生产的 Bean 。一般情况下咱也用不到 FactoryBean 本体, 但如果真的需要取, 使用的方法也很简单: 要么直接传 FactoryBean 的 class (很容易理解), 也可以传 ID 。不过, 如果真的靠传ID的话, 传配置文件 / 配置类声明的 ID 就不好使了, 因为那样只会取出生产出来的 Bean

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);

        // 打印结果: com.linkedbear.spring.bean.a_type.bean.ToyFactoryBean@d29f28
        System.out.println(ctx.getBean("toyFactory"));
        
    }
}

FactoryBean 的方式, 需要在Bean的ID前面加&符号

public class BeanTypeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);

        // 打印结果: Toy{name='ball'}
        System.out.println(ctx.getBean("&toyFactory"));
        
    }
}
BeanFactory与FactoryBean的区别

BeanFactory: SpringFramework 中实现 IOC的最底层容器(此处的回答可以从两种角度出发: 从类的继承结构上看, 它是最顶级的接口, 也就是最顶层的容器实现;从类的组合结构上看, 它则是最深层次的容器, ApplicationContext 在最底层组合了 BeanFactory )

FactoryBean: 创建对象的工厂Bean, 可以使用它来直接创建一些初始化流程比较复杂的对象

Bean作用域

为什么会出现多种不同的作用域呢

肯定是它可以被使用的范围不同了。那为什么不都统一成一样的作用范围呢?说白了, 资源是有限的, 如果一个资源允许同时被多个地方访问(如全局常量), 那就可以把作用域提的很高;反之, 如果一个资源伴随着一个时效性强的, 带强状态的动作, 那这个作用域就应该局限于这一个动作, 不能被这个动作之外的干扰。

SpringFramework中内置的作用域

SpringFramework 中内置了 6 种作用域(5.x 版本):

作用域类型概述
singleton一个 IOC 容器中只有一个【默认值】
prototype每次获取创建一个
request一次请求创建一个(仅Web应用可用)
session一个会话创建一个(仅Web应用可用)
application一个 Web 应用创建一个(仅Web应用可用)
websocket一个 WebSocket 会话创建一个(仅Web应用可用)

划分为三类,

  1. singleton(Spring核心)
  2. peorotype(Spring核心)
  3. web应用领域: request, session, application, websocket

singleton

SpringFramework中默认所有的 Bean 都是单实例的, 即: 一个IOC容器中只有一个

前置准备

创建ChildToy共两个类

@Data
public class Child { 
    private Toy toy;
}

// Toy中标注@Component注解, 表示给Spring托管
@Component
public class Toy {}

创建配置类, 并注册两个Child类, 表示两个小孩

@Configuration
@ComponentScan("com.linkedbear.spring.bean.b_scope.bean")
public class BeanScopeConfiguration {
    
    @Bean
    public Child child1(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }
    
    @Bean
    public Child child2(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }
    
}
运行示例

获取其中的 Child , 打印里面的 Toy

public class BeanScopeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanScopeConfiguration.class);
        ctx.getBeansOfType(Child.class)
            .forEach((name, child) -> {
                System.out.println(name + ":" + child);
            });
			/*
			打印结果: 
			Toy constructor run ...
			child1:Child(toy=com.linkedbear.spring.bean.b_scope.bean.Toy@4b0d79fc)
			child2:Child(toy=com.linkedbear.spring.bean.b_scope.bean.Toy@4b0d79fc)
        */
    }

}

可以看到上述两个child的内存地址都一样, 证明默认情况下, Bean 的作用域是单实例的

prototype: 原型Bean

Spring官方的定义是: 每次对原型Bean提出请求时, 都会创建一个新的Bean实例。 这里面提到的"提出请求" , 包括任何依赖查找, 依赖注入的动作, 都算做一次"提出请求"

由此咱也可以总结一点: 如果连续 getBean() 两次, 那就应该创建两个不同的Bean实例;向两个不同的Bean中注入两次, 也应该注入两个不同的Bean实例

其实对于原型这个概念, 在设计模式中也是有对应的: 原型模式。原型模式实质上是使用对象深克隆, 乍看上去跟 SpringFramework 的原型 Bean 没什么区别, 但咱仔细想, 每一次生成的原型Bean本质上都还是一样的, 只是可能带一些特殊的状态等等, 这个可能理解起来比较抽象, 可以跟下面的 request 域结合着理解。

前置准备和运行示例

修改Toy 类: 给 Toy 的类上标注一个额外的注解: @Scope , 并声明为原型类型

@Component
@Scope("prototype")
// @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 这种写法也行, 建议使用常量
public class Toy {}

这个 prototype 不是随便写的常量, 而是在 ConfigurableBeanFactory 中定义好的常量

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
    String SCOPE_SINGLETON = "singleton";

    String SCOPE_PROTOTYPE = "prototype";

    // 省略其它代码
}

重新运行BeanScopeAnnoApplication类, 打印结果如下

child1:Child(toy=com.linkedbear.spring.bean.b_scope.bean.Toy@273e7444)
child2:Child(toy=com.linkedbear.spring.bean.b_scope.bean.Toy@7db12bb6)
原型Bean的创建时机

单实例 Bean 的创建咱已经知道, 是在 ApplicationContext 被初始化时就已经创建好了, 那这些原型 Bean 又是什么时候被创建的呢?其实也不难想出, 它都是什么时候需要, 什么时候创建

从以下角度时候, 原型Bean每次创建都是新, 即是初始化就创建也无济于事

修改Toy, 添加一个无参构造方法

@Component
@Scope("prototype")
public class Toy {
    public Toy() {
        System.out.println("Toy constructor run ...");
    }
}

修改BeanScopeAnnoApplication, 只让它扫描bean包(注意Toy类上要添加@Component注解), 不加载配置类, 这样就相当于只有一个Toy类被扫描进去了, Child 不会注册到 IOC 容器中

public class BeanScopeAnnoApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring.bean.b_scope.bean");
        // 控制台什么也没打印, 因为没有Toy的使用需求嘛, 它当然不会被创建
    }
}

Web应用领域的作用域

  • request: 请求Bean, 每次客户端向 Web 应用服务器发起一次请求, Web 服务器接收到请求后, 由 SpringFramework 生成一个 Bean , 直到请求结束
  • session: 会话Bean, 每个客户端在与 Web 应用服务器发起会话后, SpringFramework 会为之生成一个 Bean , 直到会话过期
  • application: 应用Bean, 每个 Web 应用在启动时, SpringFramework 会生成一个 Bean , 直到应用停止(有的也叫 global-session )
  • websocket: WebSocket Bean , 每个客户端在与 Web 应用服务器建立 WebSocket 长连接时, SpringFramework 会为之生成一个 Bean , 直到断开连接

Bean实例化方式

普通Bean实例化

所有<bean> 标签@Bean 注解的方式, 都是普通Bean的对象, 它们默认是单实例, 在 IOC 容器初始化时就已经被初始化了

借助FactoryBean创建Bean

普通Bean+FactoryBean
public class Ball {}

/*
Ball的FacotryBean
*/
public class BallFactoryBean implements FactoryBean<Ball> {

    /*
    返回Bean
    */
    @Override
    public Ball getObject() {
        return new Ball();
    }

    /*
    返回Bean的类型
    */
    @Override
    public Class<Ball> getObjectType() {
        return Ball.class;
    }
}

@Configuration
public class BeanConfiguration {
    /*
    注册BallFactoryBean
    */
    @Bean
    public BallFactoryBean ballFactoryBean() {
        return new BallFactoryBean();
    }
}

注册Bean时, 只需要注入FactoryBean , IOC容器会自动识别, 并默认在第一次获取时创建对应的Bean并缓存(针对默认的单实例 FactoryBean )

借助静态工厂创建Bean

测试示例

创建Bean+静态工厂

public class Car {    
    public Car() {
        System.out.println("Car constructor run ...");
    }
}

/*
静态类工厂
*/
public class CarStaticFactory {
    public static Car getCar() {
        return new Car();
    }
}

配置xml

静态工厂的使用通常运用于 xml 方式比较多(主要是注解驱动没有直接能让它起作用的注解, 编程式配置又可以直接调用, 显得没那么大必要)

<!--注册Car-->
<bean id="car1" class="com.linkedbear.spring.bean.c_instantiate.bean.Car"/>

<!--注册工厂类, 并指定创建对象的工厂方法-->
<bean id="car2" class="com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory" factory-method="getCar"/>

运行启动类进行测试

public class BeanInstantiateXmlApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("bean/bean-instantiate.xml");
        ctx.getBeansOfType(Car.class).forEach((beanName, car) -> {
            System.out.println(beanName + " : " + car);
        });
		/*
		控制台打印结果如下: 
		Car constructor run ...
		Car constructor run ...
		car1 : com.linkedbear.spring.bean.c_instantiate.bean.Car@5b239d7d
		car2 : com.linkedbear.spring.bean.c_instantiate.bean.Car@6572421
		*/
    }
}

即是创建两个Car, 一个Car的Bean, 一个CarStaticFactorygetCar()生产的Bean

静态工厂是否在IOC容器内

car2注册到IOC容器内了, 静态工厂是否会像FacotryBean一样注册到IOC容器中, 尝试从IOC容器中获取试试

public static void main(String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("bean/bean-instantiate.xml");
    ctx.getBeansOfType(Car.class).forEach((beanName, car) -> {
        System.out.println(beanName + " : " + car);
    });
    // 尝试取一下试试
    System.out.println(ctx.getBean(CarInstanceFactory.class));
}

运行, 发现程序抛出了 NoSuchBeanDefinitionException 异常:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory' available

结论: 静态工厂本身不会被注册到 IOC 容器中

编程式使用静态工厂

由于 SpringFramework 中并没有提供关于静态工厂相关的注解, 所以只能使用注解配置类+编程式使用静态工厂了, 而这个使用方式相当的简单

@Configuration
public class BeanConfiguration {    
    @Bean
    public Car car2() {
        // 直接 类.方法 调用即可
        return CarStaticFactory.getCar();
    }
}

借助实例工厂创建Beans

测试示例

创建实例工厂

创建一个CarInstanceFactory代表实例工厂, 它跟静态工厂唯一的区别是方法不再是static方法了

public class CarInstanceFactory {
    public Car getCar() {
        return new Car();
    }
}

配置xml

对于实例工厂, 要想调用对象的方法, 那自然得先把对象实例化才行了, 所以咱就需要先在 xml中注册实例工厂, 随后才能创建真正的目标 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="carInstanceFactory" class="com.linkedbear.spring.bean.c_instantiate.bean.CarInstanceFactory"/>
    <bean id="car3" factory-bean="carInstanceFactory" factory-method="getCar"/>

</beans>

发现car3<bean> 标签可以不传入class属性, 用 factory-beanfactory-method 属性也可以完成Bean的创建

运行启动类

public class BeanInstantiateXmlApplication {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("bean/bean-instantiate.xml");
        ctx.getBeansOfType(Car.class).forEach((beanName, car) -> {
            System.out.println(beanName + " : " + car);
        });
    }
}
实例工厂是否在IOC容器中

在, 实例工厂的<bean>中都注册了, 那么必然是在IOC容器当中的

编程式使用实例工厂
@Configuration
public class BeanConfiguration {      
    @Bean
    public Car car3(CarInstanceFactory carInstanceFactory) {
        return carInstanceFactory.getCar();
    }
}

Bean生命周期(简易版)

Bean生命周期阶段划分

一个对象从被创建, 到被垃圾回收, 可以宏观的划分为 5 个阶段:

  • 创建/实例化阶段: 此时会调用类的构造方法, 产生一个新的对象
  • 初始化阶段: 此时对象已经创建好, 但还没有被正式使用, 可能这里面需要做一些额外的操作(如预初始化数据库的连接池)
  • 运行使用期: 此时对象已经完全初始化好, 程序正常运行, 对象被使用
  • 销毁阶段: 此时对象准备被销毁, 已不再使用, 需要预先的把自身占用的资源等处理好(如关闭, 释放数据库连接)
  • 回收阶段: 此时对象已经完全没有被引用了, 被垃圾回收器回收

Spring能干预的生命周期阶段(针对单例Bean)

生命周期阶段是否能干预
创建/实例化
初始化
运行使用
销毁
回收

SpringFramework如何能让我们干预Bean的初始化和销毁

Servlet里面有两个方法, 分别叫initdestroy, 被Web容器(Tomcat等)调用的吧, 用来初始化和销毁Servlet的

方法的设计思想其实就是回调机制, 它都不是自己设计的, 而是由父类/接口定义好的, 由第三者(框架, 容器等)来调用

生命周期的触发, 更适合叫回调, 因为生命周期方法是咱定义的, 但方法被调用, 是框架内部帮我们调的, 那也就可以称之为"回调"了

init-method&desotry-method

Bean的初始化和销毁阶段做一个额外操作

前置准备
创建Cat和Dog类
@Data
public class Cat {
    private String name;
    
    public void init() {
        System.out.println(name + "被初始化了。。。");
    }
    public void destroy() {
        System.out.println(name + "被销毁了。。。");
    }
}
@Data
public class Dog {    
    private String name;
    
    public void init() {
        System.out.println(name + "被初始化了。。。");
    }
    public void destroy() {
        System.out.println(name + "被销毁了。。。");
    }
}
创建xml配置文件/配置类

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="com.linkedbear.spring.lifecycle.a_initmethod.bean.Cat"
          init-method="init" destroy-method="destroy">
        <property name="name" value="mimi"/>
    </bean>
</beans>
  • init-method: 指向初始化方法
  • destroy-method: 执行销毁方法

配置类

@Configuration
public class InitMethodConfiguration {
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Dog dog() {
        Dog dog = new Dog();
        dog.setName("wangwang");
        return dog;
    }
    
}

initMethod和destroyMethod的作用的同理

初始化和销毁方法的特征

  • 方法访问权限无限制要求(SpringFramework底层会反射调用的)
  • 方法无参数(如果真的设置了参数, SpringFramework也不知道传什么进去)
  • 方法无返回值(返回给 SpringFramework 也没有意义)
  • 可以抛出异常(异常不由自己处理, 交予SpringFramework可以打断Bean的初始化/销毁步骤)
运行示例

分别初始化xml和注解驱动的容器, 不过需要注意的是, 这次咱接收的类型不再用 ApplicationContext , 而是用实现类本身, 目的是为了调用 close 方法对容器进行关闭, 以触发 Bean 的销毁动作

xml驱动
public class InitMethodXmlApplication {

    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("lifecycle/bean-initmethod.xml");
        System.out.println("IOC容器初始化完成。。。");

        System.out.println();

        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");

        /*
			控制台输出结果如下:
			Cat 构造方法执行了。。。
			setName方法执行了。。。
			mimi被初始化了。。。
			IOC容器初始化完成。。。

			准备销毁IOC容器。。。
			mimi被销毁了。。。
			IOC容器销毁完成。。。
        */
    }
}

结论:

  • 在 IOC 容器初始化之前, 默认情况下Bean已经创建好了, 而且完成了初始化动作;容器调用销毁动作时, 先销毁所有Bean, 最后IOC容器全部销毁完成
  • Bean 的生命周期中, 是先对属性赋值, 后执行 init-method 标记的方法

initMethod和destroyMethod方法作用范围如下图
在这里插入图片描述

注解驱动
public class InitMethodAnnoApplication {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(InitMethodConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
        Thread.sleep(3000);
        ctx.close();
    }
}

控制台打印结果如下

wangwang被初始化了。。。
IOC容器初始化完成。。。
wangwang被销毁了。。。
JSR250规范: @PostConstruct和@PreDestroy

之前手动声明注册的Bean, 对于那些使用模式注解的Bean, 没有可以让你声明 init-methoddestroy-method 的地方了, @Component 注解上也只有一个 value 属性而已

JSR250 规范中除了有 @Resource 这样的自动注入注解, 还有负责生命周期的注解, 包括 @PostConstruct , @PreDestroy 两个注解, 分别对应init-methoddestroy-method

使用示例

创建Bean

钢笔与墨水, 刚买来的动作代表实例化, 加墨水的动作代表初始化, 倒掉所有墨水的动作代表销毁

对于JSR250规范的这两个注解的使用, 直接标注在Bean的方法上即可

@Data
@Component
public class Pen {
    /*
    墨水含量
    */
    private Integer ink;
    
    /*
    添加墨水
    */
    @PostConstruct // 实现了initMethod相似的作用, 方法声明一样的, 修饰符可以是private
    public void addInk() {
        System.out.println("钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    /*
    倒掉所有墨水
    */
    @PreDestroy // 实现了destroyMethod相似的作用, 方法声明一样的, 修饰符可以是private
    public void outwellInk() {
        System.out.println("钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
}

启动

public class JSR250AnnoApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JSR250Configuration.class);
        System.out.println("IOC容器初始化完成。。。");
        
        System.out.println();
        
        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
        /*
			控制台打印结果如下: 
			准备初始化IOC容器。。。
			@PostConstruct - 钢笔中已加满墨水。。。
			init-method - 打开钢笔。。。
			IOC容器初始化完成。。。

			准备销毁IOC容器。。。
			@PreDestroy - 钢笔中的墨水都放干净了。。。
			destory-method - 合上钢笔。。。
			IOC容器销毁完成。。。
        */
    }
}

如下图
在这里插入图片描述

JSR250规范与init-method共存

如果不使用@Component注解来注册Bean而转用<bean>/@Bean的方式, 那@PostConstruct@PreDestroy注解是可以与init-method/destroy-method共存的

前期准备

新建一个Pen2, 方法如下

@Data
@Component
public class Pen2 {
    /**
     * 墨水含量
     */
    private Integer ink;

    public void open() {
        System.out.println("init-method - 打开钢笔。。。");
    }
    
    public void close() {
        System.out.println("destory-method - 合上钢笔。。。");
    }
    
    @PostConstruct // JSR250规范
    public void addInk() {
        System.out.println("@PostConstruct - 钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    @PreDestroy // JSR250规范
    public void outwellInk() {
        System.out.println("@PreDestroy - 钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
}

配置类注册Pen2, 并使用指定initMethod和destroyMethod

@Configuration
public class JSR250Configuration {
    @Bean(initMethod = "open", destroyMethod = "close")
    public Pen2 pen() {
        return new Pen2();
    }
}
运行示例
public class JSR250AnnoApplication {
    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JSR250Configuration.class);
        System.out.println("IOC容器初始化完成。。。");

        System.out.println();

        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
        /*
        控制条打印结果: 
        准备初始化IOC容器。。。
        @PostConstruct - 钢笔中已加满墨水。。。
        init-method - 打开钢笔。。。
        IOC容器初始化完成。。。

        准备销毁IOC容器。。。
        @PreDestroy - 钢笔中的墨水都放干净了。。。
        destory-method - 合上钢笔。。。
        IOC容器销毁完成。。。
        */
    }
}

结论: JSR250规范的执行优先级高于init/destroy

如下图
在这里插入图片描述

InitializingBean&DisposableBean

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

前置准备

使用Pen作为演示对象, 实现InitializingBean和DisposableBean接口

@Data
@Component
public class Pen implements InitializingBean, DisposableBean {
    /*
    墨水含量
    */
    private Integer ink;

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

    @Override
    public void destroy() throws Exception {
        System.out.println("钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
}

配置类不变

@Configuration
public class InitializingDisposableConfiguration {   
    @Bean(initMethod = "open", destroyMethod = "close")
    public Pen3 pen() {
        return new Pen3();
    }
}
运行示例
public class InitializingDisposableAnnoApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring.lifecycle.c_initializingbean.bean");
        System.out.println("IOC容器初始化完成。。。");

        System.out.println();

        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
        /*
        控制台打印结果:
        准备初始化IOC容器。。。
        钢笔中已加满墨水。。。
        IOC容器初始化完成。。。

        准备销毁IOC容器。。。
        钢笔中的墨水都放干净了。。。
        IOC容器销毁完成。。。
        */
    }
}
三种生命周期并存

复制出一个 Pen 来, 命名为 Pen3 , 并同时实现三种生命周期的控制, Servelt的init/destory, JSR250规范的@PostConstruct和@PreDestroy, Spring的InitializingBean和DisposableBean

@Data
public class Pen3 implements InitializingBean, DisposableBean {
    private Integer ink;
    
    public void open() {
        System.out.println("init-method - 打开钢笔。。。");
    }
    
    public void close() {
        System.out.println("destroy-method - 合上钢笔。。。");
    }
    
    @PostConstruct // JSR250规范
    public void addInk() {
        System.out.println("@PostConstruct - 钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    @PreDestroy // JSR250规范
    public void outwellInk() {
        System.out.println("@PreDestroy - 钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
    
    // Spring
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean - 准备写字。。。");
    }
    
    // Spring
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean - 写完字了。。。");
    }
}

注解驱动方式注册这个 Pen3

@Configuration
public class InitializingDisposableConfiguration {
    // Sevlet
    @Bean(initMethod = "open", destroyMethod = "close")
    public Pen3 pen() {
        return new Pen3();
    }
}
public class InitializingDisposableAnnoApplication {
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(InitializingDisposableConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
        System.out.println();
        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
        /*
        控制台打印: 
        准备初始化IOC容器。。。
        @PostConstruct - 钢笔中已加满墨水。。。
        InitializingBean - 准备写字。。。
        init-method - 打开钢笔。。。
        IOC容器初始化完成。。。

        准备销毁IOC容器。。。
        @PreDestroy - 钢笔中的墨水都放干净了。。。
        DisposableBean - 写完字了。。。
        destroy-method - 合上钢笔。。。
        IOC容器销毁完成。。。
        */
    }
}

执行顺序才是最关键的:@PostConstructInitializingBeaninit-method

如下图
在这里插入图片描述

prototype(原型)Bean的生命周期

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

前置准备

Pen类

@Data
public class Pen implements InitializingBean, DisposableBean {
    
    private Integer ink;
    
    public void open() {
        System.out.println("init-method - 打开钢笔。。。");
    }
    
    public void off() {
        System.out.println("destroy-method - 合上钢笔。。。");
    }
    
    @PostConstruct
    public void addInk() {
        System.out.println("@PostConstruct - 钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    @PreDestroy
    public void outwellInk() {
        System.out.println("@PreDestroy - 钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean - 准备写字。。。");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean - 写完字了。。。");
    }
}

配置类

@Configuration
public class PrototypeLifecycleConfiguration {
    @Bean(initMethod = "open", destroyMethod = "off")
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 注意这里的Bean范围是prototype
    public Pen pen() {
        return new Pen();
    }
}
IOC容器初始化时原型Bean不初始化
public class PrototypeLifecycleApplication {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                PrototypeLifecycleConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
    }
}

控制台只打印了 IOC容器初始化完成。。。 表明了原型 Bean 的创建不随 IOC 的初始化而创建

原型Bean的初始化动作与单实例Bean一致

修改main方法, 获取一次Pen实例

public class PrototypeLifecycleAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                PrototypeLifecycleConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
        System.out.println();
        System.out.println("准备获取一个Pen。。。");
        Pen pen = ctx.getBean(Pen.class);
        /*
        控制台打印结果如下: 
        IOC容器初始化完成。。。

        准备获取一个Pen。。。
        @PostConstruct - 钢笔中已加满墨水。。。
        InitializingBean - 准备写字。。。
        init-method - 打开钢笔。。。
        */
    }
}

三种初始化的动作都执行了, 证明原型Bean的初始化动作与单实例Bean完全一致, 注意这里强调了是初始化动作

原型Bean的销毁不包括destroy-method

main 方法中再添加几行代码, 将这个 Pen 销毁掉

public class PrototypeLifecycleAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                PrototypeLifecycleConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
        System.out.println();
        System.out.println("准备获取一个Pen。。。");
        Pen pen = ctx.getBean(Pen.class);
        System.out.println("已经取到了Pen。。。");
        System.out.println();
        System.out.println("用完Pen了, 准备销毁。。。");
        ctx.getBeanFactory().destroyBean(pen);
        System.out.println("Pen销毁完成。。。");
        /*
        控制台打印结果如下: 
        IOC容器初始化完成。。。

        准备获取一个Pen。。。
        @PostConstruct - 钢笔中已加满墨水。。。
        InitializingBean - 准备写字。。。
        init-method - 打开钢笔。。。
        已经取到了Pen。。。

        用完Pen了, 准备销毁。。。
        @PreDestroy - 钢笔中的墨水都放干净了。。。
        DisposableBean - 写完字了。。。
        Pen销毁完成。。。
        */
    }
}

控制台中只打印了 @PreDestroy 注解和 DisposableBean 接口的执行, 没有触发 destroy-method 的执行

原型Bean在销毁时不处理destroy-method标注的方法, 可以从以下角度理解, 由于destroy-method方法是Servlet的,原型Bean不知道是否还继续生产, 所以Servlet容器不能销毁

SpringFramework中控制Bean生命周期的三种方式

init-method & destroy-method@PostConstruct & @PreDestroyInitializingBean & DisposableBean
执行顺序最后最先中间
组件耦合度无侵入(只在 <bean>@Bean 中使用)与 JSR 规范耦合与 SpringFramework 耦合
容器支持xml 、注解原生支持注解原生支持, xml需开启注解驱动xml 、注解原生支持
单实例Bean
原型Bean只支持 init-method

如下图
在这里插入图片描述

参考资料

从 0 开始深入学习 Spring

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/654096.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Postgresql源码(134)优化器针对volatile函数的排序优化分析

相关 《Postgresql源码&#xff08;133&#xff09;优化器动态规划生成连接路径的实例分析》 上一篇对路径的生成进行了分析&#xff0c;通过make_one_rel最终拿到了一个带着路径的RelOptInfo。本篇针对带volatile函数的排序场景继续分析subquery_planner的后续流程。 subquer…

国内信创web中间件生态

国内信创web中间件生态 东方通 官网https://www.tongtech.com/pctype/25.html 宝蓝德 官网https://www.bessystem.com/product/0ad9b8c4d6af462b8d15723a5f25a87d/info?p101 金蝶天燕 官网 https://www.apusic.com/list-117.html 中创 官网http://www.inforbus.com…

小程序使用vant组件库

一:下载组件库 在小程序内npm下载的包 vant组件库官网:快速上手 - Vant Weapp (youzan.github.io) 1)首先有有package.json文件,没有的话则先初始化 即使通过package.json去下载包,也需要有,可以观察下载的包. 2)下载包 3)构建npm包 下载包之后存储在node_modules内,但是我们…

详谈 Java中的list.forEach()和list.stream().forEach() 异同点

涉及的文章链接&#xff1a;ArrayList 循环Remove遇到的坑 一、想总结本篇博客的原因 在日常开发中&#xff0c;需要对集合数据进行或多或少的赋值修改&#xff0c;那么循环赋值或者做一些处理就是最常见的一种操作了&#xff0c;但是用习惯了stream流&#xff0c;所以在循环的…

HQChart使用教程99-K线窗口设置上下间距

HQChart使用教程99-K线窗口设置上下预留间距 指标窗口布局说明设置预留间距数据结构通过Setoption设置通过ChangeIndex设置 HQChart代码地址 指标窗口布局说明 顶部预留间距(3)和底部预留间距(5) 这个部分是算在Y轴坐标上的 设置预留间距 数据结构 HorizontalReserved&#…

Python + adb 实现打电话功能

前言 其实很多年前写过一篇python打电话的功能&#xff0c;链接如下&#xff1a; Python twilio 实现打电话和发短信功能_自动发短信代码-CSDN博客 今天由于工作需要&#xff0c;又用python写了个关于打电话的小工具&#xff0c;主要是通过ADB方式实现的 实现过程 1.先利用…

车机壁纸生成解决方案,定制化服务,满足个性化需求

在数字化与智能化浪潮的推动下&#xff0c;汽车内部设计已不再仅仅满足于基本功能的需求&#xff0c;更追求为用户带来前所未有的视觉享受与沉浸式体验。美摄科技&#xff0c;凭借其在图像生成与处理领域的深厚积累&#xff0c;推出了一款创新的车机壁纸生成解决方案&#xff0…

修改Windows系统hosts文件,解决GitHub国内访问速度慢甚至无法访问的问题

对国内大多数用户&#xff0c;GitHub的访问速度非常慢&#xff0c;甚至是打不开&#xff0c;无法访问。究其原因&#xff0c;多数是GitHub的CDN域名解析&#xff08;DNS&#xff09;遭到了污染或拦截。本文以Windows 10系统为例&#xff0c;通过修改本地hosts文件&#xff0c;解…

电脑可以录音吗?这里有你想要的答案!

在数字化时代&#xff0c;电脑已经成为我们日常生活中不可或缺的工具。除了办公、娱乐等基本功能外&#xff0c;电脑还具备许多实用的辅助功能&#xff0c;其中之一就是录音功能。可是电脑可以录音吗&#xff1f;本文将介绍两种在电脑上录音的方法&#xff0c;希望通过本文的介…

人生二选一:央企就业?美国做博士后?—请看她的抉择

一位30岁的女博士&#xff0c;收到国内央企和德国、美国的博士后邀请函&#xff0c;她该如何选择&#xff1f;知识人网小编推荐这篇文章&#xff0c;为大家解开谜题的同时&#xff0c;也给有同样纠结的学者提供一些启迪。 去年12月底的一个晚上&#xff0c;我收到美国一所高校发…

ubuntu系统开启ssh密码登录

文章目录 前言 一、确认否有ssh服务 二、修改/etc/ssh/sshd_config配置文件 三、重启ssh服务 总结 前言 安装好ubuntu系统后&#xff0c;默认是无法通过密码远程shell连接的&#xff0c;需要修改配置文件。 一、确认否有ssh服务 我这边使用的是ubuntu 22.04 LTS的系统&a…

AirBnb架构简史

2007 年&#xff0c;布莱恩切斯基 (Brian Chesky) 和乔加比亚 (Joe Gabbia) 搬到了旧金山。他们一边想为自己的创业想法筹集资金&#xff0c;一边又需要支付房租。 碰巧的是&#xff0c;当时城里正要举行一个设计会议&#xff0c;这意味着很多设计师都会寻找住处。他们想出了在…

海外仓系统要多少钱?最贵的未必是最好的,性价比高的才是

海外仓系统可以说已经是现在海外仓管理不可或缺的重要工具&#xff0c;然而&#xff0c;很多海外仓企业在选择海外仓系统时最头疼的问题就是不知道到底多少钱才合适。 确实&#xff0c;现在的海外仓系统市场价格体系非常多&#xff0c;几万几十万各种定价都有&#xff0c;让人…

linux父进程fork出子进程后,子进程为何首先需要close文件描述符。

在linux c/c编程时&#xff0c;父进程fork出子进程后&#xff0c;子进程经常第一件事就是close掉所有的文件描述符&#xff1b;为何需要这样做&#xff0c;本文用一个例子进行简单说明。 考虑到一种情况&#xff0c;父进程创建了tcp服务端套接字&#xff0c;并且listen&#x…

redis核心面试题二(实战优化)

文章目录 10. redis配置mysql实战优化[重要]11. redis之缓存击穿、缓存穿透、缓存雪崩12. redis实现分布式session 10. redis配置mysql实战优化[重要] // 最初实现OverrideTransactionalpublic Product createProduct(Product product) {productRepo.saveAndFlush(product);je…

ProxySQL路由策略实现读写分离

目的&#xff1a;配置proxysql路由策略后将不同用户的不同请求路由到不同的节点&#xff0c;实现读写分离 前提条件&#xff1a; 配置表mysql_replication_hostgroups&#xff0c;10为写组&#xff0c;20为读组 mysql_users表中已添加用户writer用户加入10写组&#xff0c;rea…

linux开发之设备树基本语法二

设备树特殊节点,对节点定义别名,chosen节点用来uboot给内核传参 上面的mmc0就是sdmmc0节点的别名 device_type属性 只对cpu节点和memory节点进行描述 自定义属性 这部分自定义,比如定义管脚标号,初始数值等 为什么我们可以在设备树上自己定义属性呢?设备树文件描述的是硬…

AI手语研究数据集;视频转视频翻译和风格化功能如黏土动画;AI检测猫咪行为;开放源码的AI驱动搜索引擎Perplexica

✨ 1: Prompt2Sign 多语言手语数据集&#xff0c;便捷高效用于手语研究。 Prompt2Sign 是一个全面的多语言手语数据集&#xff0c;旨在通过工具自动获取和处理网络上的手语视频。该数据集具有高效、轻量的特点&#xff0c;旨在减少先前手语数据集的不足之处。该数据集目前包含…

Python---Matplotlib(2万字总结)【从入门到掌握】

数据可视化 在完成了对数据的透视之后&#xff0c;可以将数据透视的结果通过可视化的方式呈现出来&#xff0c;简单的说&#xff0c;就是将数据变成漂亮的图表&#xff0c;因为人类对颜色和形状会更加敏感&#xff0c;然后再进一步解读数据背后隐藏的价值。在之前的文章中已经…

gitlab push 代码,密码正确,仍然提示HTTP Basic: Access denied. The provided password

HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password gitlab 登录账户密码确认正确&#xff0c;登录获取代码仍然提示以上问题&#xff0c;解决方案 …