Spring基础专题——第四章(控制Spring创建对象的次数+对象生命周期)

前言:去年到现在一直没有很好的时间完成这个spring基础+源码的博客目标,去年一年比较懒吧,所以今年我希望我的知识可以分享给正在奋斗中的互联网开发人员,以及未来想往架构师上走的道友们我们一起进步,从一个互联网职场小白到一个沪漂湿人,一路让我知道分享是一件多么重要的事情,总之不对的地方,多多指出,我们一起徜徉代码的海洋!

 

我这里做每个章节去说的前提,不是一定很标准的套用一些官方名词,目的是为了让大家可以理解更加清楚,如果形容的不恰当,可以留言出来,万分感激

1、如何控制简单对象的创建次数

上节我们提到了,我们要是创建复杂对象,需要实现了FactoryBean接口,如果isSingleton返回为true,说明只需要创建一次,如果为false,就需要创建多次复杂对象,每个对象都不同!

我们定义一个Account类并在xml中这么写

<!-- 如果我们想对这个简单对象,控制次数,加个scope标签,为singleton,那么这个scope对象只会被创建一次-->
    <bean id="account" scope="singleton" class="com.chenxin.spring5.scope.Account"></bean>

如果scope为singleton,说明只创建一次,这个标签默认不写,就是默认只创建一次,所以Spring默认给我们的对象创建都是singleton(单例),如果是prototype,则是创建多次不同的对象

 <!-- 如果我们想对这个简单对象,控制次数,加个scope标签并且等于prototype,那么这个scope对象只会被创建多次-->
    <bean id="account" scope="prototype" class="com.chenxin.spring5.scope.Account"></bean>

2、如何控制复杂对象创建次数

我们要是创建复杂对象,需要实现了FactoryBean接口,如果isSingleton返回为true,说明只需要创建一次,如果为false,就需要创建多次复杂对象

那会有同学问,如果是实例工厂或者静态工厂,他们没实现isSingleton方法,他们其实还是以scope的方式来控制对象的创建次数!

3、为什么我们要控制对象创建次数

因为有些对象能被共用,那么就只需要创建一次就可以了;有些对象不能被大家共用,所以我们要把这个对象管理起来,根据这个对象的自身特点,来进行创建条件

好处:节省不必要的资源浪费

  • 什么样的对象只需要创建一次

1、SqlSessionFactory

2、DAO(因为你插入数据,我也插入数据,我们都是insert方法,我们没有区别)

3、Service(你登录我也要登录,我们之间的差异是在用户名或者其他在入参的不同,但是方法都是同一个,做一样的事情)

  • 什么样的对象需要创建新的

1、Connection

2、Sqlsession、Session会话

3、Struts2 Action

4、对象的生命周期

1、什么是对象的什么周期

指的是一个对象的创建,存活,消亡的一个完整过程

2、为什么要学习对象的生命周期

我们知道一开始对于对象的创建, 是通过new来创建的,这个对象在一直被引用的情况下,会一直存活在虚拟机中,由我们代码和虚拟机一起,管理这个对象的生命周期

那么今天讲的这个对象的存活和消亡过程,不是由我们来控制了,而是交给Spring去控制,如果我们更加了解对象的生命周期,实际上我们会更好利用Spring为我们创建好的对象来为我们做事情

3、生命周期的三个阶段

  • 创建阶段

Spring工厂何时帮我们创建对象?分情况

1、如果对象只被创建一次,也就是scope="singleton"的时候,Spring工厂创建的同时,对象会同时创建出来

2、如果对象被创建多次,也就是scope="prototype"的时候,Spring工厂会获取对象(ctx.getBean)的同时,创建对象

如果被创建一次:

<bean id="account" class="com.chenxin.spring5.Account">

    </bean>

实体类

public class Account {

    private Integer id;
    private String name;

