前言:去年到现在一直没有很好的时间完成这个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章节!!!!