上一篇Blog我们粗浅的搭建了第一个Spring框架,学习了基本的依赖注入实现方式,感觉能面对的场景太少了,本篇Blog我们详细的了解下Spring-Bean到底是如何工作的、作用域是什么,生命周期是什么,以及面对各种类型,依赖注入DI是通过什么方式实现注入的。依赖注入(Dependency Injection,DI)。
- 依赖 : 指Bean对象的创建依赖于容器 ,Bean对象的依赖资源
- 注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配
所以其实我们搞清楚这两个问题就彻底了解IOC思想以及DI注入了。
Spring Bean基本概念
什么是Spring Bean,这让我想到了之前学习Java Web基础时用到的Java Bean。而在Spring中被称作 bean 的对象是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象,它是构成应用程序的支柱。bean 定义包含称为配置元数据的信息,一般分为:XML配置、注解配置以及Java类(不推荐),我们本篇Blog重点讨论XML配置。
Bean与容器的关系
Bean是被容器操作的对象,注册到容器,再被容器实例化后给应用程序使用
Bean配置到容器中时有一些属性表示,这些属性标识了一个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 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- A simple bean definition --> <!-- A bean definition with lazy init set on --> <bean id="..." class="..."> <property name="message" value="tml first Spring from helloProperty"/>\\Setter属性注入的方式 <constructor-arg index="1" value="by constructor-index"/>\\构造方法注入的方式 </bean> <!-- A bean definition with lazy init set on --> <!-- 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例 --> <bean id="..." class="..." lazy-init="true"> <!-- collaborators and configuration for this bean go here --> </bean> <!-- A bean definition with initialization method --> <!-- 在 bean 的所有必需的属性被容器设置之后,调用回调方法 --> <bean id="..." class="..." init-method="..."> <!-- collaborators and configuration for this bean go here --> </bean> <!-- A bean definition with destruction method --> <!--当包含该 bean 的容器被销毁时,使用回调方法--> <bean id="..." class="..." destroy-method="..."> <!-- collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions go here --> </beans>
Bean的作用域
当在 Spring 中定义一个 bean 时,必须声明该 bean 的作用域的选项,我们一般不声明是因为使用了默认的作用域singleton
几种作用域中,request、session作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于web的Spring ApplicationContext环境。所以其实按照寿命长短:singleton最长,session次之,request再次,prototype最短。
singleton作用域
当一个bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。Singleton是单例类型,就是在创建启动容器时就同时自动创建了一个bean的对象,不管你是否使用,他都存在了,每次获取到的对象都是同一个对象。注意,Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton,可以这样配置:
<bean id="helloProperty" class="com.example.Spring.model.HelloSpring" scope="singleton">
prototype作用域
当一个bean的作用域为prototype,表示一个bean定义对应多个对象实例。prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。prototype是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。在XML中将bean定义成prototype,可以这样配置:
<bean id="helloProperty" class="com.example.Spring.model.HelloSpring" scope="prototype">
request作用域
当一个bean的作用域为Request,表示在一次HTTP请求中,一个bean定义对应一个实例;即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext情形下有效
<bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>
针对每次HTTP请求,Spring容器会根据loginAction bean的定义创建一个全新的LoginAction bean实例,且该loginAction bean实例仅在当前HTTP request内有效,因此可以根据需要放心的更改所建实例的内部状态,而其他请求中根据loginAction bean定义创建的实例,将不会看到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁
session作用域
当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义:
<bean id="userPreferences" class="com.example.Spring.UserPreferences" scope="session"/>
针对某个HTTP Session,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域内的bean也会被废弃掉。
Bean的生命周期
理解 Spring bean 的生命周期很容易。当一个 bean 被实例化时,它可能需要执行一些初始化使它转换成可用状态。同样,当 bean 不再需要,并且从容器中移除时,可能需要做一些清除工作,为了定义安装和拆卸一个 bean,我们只要声明带有 init-method 和/或 destroy-method 参数的 。
init-method
属性指定一个方法,在实例化 bean前,立即调用该方法。destroy-method
指定一个方法,只有从容器中移除 bean 之后,才能调用该方法
Bean的生命周期可以表达为:Bean的定义——Bean的初始化——Bean的使用——Bean的销毁,由于我们一般在Web环境中使用,所以Bean的初始化和销毁都由容器帮忙处理,所以这里我们只要知道大概的生命周期即可,不必深究
Bean的继承关系
Bean继承与Java类继承相似,继承者有被继承者的属性配置,继承的原则有如下几条:
- Spring允许继承bean的配置,被继承的bean称为父bean,继承者被称为子bean
- 子bean从父bean中继承配置,包括bean的属性配置。
- 子bean可以覆盖从父bean继承过来的配置
- 父bean可以单纯作为配置模版,若只想把父bean设置为配置模版而非实例,可以设置
<bean>
的abstract属性为true,这样Spring将不会实例化这个Bean,类似于Java的抽象类
接下来我们做个简单事件探讨下父子bean:
1 创建FatherBean和ChildBean类
首先我们创建两个类:FatherBean和ChildBean类,从代码中看不出他们有继承关系,所以其实ChildBean的属性还是需要再定义一遍:
FatherBean
package com.example.Spring.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class FatherBean { private String message1; private String message2; public void show(){ System.out.println(message1+" "+message2); } }
ChildBean
package com.example.Spring.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class ChildBean { private String message1; private String message2; private String message3; public void show(){ System.out.println(message1+" "+message2+" "+message3); } }
2 applicationContext.xml配置修改
在applicationContext.xml配置中我们配置下父子关系:
<bean id="fatherBean" class="com.example.Spring.model.FatherBean"> <property name="message1" value="fatherBean 的message1"/> <property name="message2" value="fatherBean 的message2"/> </bean> <bean id="childBean" class="com.example.Spring.model.ChildBean" parent="fatherBean"> <property name="message2" value="ChildBean 自己的message1"/> <property name="message3" value="ChildBean 自己的message"/> </bean>
3 单元测试进行验证
我们编写如下单元测试进行验证:
import com.example.Spring.model.ChildBean; import com.example.Spring.model.FatherBean; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class ExtendTest { @Test public void ExtendTest(){ //解析beans.xml文件,生成管理相应的bean对象 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); FatherBean fatherBean = (FatherBean) context.getBean("fatherBean"); fatherBean.show(); ChildBean childBean = (ChildBean) context.getBean("childBean"); childBean.show(); } }
打印结果如下:
需要注意如果bean标识了abstract,就不可以实例化了,例如:
<bean id="fatherBean" class="com.example.Spring.model.FatherBean" abstract="true">
但是子bean依然可以使用父bean的属性:
DI注入深度实践
按照注入的配置元数据来讲,一般分为三类:基于 XML 的配置文件,基于注解的配置,基于 Java 的配置【不推荐】。从注入的注入方式来讲,一般分为三类:Setter属性注入,构造函数注入。我们一般使用的XML配置有两种注入方式:Setter属性注入和构造函数注入,另两种注解配置以及基于Java配置使用的是Setter属性注入,这个之后再详细介绍,上一篇Blog也详细讨论过构造函数的注入方式,与Setter是类似的,所以这里我们以基于 XML 的配置文件+Setter属性注入来讨论通用的注入方式,并且再讨论下基于 XML 的配置文件更简化的命名空间注入写法
注入内容分类实践
我们分别测试下这几种类型的注入:常量注入,Bean注入(包含内部Bean注入) ,数组注入,List注入,Map注入,Set注入,Null注入以及Property注入方式:
1 Person类编写
首先编写我们的Person类,包含各种类型属性
package com.example.Spring.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; @Data @AllArgsConstructor @NoArgsConstructor public class Perosn { private String name; private int age; private String[] emails; private List<String> interests; private Set<String> games; private Map<String,String> bankAccountMap; private String badHobby; private Address address; private Properties info; public void show(){ System.out.println("name: "+name); System.out.println("age: "+age); System.out.print("emails: "); for (String email:emails){ System.out.print(email+" "); } System.out.println(" "); System.out.println("interests: "+interests); System.out.println("games: "+games); System.out.println("bankAccountMap: "+bankAccountMap); System.out.println("badHobby: "+badHobby); System.out.println("address: "+address); System.out.println("info: "+info); } }
2 applicationContext.xml配置
然后增加bean的配置文件:
<bean id="person" class="com.example.Spring.model.Perosn" > <!--常量注入--> <property name="name" value="tml"/> <!--常量注入--> <property name="age" value="30"/> <!--array注入--> <property name="emails"> <array> <value>12345668@qq.com</value> <value>12345668@163.com</value> <value>12345668@126.com</value> </array> </property> <!--list注入--> <property name="interests"> <list> <value>听歌</value> <value>看电影</value> <value>爬山</value> </list> </property> <!--Set注入--> <property name="games"> <set> <value>LOL</value> <value>王者荣耀</value> <value>使命召唤</value> </set> </property> <!--Map注入--> <property name="bankAccountMap"> <map> <entry key="中国邮政" value="456456456465456"/> <entry key="建设" value="1456682255511"/> </map> </property> <!--Null注入--> <property name="badHobby"><null/></property> <!--Bean注入--> <property name="address" ref="address"/> <!--属性注入--> <property name="info"> <props> <prop key="学号">20190604</prop> <prop key="性别">男</prop> <prop key="姓名">小明</prop> </props> </property> </bean> <bean id="address" class="com.example.Spring.model.Address" > <property name="cityCode" value="100031"/> <property name="cityName" value="北京"/> </bean> <bean id="fatherBean" class="com.example.Spring.model.FatherBean" abstract="true"> <property name="message1" value="fatherBean 的message1"/> <property name="message2" value="fatherBean 的message2"/> </bean> <bean id="childBean" class="com.example.Spring.model.ChildBean" parent="fatherBean"> <property name="message2" value="ChildBean 自己的message1"/> <property name="message3" value="ChildBean 自己的message"/> </bean> <!--bean就是java对象 , 由Spring创建和管理--> <bean id="helloProperty" class="com.example.Spring.model.HelloSpring"> <property name="message" value="tml first Spring from helloProperty"/> <property name="from" value="by property"/> </bean>
3 编写测试类进行测试
通过测试类编写进行测试:
import com.example.Spring.model.Perosn; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class PersonTest { @Test public void PersonTest(){ //解析beans.xml文件,生成管理相应的bean对象 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Perosn person = (Perosn) context.getBean("person"); person.show(); } }
打印结果如下:
命名空间注入写法
除了常规写法外,还有命名空间的注入方式,就像我们之前学JSP的写法时一样的,像我们之前使用JSP的JSTL表达式一样,通过命名空间注入写法也可以节省一些配置,有两种方式对应我们上边提到的两种方式:p命名空间注入和c命名空间注入
1 头文件添加约束
首先我们需要在配置文件applicationContext.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" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
添加内容为:
xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c"
2 User类编写
首先编写我们的User类,包含各种类型属性
package com.example.Spring.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private String name; private int age; public void show(){ System.out.println("name: "+name); System.out.println("age: "+age); } }
3 applicationContext.xml配置
然后增加bean的配置文件:
<!--P(属性: properties)命名空间 , 属性依然要设置set方法--> <bean id="userProperty" class="com.example.Spring.model.User" p:name="tmlProperty" p:age="30"/> <!--C(构造: Constructor)命名空间 , 属性依然要设置set方法--> <bean id="userConstructor" class="com.example.Spring.model.User" c:name="tmlConstructor" c:age="30"/>
4 编写测试类进行测试
通过测试类编写进行测试:
import com.example.Spring.model.User; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserTest { @Test public void UserTest(){ //解析beans.xml文件,生成管理相应的bean对象 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); User userProperty = (User) context.getBean("userProperty"); userProperty.show(); User userConstructor = (User) context.getBean("userConstructor"); userConstructor.show(); } }
打印结果如下:
总结一下
这篇Blog主要详细了解了依赖注入各种类型的DI注入方式,Beans的生命周期、Bean的作用域以及Beans的作用、和IOC容器的搭配工作方式。以及比较类似JSP的JSTL表达式写法的命名空间注入的方式。这里我们可以发现其实一通百通,知识都是连贯的,简化的配置方式在各种框架和实现方式中都有其作用,理解起来也很通俗,良好的实现方式是相通的,不同的只是表面呈现给我们的东西,透过现象看本质并且将本质拓展为良好的实现是一种需要锻炼的能力。