    public Account() {
        System.out.println("Account.Account");
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

 测试,如果scope="singleton"或者bean这个scope标签不写的时候,默认对象只创建一次,那么在工厂被new出来的时候,一定是会调用构造方法进行对象的创建的

    @Test
    public void test1(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
//        Account account = (Account) ctx.getBean("account");
//        System.out.println("account = " + account);
    }
Account.Account

 如果是scope="prototype",那么对象会创建多次,则会在getBean的时候,工厂才会帮我们把对象创建。

  <bean id="account" scope="prototype" class="com.chenxin.spring5.Account">

    </bean>
    @Test
    public void test1(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Account account = (Account) ctx.getBean("account");
//        System.out.println("account = " + account);
    }

 所以可以很清楚了解到工厂何时帮我们创建对象的情况。

那加入我此时,scope="singleton",但是我不想在工厂创建的时候,帮我们创建,对象,我需要在getBean的时候,来创建对象,这回怎么搞呢?

那我们这么来,我们只需要加个标签lazy-init="true",表示懒加载

<bean id="account" scope="prototype" class="com.chenxin.spring5.Account" lazy-init="true">

    </bean>

这样的话我们就可以在singleton前提下,再调用getBean时候, 工厂才会开始帮我们创建对象!

  • 初始化阶段

什么是初始化阶段呢?

指的是Spring工厂在创建完对象后,调用对象的初始化方法,完成对应的初始化操作!

1、初始化谁来提供呢:程序员根据需求,提供初始化方法,完成初始化操作。

2、初始化方法是谁调用:Spring工厂来进行调用

Spring为初始化提供了两种途径:

1、InitializingBean接口:需要实现InitializingBean的afterPropertiesSet()这个方法,完成初始化操作的代码,可以写在这个方法中;因为你实现的是Spring接口,所以Spring就可以找到你实现的接口,并且可以找到这个方法;

public class Product implements InitializingBean {

    public Product() {
        System.out.println("Product.Product");
    }

    /*
    *  这个就是初始化方法:做一些初始化的操作
    * Spring会进行调用
    */
    public void afterPropertiesSet() throws Exception {
        System.out.println("Product.afterPropertiesSet");
    }
}
<bean id="product" class="com.chenxin.spring5.Product"></bean>
    @Test
    public void test2(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
    }

 所以输出结果是先创建对象,调用了构造方法,Spring再进行对象的初始化,发现要初始化的时候,我实现了这个InitializingBean接口的afterPropertiesSet方法,进而会帮我们调用初始化方法进行初始化

但是这里有个问题,看过上一章节的同学可以知道,这个InitializingBean类似FactoryBean接口,都是耦合了Spring的框架,对代码扩展性很不好,如果我离开了Spring框架,也就以为着这些初始化代码,就无法可以继续使用了,所以我们对初始化为了灵活,我们会有另一个策略;

2、类中提供一个普通的方法

public class Cat {

    public Cat() {
        System.out.println("Cat.Cat");
    }

    /**
     * 提供一个普通方法
     */
    public void myInit(){
        System.out.println("Cat.myInit");
    }
}

要想让Spring知道你这个初始化方法的调用,是不是只能在配置文件里告知Spring你要调用我这个初始化方法,这样我可以不用你的InitializingBean接口了,即便后期你换了其他的框架,你代码依旧是可以用,起码不会报错can not find InitializingBean

   <bean id="cat" class="com.chenxin.spring5.Cat" init-method="myInit"></bean>
Cat.Cat
Cat.myInit

 这样就可以更加灵活控制对象的初始化了!

细节分析:

如果一个对象既实现了InitializingBean,同时又提供了普通的初始化方法myInit(),执行顺序是什么呢?我们测试下

public class Product implements InitializingBean {//不仅实现了InitializingBean

    public Product() {
        System.out.println("Product.Product");
    }

    //还提供了普通的初始化方法
    public void myInit(){
        System.out.println("Product.myInit");
    }
    /*
    *  这个就是初始化方法:做一些初始化的操作
    * Spring会进行调用
    */
    public void afterPropertiesSet() throws Exception {
        System.out.println("Product.afterPropertiesSet");
    }
}

配置文件也做了初始化方法的指向

<bean id="product" class="com.chenxin.spring5.Product" init-method="myInit"></bean>

结果是:

Product.Product
Product.afterPropertiesSet
Product.myInit

 很明显,afterPropertiesSet初始化执行在自定义初始化myInit前;

所以我们知道对象在创建完后,会调用初始化方法;此时我们还忽略了一些细节,如果这个时候需要注入属性,那你说这个注入操作,又是应该在对象创建之后,是先进行注入呢,还是先进行初始化操作呢?

我们测试下

public class Product implements InitializingBean {

    private String name;

    public void setName(String name) {
        System.out.println("Product.setName");
        this.name = name;
    }

    public Product() {
        System.out.println("Product.Product");
    }


    public void myInit(){
        System.out.println("Product.myInit");
    }
    /*
    *  这个就是初始化方法:做一些初始化的操作
    * Spring会进行调用
    */
    public void afterPropertiesSet() throws Exception {
        System.out.println("Product.afterPropertiesSet");
    }
}
<bean id="product" class="com.chenxin.spring5.Product" init-method="myInit">
        <property name="name" value="chenxin"></property>
    </bean>

我们代码在set的方法加了一句话setName

Product.Product
Product.setName
Product.afterPropertiesSet
Product.myInit

 所以很明显,set注入是在创建对象后,初始化之前,所以是创建对象->属性注入->初始化

其实,你发现这个初始化接口afterPropertiesSet这个方法的含义就是在属性set注入后,也表达了初始化是在set注入之前!!

那什么是初始化操作呢?

实际上是对资源的初始化,数据库资源,io资源,网络等...所以这些功能一般情况,会在这个afterPropertiesSet里面写

后面其实应用初始化的阶段,还是比较少的!

  • 销毁阶段

Spring销毁对象前,会调用对象的销毁方法,来完成销毁操作

1、Spring在什么时候销毁所创建的处对象呢?

ctx.close();意味着工厂关闭

销毁方法是程序员根据自己的需求,定义销毁方法,由Spring工厂完成调用!

那Spring给我们提供了哪些方法可以将对象进行销毁呢?(其实和初始化是很像的)

1、实现DisposableBean接口

public class Product implements InitializingBean, DisposableBean {

    private String name;

    public void setName(String name) {
        System.out.println("Product.setName");
        this.name = name;
    }

    public Product() {
        System.out.println("Product.Product");
    }


    public void myInit(){
        System.out.println("Product.myInit");
    }
    /*
    *  这个就是初始化方法:做一些初始化的操作
    * Spring会进行调用
    */
    public void afterPropertiesSet() throws Exception {
        System.out.println("Product.afterPropertiesSet");
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                '}';
    }

    /**
     * 实现销毁方法,资源释放的操作
     * @throws Exception
     */
    public void destroy() throws Exception {
        System.out.println("Product.destroy");
    }
}
    @Test
    public void test3(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Product product = (Product) ctx.getBean("product");
        System.out.println("product = " + product);
    }

 结果你会发现,对象创建了,注入了,初始化了,但是怎么没销毁?

Product.Product
Product.setName
Product.afterPropertiesSet
Product.myInit

因为对象的销毁发现在工厂关闭的时候,所以需要ctx.close(),因为close方法是在ClassPathXmlApplicationContext的父类AbstractApplicatonContext里实现的,在ApplicationContext接口没有,所以我们要改下

    @Test
    public void test3(){
//        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Product product = (Product) ctx.getBean("product");
        System.out.println("product = " + product);
        ctx.close();
    }

结果就可以打印出销毁方法了!

2、定义一个普通方法

我们还可以在Product中定义一个销毁方法,叫做myDestory():

public void myDestory(){
        System.out.println("Product.myDestory");
    }
<bean id="product" class="com.chenxin.spring5.Product" init-method="myInit" destroy-method="myDestory">
        <property name="name" value="chenxin"></property>
    </bean>

同理打印顺序也是先Spring自家人,再自定义!

Product.Product
Product.setName
Product.afterPropertiesSet
Product.myInit
product = Product{name='chenxin'}
Product.destroy
Product.myDestory

细节分析:

销毁方法的操作只适用于scope="singleton"的方法,如果为prototype是不起任何作用的;因为对象每次都会创建新的,不知道到底你需要销毁哪一个,所以我理解为Spring工厂索性就不帮你销毁了。

实际上销毁操作在开发中用的也非常少,所以大家了解下就可以了

对象生命周期总结下一张图:

1、首先Spring工厂创建出来,检查你bean标签是否是单例对象还是非单例,然后再根据不同的时机(是否懒加载)开始调用对象的构造方法,反射来创建对象

2、创建后进行DI操作,属性注入

3、然后开始进行初始化方法的调用

4、关闭工厂,调用销毁方法

5、配置文件参数化

什么是配置文件参数化?指的是把Spring配置文件中需要经常修改的字符串信息,转移到一个更小的配置文件中!

我先提出问题:

Spring配置文件中,存在经常需要修改的字符串吗?

当然存在,比如数据库连接相关的参数,在Spring早期的时候,在applicationContext.xml中配置长达几千行,如果关于数据库的某个配置,在其中的某一段代码,对于开发人员,是不是不容易知道这个情况?

好吧如果你说开发都不知道还有谁知道,那我就会觉得你接触的业务少了,格局小啦,如果这个是给客户的产品呢,客户要修改某个配置,你让他去找这些配置文件,找到具体某个配置,说我要换个数据库配置信息在哪里?

或者交给运维去修改,运维不懂Spring,你说他一旦改错了,坑的是谁,还是不开发吗?所以能不能把这个会修改的字符串,转移到一个小的配置文件中,这样的话,是不是很方便修改和维护呢?

所以我们为了方便,我们就单独拉出一个小的配置文件(db.properties)专门存放这个配置信息,这个文件你可以随便放,我这里放在了resources下

jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/users?useSSL=false
jdbc.username = root
jdbc.password = root

那你说Spring是怎么能找到你这个配置文件呢?

我们要在xml中写这么一段

<context:property-placeholder location="classpath:/db.properties"></context:property-placeholder>
<context:property-placeholder location="classpath:/db.properties"></context:property-placeholder>

    <bean id="conn" class="com.chenxin.spring5.factorybean.ConnectionFactoryBean">
        <property name="driverName" value="${jdbc.driverClassName}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

这样就完成了配置文件参数化的操作!后续我们想改相关的配置参数,就不需要在Spring的配置文件里改了,对吧?

6、类型转换器

我们之前讲过通过Spring配置文件,进行属性注入,我们对于Integer属性的值,为什么我们可以通过字符串的<value></value>标签的注入方式,来注入到Integer的属性中呢?

其实Spring在底层帮我们完成了类型转换,实际上就是Spring的类型转换器(Converter接口),因为会涉及到多种类型的转换,所以定义成接口屏蔽这个差异

所以Spring把字符串给Integer的时候,是通过一个叫做StringToNumber这个类型转换器完成的

6.1、自定义类型转换器

当Spring内部没有提供特定类型转换器时,⽽程序员在应⽤的过程中还需要使⽤,那么就 需要程序员⾃⼰定义类型转换器

我们来试下定义一个Person类有个属性是Date类型的

public class Person implements Serializable {

    private String name;

    private Date birthday;

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

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}';
    }
}

