Spring之IOC的应用-4
4. Spring IOC的应用
4.1 Spring IOC基础
IOC:控制反转 把对象的创建和对象之间的调用关系 交给spring去管理。
目的就是为了降低耦合度
我们把对象交给spring去管理,spring是如何操作这些对象的?其实spring会在内存中生成一个容器,把创建出来的对象都存放到里面进行管理,所以我们从spring中获取对象,也叫从IOC容器中/spring容器中获取对象。
4.2 BeanFactory与ApplicationContext区别
BeanFactory 是 Spring 框架中 IoC 容器的顶层接⼝ , 它只是⽤来定义⼀些基础功能 , 定义⼀些基础规范 , ⽽ApplicationContext 是它的⼀个⼦接⼝,所以 ApplicationContext 是具备 BeanFactory 提供的全部功能的。
通常,我们称 BeanFactory 为 SpringIOC 的基础容器,ApplicationContext 是容器的⾼级接⼝,⽐BeanFactory 要拥有更多的功能,⽐如说国际化⽀持和资源访问( xml , java 配置类)等等。
1. BeanFactory是什么
BeanFactory是所有SpringBean容器的根接口,给Spring容器定义一套规范,给IOC容器提供了一套完整的规范,比如常用到的getBean方法等。
2. ApplicationContext是什么
ApplicationContext是Spring中的容器,可以用来获取容器中的各种bean组件,注册监听事件,加载资源文件。
ApplicationContext是BeanFactory的子接口
ApplicationContext提供了更完整的功能:
继承MessageSource支持国际化
统一的资源访问方式
提供在监听器注册bean的事件
同时加载多个配置文件
载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层
BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用GetBean())才对该Bean进行加载实例化。这样我们就不能发现一些存在的Spring配置问题。如果Bean的某一属性没有注入,BeanFactory加载后直至第一次调用GetBean()方法才会抛出异常。
ApplicationContext在容器启动时一次性的创建了所有的Bean,在容器启动时,我们就可以发现Spirng中存在的配置错误,有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例Bean,确保需要的时候不用等待,因为它们已经创建好了。
相对与基本的BeanFactory,ApplicationContext唯一不足的是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
BeanFactory和ApplicationContext都支持BeanProcessor、BeanFactoryPostProcessor的使用,BeanFactory需要手动注册,ApplicationContext是自动注册。
3. 两者的区别?
相同:
- Spring提供了两种不同的IOC容器,一个是BeanFactory,另一个是ApplicationContext,它们都是Java interface,ApplicationContext继承于BeanFactory。
- 它们都可以用来配置XML属性,也支持属性的自动注入。
- Beanfactory和ApplicationContext都提供了一种方式,使用getBean(“bean name”)获取bean。
不同:
当你调用getBean()方法时,BeanFactory仅实例化bean,而ApplicationContext在启动容器的时候实例化单例bean,不会等待调用getBean()方法时再实例化。
BeanFactory不支持国际化,即i18n,但ApplicationContext提供了对它的支持。
BeanFactory与ApplicationContext之间的另一个区别是能够将事件发布到注册为监听器的bean。
BeanFactory的一个核心实现是XMLBeanFactory而ApplicationContext的一个核心实现是ClassPathXMLApplicationContext,Web容器的环境我们使用WebApplicationContext并增加了getServletContext方法。
如果使用自动注入并使用BeanFactory,则需要使用API注册AutoWiredBeanPostProcessor,如果使用ApplicationContext,则可以使用XML进行配置。
简而言之,BeanFactory提供基本的IOC和DI功能,而ApplicationContext提供高级功能,BeanFactory可用于测试和非生产使用,但ApplicationContext是功能更丰富的容器实现,应该优于BeanFactory。
启动 IoC 容器的⽅式:
- Java 环境下启动 IoC 容器
- ClassPathXmlApplicationContext :从类的根路径下加载配置⽂件(推荐使⽤)
- FileSystemXmlApplicationContext :从磁盘路径上加载配置⽂件
- AnnotationConfifigApplicationContext :纯注解模式下启动 Spring 容器
- Web 环境下启动 IoC 容器
从 xml 启动容器
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--配置Spring ioc容器的配置文件--> <context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value> </context-param>
<!--使用监听器启动Spring的IOC容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>从配置类启动容器
<web-app>
<display-name>Archetype Created Web Application</display-name>
<context-param>
<!--告诉ContextloaderListener知道我们使用注解的方式启动ioc容器-->
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<!--配置启动类的全限定类名-->
<param-name>contextConfigLocation</param-name>
<param-value>com.lagou.edu.SpringConfig</param-value>
</context-param>
<!--使用监听器启动Spring的IOC容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
4.3 纯xml模式
采⽤ Spring IoC 纯 xml 模式改造我们前⾯⼿写的IoC 和 AOP 实现,在改造的过程中,把各个知识点串起来。
1. xml ⽂件头
引入spring相关依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.5</version>
</dependency>把原来的bean.xml修改成为spring配置文件,修改名字为applicationContext.xml,加入spring的文件头
<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">
xmlns就是xml的namespace,可以配置多个,底下用相应的别名去调用其中的方法,比如:
|
2. 实例化 Bean 的三种⽅式
- ⽅式⼀:使⽤⽆参构造函数
在默认情况下,它会通过反射调⽤⽆参构造函数来创建对象。如果类中没有⽆参构造函数,将创建失败。(这是推荐使用的方式 )
<bean id="userService" class="com.lagou.service.impl.TransferServiceImpl"></bean> |
- ⽅式⼆:使⽤静态⽅法创建
在实际开发中,我们使⽤的对象有些时候并不是直接通过构造函数就可以创建出来的,它可能在创建的过程 中会做很多额外的操作。此时会提供⼀个创建对象的⽅法,恰好这个⽅法是 static 修饰的⽅法,即是此种情况。
例如,我们在做 Jdbc 操作时,会⽤到 java.sql.Connection 接⼝的实现类,如果是 mysql 数据库,那么⽤的就是 JDBC4Connection ,但是我们不会去写 JDBC4Connection connection = new JDBC4Connection() ,
因为我们要注册驱动,还要提供 URL 和凭证信息,⽤ DriverManager.getConnection ⽅法来获取连接。
那么在实际开发中,尤其早期的项⽬没有使⽤Spring框架来管理对象的创建,但是在设计时使⽤了⼯⼚模式 解耦,那么当接⼊spring 之后,⼯⼚类创建对象就具有和上述例⼦相同特征,即可采⽤此种⽅式配置。
<!--使用静态方法创建对象的配置方式--> |
- ⽅式三:使⽤实例化⽅法创建
此种⽅式和上⾯静态⽅法创建其实类似,区别是⽤于获取对象的⽅法不再是 static 修饰的了,⽽是类中的⼀ 个普通⽅法。此种⽅式⽐静态⽅法创建的使⽤⼏率要⾼⼀些。
在早期开发的项⽬中,⼯⼚类中的⽅法有可能是静态的,也有可能是⾮静态⽅法,当是⾮静态⽅法时,即可采⽤下⾯的配置:
<!--使用实例方法创建对象的配置方式--> |
创建CreateBeanFactory,进行测试
package com.lagou.edu.factory; |
测试方法:
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); |
3. Bean的作用范围和生命周期
- 作用范围的改变
在spring框架管理Bean对象的创建时,Bean对象默认都是单例的,但是它支持配置的方式改 变作用范围。作用范围官方提供的说明如下图:
- 不同作用范围的声明周期
单例模式:singleton
对象出生:当创建容器时,对象就被创建了。
对象活着:只要容器在,对象一直活着。
对象死亡:当销毁容器时,对象就被销毁了。
一句话总结:单例模式的bean对象生命周期与容器相同。
多例模式:prototype
对象出生:当使用对象时,创建新的对象实例。
对象活着:只要对象在使用中,就一直活着。
对象死亡:当对象⻓时间不用时,被java的垃圾回收器回收了。
一句话总结:多例模式的bean对象,spring框架只负责创建,不负责销毁。
Bean标签属性:
在基于xml的IoC配置中,bean标签是最基础的标签。它表示了IoC容器中的一个对象。换句话 说,如果一个对象想让spring管理,在XML的配置中都需要使用此标签配置,Bean标签的属性如 下:
id属性: 用于给bean提供一个唯一标识。在一个标签内部,标识必须唯一。
class属性:用于指定创建Bean对象的全限定类名。
name属性:用于给bean提供一个或多个名称。多个名称用空格分隔。
factory-bean属性:用于指定创建当前bean对象的工厂bean的唯一标识。当指定了此属性之后, class属性失效。
factory-method属性:用于指定创建当前bean对象的工厂方法,如配合factory-bean属性使用, 则class属性失效。如配合class属性使用,则方法必须是static的。
scope属性:用于指定bean对象的作用范围。通常情况下就是singleton。当要用到多例模式时, 可以配置为prototype。
init-method属性:用于指定bean对象的初始化方法,此方法会在bean对象装配后调用。必须是 一个无参方法。
destory-method属性:用于指定bean对象的销毁方法,此方法会在bean对象销毁前执行。它只 能为scope是singleton时起作用。
DI 依赖注入的xml配置:
依赖注入分类
按照注入的方式分类
构造函数注入:顾名思义,就是利用带参构造函数实现对类成员的数据赋值。
set方法注入:它是通过类成员的set方法实现数据的注入。(使用最多的)
按照注入的数据类型分类
基本类型和String
注入的数据类型是基本类型或者是字符串类型的数据。
- 其他Bean类型
注入的数据类型是对象类型,称为其他Bean的原因是,这个对象是要求出现在IoC容器 中的。那么针对当前Bean来说,就是其他Bean了。
- 复杂类型(集合类型)
注入的数据类型是Aarry,List,Set,Map,Properties中的一种类型。
- 依赖注入的配置实现之构造函数注入
顾名思义,就是利用构造函数实现对类成员的赋值。它的使用要求是,类中提供的构造函数参数个数必须和配置的参数个数一致,且数据类型匹配。同时需要注意的是,当没有无参构造时,则必须提供构造函数参数的注入,否则Spring 框架会报错。
在使用构造函数注入时,涉及的标签是 constructor-arg ,该标签有如下属性:
name:用于给构造函数中指定名称的参数赋值。
index:用于给构造函数中指定索引位置的参数赋值。
value:用于指定基本类型或者String类型的数据。
ref:用于指定其他Bean类型的数据。写的是其他bean的唯一标识。
依赖注入的配置实现之set方法注入
顾名思义,就是利用字段的set方法实现赋值的注入方式。此种方式在实际开发中是使用最多的注 入方式。
在使用set方法注入时,需要使用 property 标签,该标签属性如下:
name:指定注入时调用的set方法名称。(注:不包含set这三个字母,druid连接池指定属性名称)
value:指定注入的数据。它支持基本类型和String类型。
ref:指定注入的数据。它支持其他bean类型。写的是其他bean的唯一标识。
复杂数据类型注入
首先,解释一下复杂类型数据,它指的是集合类型数据。集合分为两类,一类 是List结构(数组结构),一类是Map接口(键值对) 。
接下来就是注入的方式的选择,只能在构造函数和set方法中选择,我们的示例选用set方法注入。
在List结构的集合数据注入时, array , list , set 这三个标签通用,另外注值的 value 标签内部 可以直接写值,也可以使用 bean 标签配置一个对象,或者用 ref 标签引用一个已经配合的bean 的唯一标识。
在Map结构的集合数据注入时, map 标签使用 entry 子标签实现数据注入, entry 标签可以使 用key和value属性指定存入map中的数据。使用value-ref属性指定已经配置好的bean的引用。 同时 entry 标签中也可以使用 ref 标签,但是不能使用 bean 标签。而 property 标签中不能使 用 ref 或者 bean 标签引用对象
4.4 xml与注解模式相结合
注意:
- 实际企业开发中,纯xml模式使用已经很少了
- 引入注解功能,不需要引入额外的jar
- xml+注解结合模式,xml文件依然存在,所以,spring IOC容器的启动仍然从加载xml开始
- 哪些bean的定义写在xml中,哪些bean的定义使用注解
第三方jar中的bean定义在xml,比如Druid数据库连接池
自己开发的bean定义使用注解
1. xml中标签与注解的对应(IoC)
2. DI 依赖注入的注解实现方式
@Autowired(推荐使用)
@Autowired为Spring提供的注解,需要导入包 org.springframework.beans.factory.annotation.Autowired。
@Autowired采取的策略为按照类型注入。
public class TransferServiceImpl { |
@Autowired按照类型进行注入,如上代码所示,这样装配回去spring容器中找到类型为AccountDao的类,然后将其注⼊进来。这样会产⽣⼀个问题,当一个类型有多个bean值的时候,会造成无法选择具体注入哪一个的情况, 这个时候我们需要配合着@Qualifier使用。
@Qualififier 告诉 Spring具体去装配哪个对象。这个时候我们就可以通过类型和名称定位到我们想注⼊的对象。
public class TransferServiceImpl { |
@Resource
@Resource 注解由 J2EE 提供,需要导入包 javax.annotation.Resource。 @Resource 默认按照 ByName 自动注入。
如果同时指定了 name 和 type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
如果指定了 name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
如果指定了 type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;
注意:
@Resource 在 Jdk 11中已经移除,如果要使用,需要单独引入jar包
<dependency> |
public class TransferService { |
4.5 纯注解模式
1. 使用方式
改造xml+注解模式,将xml中遗留的内容全部以注解的形式迁移出去,最终删除xml,从Java配置类启动
**对应注解: **
@Configuration
注解,表名当前类是一个配置类@ComponentScan
注解,替代 context:component-scan@PropertySource
,引入外部属性配置文件@Import
引入其他配置类@Value
对变量赋值,可以直接赋值,也可以使用 ${} 读取资源配置文件中的信息@Bean
将方法返回对象加入 SpringIOC 容器
// @Configuration 注解表明当前类是一个配置类 |
2. @Bean和@Compont两者的区别
注解作用
@Component注解表明一个类会作为组件类,并告知Spring要为这个类创建bean。
@Bean注解告诉Spring这个方法将会返回一个对象,这个对象要注册为Spring应用上下文中的bean。通常方法体中包含了最终产生bean实例的逻辑。
两者对比
相同点:两者的结果都是为spring容器注册Bean.
不同点:@Component 通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中。
@Bean 注解通常是我们在标有该注解的方法中定义产生这个bean的逻辑。
理解
@Component
(@Controller @Service @Respository)作用于类上,只有在我们的SpringBoot应用程序启用了组件扫描并且包含了被注解的类时才有效。通过组件扫描,Spring将扫描整个类路径,并将所有@Component注释类添加到Spring Context,这里有的不足就是会把整个类当成bean注册到spring 容器上,如果这个类中并不是所有方法都需要注册为bean的话,会出现不需要的方法都注册成为bean,这时候必须确保这些不需要的方法也能注册为bean或者在扫描中加filter 过滤这些不需要的bean,否者spring将无法成功启动。
@Bean
相对来说就更加灵活了,它可以独立加在方法或者类上,按需注册到spring容器,而且如果你要用到第三方类库里面某个类或者方法的时候,你就只能用@Bean把这个类或者方法注册到spring容器,因为用@Component你需要配置组件扫描到这个第三方类路径而且还要在别人源代码加上这个注解,很明显是不现实的。
4.6 lazy-Init 延迟加载
Bean的延迟加载(bean对象的延迟创建)
ApplicationContext 容器的默认行为是在启动服务器时将所有 singleton bean 提前进行实例化。提前实例化
意味着作为初始化过程的一部分,ApplicationContext 实例会创建并配置所有的singleton bean。
比如:
<bean id="testBean" class="cn.lagou.LazyBean" /> |
lazy-init="false"
,立即加载,表示在spring启动时,立刻进行实例化。
如果不想让一个singleton bean 在 ApplicationContext实现初始化时被提前实例化,那么可以将bean设置为延迟实例化。
<bean id="testBean" calss="cn.lagou.LazyBean" lazy-init="true" /> |
设置 lazy-init 为 true 的 bean 将不会在 ApplicationContext 启动时提前被实例化,而是第一次向容器通过 getBean 索取 bean 时实例化的。
如果一个设置了立即加载的 bean1,引用了一个延迟加载的 bean2 ,那么 bean1 在容器启动时被实例化,而 bean2 由于被 bean1 引用,所以也被实例化,这种情况也符合延时加载的 bean 在第一次调用时才被实例化的规则。
也可以在容器层次中通过在 元素上使用 “default-lazy-init” 属性来控制延时初始化。如下面配置:
<beans default-lazy-init="true"> |
如果一个 bean 的 scope 属性为 scope=”pototype” 时,即使设置了 lazy-init=”false”,容器启动时也不 会实例化bean,而是调用 getBean 方法实例化的。
应用场景
开启延迟加载一定程度提高容器启动和运转性能;
对于不常使用的 Bean 设置延迟加载,这样偶尔使用的时候再加载,不必要从一开始该 Bean 就占用资源。
4.7 FactoryBean 和 BeanFactory
BeanFactory接口是容器的顶级接口,定义了容器的一些基础行为,负责生产和管理Bean的一个工厂,具体使用它下面的子接口类型,比如ApplicationContext;
此处我们重点分析FactoryBean:
Spring中Bean有两种,一种是普通Bean,一种是工厂Bean(FactoryBean),FactoryBean可以生成某一个类型的Bean实例(返回给我们),也就是说我们可以借助于它自定义Bean的创建过程。
Bean创建的三种方式中的静态方法和实例化方法和FactoryBean作用类似,FactoryBean使用较多,尤其在Spring框架一些组件中会使用,还有其他框架和Spring框架整合时使用。
// 可以让我们自定义Bean的创建过程(完成复杂Bean的定义) |
<bean id="companyBean" class="com.lagou.edu.factory.CompanyFactoryBean"> |
测试结果:
Object companyBean = applicationContext.getBean("companyBean"); |
4.8 后置处理器
Spring提供了两种后处理bean的扩展接口,分别为 BeanPostProcessor 和BeanFactoryPostProcessor,两者在使用上是有所区别的。
工厂初始化(BeanFactory) —> Bean对象
在BeanFactory初始化之后可以使用BeanFactoryPostProcessor进行后置处理做一些事情
在Bean对象实例化(并不是Bean的整个生命周期完成)之后可以使用BeanPostProcessor进行后置处理做一些事情
注意:对象不一定是springbean,而springbean一定是个对象
1. SpringBean生命周期
在实例化之前,容器会把xml文件的配置信息读进来:
public class Result implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean { |
2. BeanPostProcessor
BeanPostProcessor是针对Bean级别的处理,可以针对某个具体的Bean,如下对lazyResult处理
该接口提供了两个方法,分别在Bean的初始化方法前和初始化方法后执行,具体这个初始化方法指的是什么方法,类似我们在定义bean时,定义了init-method所指定的方法
定义一个类实现了BeanPostProcessor,默认是会对整个Spring容器中所有的bean进行处理。如果要对 具体的某个bean处理,可以通过方法参数判断,两个类型参数分别为Object和String,第一个参数是每 个bean的实例,第二个参数是每个bean的name或者id属性的值。所以我们可以通过第二个参数,来判断我们将要处理的具体的bean。
注意:BeanPostProcessor处理是发生在Spring容器的实例化和依赖注入之后。
/** |
3. BeanFactoryPostProcessor
BeanFactory 级别的处理,是针对整个 Bean 的⼯⼚进⾏处理,典型应⽤ :PropertyPlaceholderConfifigurer
此接⼝只提供了⼀个⽅法,⽅法参数为ConfifigurableListableBeanFactory
,该参数类型定义了⼀些⽅法。
其中有个⽅法名为 getBeanDefifinition
的⽅法,我们可以根据此⽅法,找到我们定义 bean 的BeanDefifinition
对象。
然后我们可以对定义的属性进⾏修改,以下是 BeanDefifinition 中的⽅法:
⽅法名字类似我们 bean 标签的属性, setBeanClassName
对应bean 标签中的 class 属性
,所以当我们拿到 BeanDefifinition
对象时,我们可以⼿动修改 bean 标签中所定义的属性值。
BeanDefifinition 对象: 我们在 XML 中定义的 bean 标签, Spring 解析 bean 标签成为⼀个 JavaBean ,这个 JavaBean 就是 BeanDefifinition。
注意:调⽤ BeanFactoryPostProcessor ⽅法时,这时候 bean 还没有实例化,此时 bean 刚被解析成BeanDefifinition 对象。
留给下一章的问题:
ClassPathXmlApplicationContext初始化过程?
ClassPathXmlApplicationContext初始化怎么对注解进行处理的?
AnnotationConfigApplicationContext初始化过程,如何对注解解析的?
bean实例化过程,对应三种方式?
beanfactory启动后实例化bean的过程中怎么过滤掉prototype,然后再getBean方法中直接返回对象?
bean什么时候依赖注入?@Autowired和@Qualified的工作原理?
懒加载怎么实现的?
BeanPostProcessor的调用时机?
BeanFactoryPostProcessor的调用时机?
多例模式直接返回,怎么实现的?