转载于:https://blog.csdn.net/qq_42192693/article/details/112967959
1,IoC(Spring的核心和基础)
1.1,IoC原理
纵观所有的Java应用(从基于Applet的小应用到多层结构的企业级应用),这些应用中大量存在A对象调用B对象方法的情况,这种情况被Spring称为依赖,即A对象依赖B对象。对于Java应用而言,它们总是由一些相互调用的对象构成的,Spring把这种相互调用的关系称为依赖关系。
Spring框架的核心功能有两个:
- Spring容器作为超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。
- Spring容器管理容器中的Bean之间的依赖关系,Spring使用一种被称为“依赖注入”的方式来管理Bean之间的依赖关系。
IOC(控制反转):IoC 不是一种技术,只是一种思想并非 Spring 特有,其思想是反转资源获取的方向。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
简单的说之前我们在代码中创建一个对象是通过
new
关键字,而使用了Spring
之后,我们不在需要自己去new
一个对象了,而是直接通过容器里面去取出来,再将其自动注入到我们需要的对象之中,也就说创建对象的控制权不在我们程序员手上了,全部交由Spring
进行管理。交给 Spring 管理的也称为 Bean,所有的 Bean 都被存储在一个 Map 集合中,这个 Map 集合也称为 IoC 容器。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。比如说,在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能每次都要搞清这个 Service 所有底层类的构造函数,这显然过于繁琐。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
DI(依赖注入): IoC 的另一种表述方式:即组件以一些预先定义好的方式(例如: setter 方法)接受来自如容器的资源注入. 相对于 IOC 而言,这种表述更直接。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
因此,不管是依赖注入还是控制反转,其含义是完全相同的。当某个Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常有如下两个做法:
- 原始做法:调用者主动创建被依赖对象,然后再调用被依赖对象的方法。由于调用者需要通过“new 被调用对象构造器()”的代码创建安对象,因此,必然导致调用者与被依赖对象实现类的硬编码耦合,非常不利于项目和维护。
- 简单工厂模式:调用者先找到被依赖对象的工厂,然后主动通过工厂去获取被依赖对象,最后再调用被依赖对象的方法。大致需要需要把握三点:(1)调用者向被依赖对象的接口编程;(2)将被依赖对象的创建交给工厂完成;(3)调用者通过工厂来获得被依赖组件。通过这三点,可以保证调用者只需要与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合。这个方法的唯一缺点是,调用组件需要主动通过工厂去获取被依赖对象,这就会带来调用组件与被依赖对象工厂的耦合。
Spring框架的IOC原理:调用者无需主动获取被依赖对象,调用者只需要被动接受Spring容器为调用者的成员变量赋值即可(只要配置一个<property.../>子元素,Spring就会执行对应的setter方法为调用者的成员变量赋值)。IoC实现的基础是工厂模式,所使用的技术主要是java的反射技术。(工厂模式+反射)
- 使用Spring框架之后,调用者获取被依赖对象的方式由原来的主动获取,变成了被动接受——也被称为控制反转。
- 从Spring角度来看,Spring容器负责将被依赖对象赋值给调用者的成员变量——相当于为调用者注入它依赖的实例,因此也被称为依赖注入。
- 正因为Spring将被依赖对象注入给了调用者,所以调用者无须主动获取被依赖对象,只要被动等待Spring容器注入即可。由此可见,控制反转和依赖注入其实是同一种行为的两种表达,只是描述的角度的问题。
使用依赖注入,不仅可以为Bean注入普通的属性值,还可以注入其他Bean的引用。通过这种依赖注入,JavaEE应用中的各种组件不需要以硬编码的方式耦合在一起,甚至无须使用工厂模式。依赖注入达到的效果,非常类似于“共产主义”,当某个Java实例需要其他Java实例时,系统自动提供所需的实例,无须程序显式获取。
依赖注入是一种非常优秀的解耦方式。依赖注入让Spring的Bean以配置的文件组合在一起,而不是以硬编码的方式耦合在一起。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。
1.2,理解依赖注入
案例:一个人(Java实例,调用者)需要一把斧头(Java实例,被依赖对象)。
原始做法:需要斧头的人(调用者)只能自己去磨一把斧子(被依赖对象)。这就相当于在Java程序里的调用者自己创建被依赖对象,通常采用new关键字来调用构造器创建一个被依赖对象。
Java实例的调用者创建被调用的Java实例,调用者直接使用new关键字创建被依赖对象,程序高度耦合,效率低下,真是应用极少使用这种方式。
- 可扩展性差:由于人和斧头实现高度耦合,当程序视图扩展斧头时,人的代码也要随之改变。
- 各组件职责不清:对于人组件而言,它只需要调用斧头的方法即可,并不关心斧头的创建过程。这种种情况下,人却主动创建斧头,因此职责混乱。
简单工厂模式:斧头不再由普通人完成,而是在工厂里被生产出来,此时需要斧头的人(调用者)找到工厂,购买斧头,无须关心斧头的制作过程。对应简单工厂模式,调用者只需要定位工厂,无须理会被依赖对象的具体实现过程。
调用者无须关心被依赖对象的具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被依赖对象的实现类的解耦,专业是工厂模式大量使用的原因。但调用者依然需要主动定位工厂,调用者与工厂耦合在一起。
IoC:需要斧头的人无须定位工厂,坐等社会的提供即可。调用者无需关心被依赖对象的实现,无须理会工厂,等待Spring的依赖注入。
程序完全无须理会被依赖对象的实现,也无须主动定位工厂,这是一种优秀的解耦方式。实例之间的依赖关系由IoC容器负责管理。
使用Spring框架之后的两个主要改变是:
- 程序无须使用new调用构造器去创建对象。所有的Java对象都可交给Spring容器去创建。
- 当调用者需要调用被依赖对象的方法时,调用者无须去主动获取被依赖对象,只要等待Spring容器注入即可。
1.3,工厂模式+反射=IoC
Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 示例:
interface Fruit {
public abstract void eat();
}
class Apple implements Fruit {
public void eat() {
System.out.println("Apple");
}
}
class Orange implements Fruit {
public void eat() {
System.out.println("Orange");
}
}
class Factory {
public static Fruit getInstance(String ClassName) {
Fruit f = null;
try {
f = (Fruit) Class.forName(ClassName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
public class Main {
public static void main(String[] args) {
Fruit f = Factory.getInstance("Apple");
if (f != null) {
f.eat();
}
}
}
2,依赖注入方式
构造注入:就是被注入对象可以在它的构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。然后,IoC Service Provider会检查被注入的对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。
设值注入:通过setter方法,可以更改相应的对象属性。所以,当前对象只要为其依赖对象所对应的属性添加 setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。setter方法注入虽不 像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。
接口注入:相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。相对于 前两种依赖注入方式,接口注入比较死板和烦琐。
总体来说,构造注入和设值注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最 多的注入方式。而接口注入因为侵入性较强,近年来已经不流行了。
2.1,设值注入
设值注入:是指IoC容器使用setter方法来注入被依赖的实例。通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即可实现基于setter的DI。这种方式简单、直观,因而在Spring的依赖注入里大量使用。
Bean与Bean之间的依赖关系由Spring管理,Spring采用setter方法为目标Bean注入所依赖的Bean,这种方式被称为设置注入。
依赖注入以配置文件管理Bean实例之间的耦合,让Bean实例之间的耦合从代码层次分类出来,依赖注入是一种优秀的解耦方式。
使用Spring IoC容器的三个基本要点:
- 应用程序的各种组件面向接口编程。面向接口编程可以将组件之间的耦合关系提升到接口层次,从而更有利于项目后期的扩展。
- 应用程序的各组件不再由程序主动创建,而是由Spring容器来负责产生并初始化。
- Spring采用配置文件或注解来管理Bean的实现类、依赖关系,Spring容器则根据配置文件或注解,利用反射来创建实例,并为之注入依赖关系。
1,定义接口
Spring推荐面向接口编程。不管是调用者,还是被依赖对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样便于后期的省级,维护。
package Bean;
public interface Person {
public void useAxe();
}
package Bean;
public interface Axe {
public String chop();
}
Spring推荐面向接口编程,这样可以更好地让规范和实现分离,从而提供更好的解耦。对于一个Java EE应用,不管DAO组件,还是业务逻辑组件,都应该先定义一个接口,该接口定义了该组件应该实现的功能,但功能的实现则由其实现类提供。
2,继承接口
package Bean;
public class Chinese implements Person{
private Axe axe;
public void setAxe(Axe axe) {
this.axe = axe;
}
public void useAxe() {
System.out.println(axe.chop());
}
}
package Bean;
public class StoneAxe implements Axe{
public String chop() {
return "石头斧头砍柴好慢啊";
}
}
上面程序实现了Person接口中的useAxe()方法,实现该方法时调用了axe的chop()方法,这就是典型的依赖关系。Java中大部分都是依赖方式,Spring容器的最大作用就是以松耦合的方式来管理这种调用关系。
3,编写XML
到目前为止,程序仍然不知道Chinese类和哪个Axe实例耦合,Spring需要使用XML配置文件来指定实例之间的依赖关系。Spring采用了XML配置文件,从Spring2.0开始,Spring推荐采用XML Schema来定义配置文件的语义约束。当采用XML Schema来定义配置文件的语义约束时,还可利用Spring配置文件的扩展性进一步简化Spring配置。
<?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.xsd">
<bean id="chinese" class="Bean.Chinese">
<property name="axe" ref="stoneAxe"/>
</bean>
<bean id="stoneAxe" class="Bean.StoneAxe">
</bean>
</beans>
- id:指定该Bean的唯一标识,Spring根据id属性值来管理Bean,程序通过id属性值来访问该Bean实例,Spring也通过Bean的id属性值管理Bean与Bean之间的依赖。
- class:指定该Bean的实现类,此处不可再用接口,必须使用实现类,Spring容器会使用XML解析器读取该属性值,并利用反射来创建该实现类的实例。
Spring管理Bean很灵巧,Bean与Bean之间的依赖关系放在配置文件里组织,而不是写在代码里。通过配置文件的指定,Spring能精确地为每个Bean的成员变量注入值。
Spring会自动监测每个<bean.../>定义里的<preperty.../>元素定义,Spring会调用默认的构造器创建Bean实例之后,立即调用对应的setter方法为Bean的成员变量注入。
4,编写主程序
package Action;
import Bean.Person;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) throws BeansException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Person chinese = context.getBean("chinese", Person.class);
chinese.useAxe();
}
}
上面代码实现了创建Spring容器,并通过Spring容器来获取Bean实例。Spring容器就是一个巨大工厂,它可以生产出所有类型的Bean实例。程序获取Bean实例的方法是getBean()。一旦通过Spring容器获得了Bean实例之后,如何调用就是按照普通的Java类使用就行了。
主程序调用Person的useAxe()方法时,该方法的方法体内需要使用Axe实例,但程序没有任何地方将特定的Person实例和Axe实例耦合在一起(程序没有为Person实例传入Axe实例,Axe实例由Spring在运行期间注入)。
Person实例不仅不需要了解Axe实例的实现类,甚至无须了解Axe的创建过程。Spring容器根据配置文件,创建Person实例时,不仅创建了Person对象,并为该对象注入它所以来的Axe实例。
5,修改实例
假如系统需要改变Axe的实现——这种改变,对于实际开发是很常见的情形,也许是因为技术的改进,也许是因为性能的优化,也许是因为需求的变化......此时只需要给出Axe的另一个实现,而Person接口、Chinese类的代码无须任何改变。
package Bean;
public class SteelAxe implements Axe{
public String chop() {
return "钢斧砍的真快";
}
}
将修改后的SteelAxe部署在Spring容器中,只需要在Spring配置文件中增加下面代码。
<bean id="steelAxe" class="Bean.SteelAxe"/>
然后修改chinese Bean的配置,将原来传入stoneAxe的地方改为传入steelAxe。
<property name="axe" ref="steelAxe"/>
从上面可以看出,因为chinese实例与具体的Axe实现类没有任何关系,chinese实例仅仅与Axe接口耦合,这就保证了chinese实例与Axe实例之间的松耦合——这也是Spring强调面向接口编程的原因。
2.2,构造注入
构造注入:构造方法注入是指IoC容器使用构造方法来注入被依赖的实例。基于构造器的DI通过调用带参数的构造器来实现,每个参数代表着一个依赖。
构造注入的本质:驱动Spring在底层以反射方式执行带指定参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化。
问题:<bean.../>元素默认总是驱动Spring调用无参数的构造器来创建对象,那怎样驱动Spring调用有参的构造参数去创建对象呢?
答案:<constructor-arg.../>子对象,每个<constructor-arg.../>子元素代表一个构造器参数,如果<bean.../>元素包含N个<constructor-arg.../>子元素,就会驱动Spring调用N个参数的构造器来创建对象。
1,继承接口
package Bean;
public class Chinese implements Person{
private Axe axe;
public Chinese(Axe axe){
this.axe = axe;
}
public void useAxe() {
System.out.println(axe.chop());
}
}
上面Chinese类没有提供设置axe成员变量的setter方法,仅仅提供了一个带Axe参数的构造器,Spring将通过该构造器为chinese注入所依赖的Bean实例。
2,编写XML
构造注入的配置文件也需要简单修改,为了使用构造注入,还需要使用<constructor-arg.../>元素指定构造器的参数。
<?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.xsd">
<bean id="chinese" class="Bean.Chinese">
<constructor-arg ref="steelAxe"/>
</bean>
<bean id="stoneAxe" class="Bean.StoneAxe">
</bean>
<bean id="steelAxe" class="Bean.SteelAxe"/>
</beans>
代码中使用<constructor-arg.../>元素指定了一个构造器参数,该参数类型Axe,这指定Spring调用Chinese类带一个Axe参数的构造器来创建chinese实例。
配置<constructor-arg.../>元素时可以指定匹配方法:按索引匹配入参、按类型匹配入参。
该实例的执行效果与设置注入steelAxe时执行效果完全一样。区别在于:创建Person实例中Axe属性的时机不同——设值注入是先通过无参数的构造器创建一个Bean实例,然后调用对应的setter方法注入依赖关系;而构造注入则直接调用有参数的构造器,当Bean实例创建完成后,已经完成了依赖关系的注入。
3,构造参数的选择
<bean id="test" class="Bean.test">
<constructor-arg value="hello"/>
<constructor-arg value="23"/>
</bean>
package Bean;
public class test {
public test(String a,String b){
}
public test(String a, int b){
}
}
这就产生了一个问题:xml配置文件的代码会调用test那个构造函数,答案是第一个构造函数。因为Spring只能解析出来一个“23”的字符串,但它到底转换为那种,只能根据test的构造器来尝试转换。
为了更明确地指定数据类型,Spring允许为<constructor-arg.../>元素指定一个type属性。
<bean id="test" class="Bean.test">
<constructor-arg value="hello" type="java.lang.String"/>
<constructor-arg value="23" type="int"/>
</bean>
2.3,两种注入方式的对比
不管是设值注入还是构造注入,都视为Bean的依赖,接受Spring容器管理,依赖关系的值要么是一个确定的值,要么是Spring容器中其他Bean的引用。
对于singleton作用域的Bean,如果没有强行取消其预初始化行为,系统会在创建Spring容器时预初始化所有的singleton Bean。与此同时,该Bean所依赖的Bean也被一起实例化。
BeanFactory和ApplicationContext实例化Bean的时机:https://shao12138.blog.csdn.net/article/details/113032492
两种依赖注入方式并没有绝对的好坏,只是适应的场景有所不同。
区别 构造注入 设值注入 是否有部分注入 没有部分注入 有部分注入 是否覆盖setter属性 不会覆盖setter属性 会覆盖setter属性 修改是否创建新的实例 任意修改都会创建一个新实例 任意修改不会创建一个新实例 场景 适用于设置很多属性 适用于设置很少属性 设值注入的优点:
- 与传统的JavaBean的写法更相似,程序开发人员更容易理解、接受。通过setter方法设定依赖关系更显得直观、自然。
- 对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring在创建Bean实时时,需要同时实例化其依赖的全部实例,因而导致性能下降。而是用设值注入,则能避免这些问题。
- 尤其是在某些成员变量可选的情况下,多参数的构造器更加笨重。
构造注入的优势:
- 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常需要依赖Datasource的注入。采用构造注入,可以在代码中清晰地决定注入顺序。
- 对于依赖关系无须变化的Bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破坏。
- 依赖关系只能在构造中设定,则只能在构造器中设定,则只有组件的穿见着才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。
建议采用以设置注入为主,构造注入为辅的注入策略。对依赖关系无须变化的注入,尽量采用构造注入。而其他依赖关系的注入,则考虑采用设值注入。
3,循环依赖
3.1,何为循环依赖
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:
循环依赖的种类:
- 构造器的循环依赖:Spring无法处理,当两个或多个Bean通过构造函数注入彼此时,Spring无法实例化任何一个Bean,因为它们都需要其他Bean的实例才能被实例化,直接抛出 BeanCurrentlylnCreationException异常。
- 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
- 非单例循环依赖:Spring无法处理,Spring在创建Bean时会先创建Bean的实例,然后再注入它的依赖项。但是,在非单例Bean的情况下,如果两个Bean之间存在循环依赖,那么Spring无法确定它们之间的创建顺序,从而导致循环依赖问题的发生。
3.2,Spring解决循环依赖
Spring的单例对象的初始化主要分为三步:
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。
- initializeBean:调用spring xml中的init 方法。
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一、第二步。也就是构造器循环依赖和field循环依赖。
那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。
首先我们看源码,三级缓存主要指:
第一级缓存:singletonObjects缓存,它保存了单例Bean的实例。
第二级缓存:earlySingletonObjects缓存,它保存了单例Bean的提前曝光的对象,也就是还未完全实例化的对象。当一个单例Bean被创建时,它会被放入earlySingletonObjects缓存中。
第三级缓存:singletonFactories缓存,它保存了创建单例Bean的工厂方法。当一个单例Bean被创建时,它会被放入singletonFactories缓存中。
当Spring发现两个Bean之间存在循环依赖时,它会先创建Bean的实例,然后将实例放入第二级缓存中。接着,Spring会将创建Bean的工厂方法放入第三级缓存中,以供后续使用。当一个Bean需要注入另一个Bean时,Spring会从第二级缓存中查找提前曝光的对象,如果找到了,就返回该对象。如果没有找到,Spring会从第三级缓存中获取工厂方法来创建对象,创建完毕后放入第二级缓存中,并从第三级缓存中删除该工厂方法。也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。