xml配置

 <bean id="person" class="com.chenxin.spring5.converter.Person">
        <property name="name" value="chenxin"></property>
        <property name="birthday" value="2020-04-05"></property>
    </bean>

 测试类:

    @Test
    public void test4(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Person person = (Person) ctx.getBean("person");
        System.out.println("person = " + person);
    }

报错了Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birthday',说明Spring这个无法帮我们转换字符串到Date类型上,Spring没有提供,因为日期的格式,不同的国家不同的格式,所以Spring不会转了,这个时候我们就要自定义类型转换器

 

开发步骤:

  • 类实现Converter接口
public class MyDateConverter implements Converter<String, Date> {
    
    public Date convert(String s) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date date = null;
        try {
            date = simpleDateFormat.parse(s);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

代码是写完了,但是你怎么告诉Spring要找这你自己定义的类型转换器呢? Spring肯定要先把你定义的转换器对象交给Spring管理,创建出来,然后这个转换器是不是要告诉Spring这不是个普通对象,这个是个转换器,你帮我注册到你的里面我要用来转换

  • 给Spring管理来创建
<bean id="myDateConverter" class="com.chenxin.spring5.converter.MyDateConverter">

    </bean>
  • 告诉Spring,你要帮我注册到你的转换器里面,这转换器类型是Set集合,并且是自定义对象,需要用到ref标签
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <ref bean="myDateConverter"></ref>
            </set>
        </property>
    </bean>

这个id,conversionService是固定,不能随便写;这个ConversionServiceFactoryBean中的属性你看下,理解为什么是set标签了吧?

这样就可以帮我们转换了,输出:person = Person{name='chenxin', birthday=Sun Apr 05 00:00:00 CST 2020}

细节:

1、ConversionSeviceFactoryBean 定义 id属性 值必须 conversionService

2、Spring框架内置⽇期类型的转换器,⽇期格式:2020/05/01 (不⽀持 :2020-05-01)

7、后置处理Bean

后置处理Bean全称是BeanPostProcessor,本次课程先入个门,后面讲aop的时候,再深入讲解

作用:对Spring工厂所创建的对象,进行再加工

我们来看个图分析下:

1、对于一个User用户,Spring创建工厂后,对扫描bean id为user,进而拿到了这个类的全路径,然后开始反射拿到构造方法,创建对象

2、在创建对象后,注入完成,然后Spring给你留了个口子,可以你来加工下这个对象,参数Object bean表示刚创建好的对象User,而id值会交给beanName,你加工完成后,返回了你加工好的对象

3、Spring拿到你加工好的对象,再进行初始化操作

4、初始化完成后,又给你留个口子,你又可以加工一次,再还给Spring,从而形成一个Bean

程序员实现BeanPostProcessor规定接⼝中的⽅法:

1、Object postProcessBeforeInitiallization(Object bean String beanName) 作⽤:Spring创建完对象,并进⾏注⼊后,可以运⾏Before⽅法进⾏加⼯ 获得Spring创建好的对象 :通过⽅法的参数 最终通过返回值交给Spring框架

2、Object postProcessAfterInitiallization(Object bean String beanName) 作⽤:Spring执⾏完对象的初始化操作后,可以运⾏After⽅法进⾏加⼯ 获得Spring创建好的对象 :通过⽅法的参数 最终通过返回值交给Spring框架 

实战中其实很少处理Spring的初始化操作:没有必要区分Before After。只需要实现其中的⼀个After ⽅法即可。

开发步骤:

1、类实现BeanPostProcessor,我在处理器中修改了person.name=modify chexin

这里一定要注意,对于BeanPostProcessor来说,BeanPostProcessor会对Spring⼯⼚中所有创建的对象进⾏加⼯!!!!所以你必须要判断下是不是你想要修改的某个对象

public class MyBeanPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(bean instanceof Person){
            Person person = (Person) bean;
            person.setName("modify chenxin!");
        }
        return bean;
    }
}

2、配置文件set name=chenxin,我们看看这个后置处理器有没有帮我们在初始化后,修改名字?

  <bean id="person" class="com.chenxin.spring5.converter.Person">
        <property name="name" value="chenxin"></property>
        <property name="birthday" value="2020-04-05"></property>
    </bean>

    <bean id="myBeanPostProcessor" class="com.chenxin.spring5.postprocessor.MyBeanPostProcessor">

结果可以试试,一定是被修改了成modify chenxin!

细节一定要注意

1、BeanPostProcessor会对Spring⼯⼚中所有创建的对象进⾏加⼯!!!!所以你必须要判断下是不是你想要修改的某个对象

2、为什么不对BeforeInitialization做操作,有个东西叫做击鼓传花知道不,这个过程自始至终,一定是每一个对象的流程,那你之前给我什么,我后面也一定要返回给你什么,所以我们这里都要return bean,只是我这个案例中没有在Before里写而已

有兴趣可以试试,正常开发,只需要实现After就可以了!

好了,本节我们讲到这里,下节开始,面向Aop章节!!!!

猜你喜欢

转载自blog.csdn.net/qq_31821733/article/details/114108469