# 创建项目

我们现在 IDEA 里面创建一个项目,之后都会在这个项目里写代码讲解。

选择 maven ,不需要选择什么 quick-start ,直接 next ,创建项目,也就是只需要 maven

导入 spring 依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.13</version>
</dependency>

resource 中创建一个 Spring 配置文件,命名随便,我这里写的是 application.xml ,复制相应的 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>

此时 IDEA 会检测到该配置文件,然后右上角有个 Configure application context ,点击就行。

在主方法中加载配置文件:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
    }
}

这里有个细节,选的是 XmlApplicationContext ,指定加载 Xml 文件。

随便创建一个实体类,放在 bean 包下:

public class Student {
    String name;
    int age;
}

在配置文件中添加这个 bean(就是为了让 IoC 容器来管理这个 bean),放在 <beans> 标签里面:

<bean name="student" class="com.bean.Student" />

name (也可以是 id ),全局唯一。

# 控制反转

还是用中文做标题,不然感觉空荡荡的。

上文讲到 Student 配置到 IoC 容器中,实现了控制反转,如果想要获得这个类的对象:

// 通过 bean 的名称获取对象
Student s1 = (Student) context.getBean("student");
Student s2 = (Student) context.getBean("student");
System.out.println(s1 == s2); // ture

两次获取是同一个对象,默认情况下,IoC 容器管理的 JavaBean 是单例模式,当然也可以修改:

<bean name="student" class="com.test.bean.Student" scope="prototype"/>

prototype 是原型模式,使得每次都会创建一个新的对象。

# bean 生命周期

<bean> 标签可以设置 bean 对象的初始化方法和销毁方法:

public class Student {
    // 一些属性
    
    private void init() {
        System.out.println("执行初始化。。。");
    }
    private void destroy() {
        System.out.println("执行销毁。。。");
    }
    public Student() {
        System.out.println("执行构造方法");
    }
}

然后在配置文件中编写:

<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy" scope="prototye"/>

其实就是 init-method 属性和 destory-method 属性的赋值。

主程序编写代码测试一下:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    context.close();  // 手动销毁容器
}
// 可以多创建几个感受一下

如果控制台输出中文有乱码,记得去 setting->encoding 改成 utf-8

# 依赖注入

向 Bean 的成员属性进行赋值,将我们预先给定的属性注入到对象中。

  • 注入基本属性

使用 <property> 标签,前提是这个属性必须有一个 set 方法(使用 Lombok 即可)

<bean name="student" class="com.bean.Student">
	<property name="name" value="cyan"/>
</bean>
  • 注入其他 bean 对象

如果属性是一般的实体类(比如 Card 类之类的),其实是一样的,只不过需要将其注册为 bean 即可,使用 ref 属性进行引用:

<bean name="card" class="com.bean.Card" />
<bean name="Student" Class="com.bean.Student">
	<property name="card" ref="card" />
</bean>
  • 注入集合
<bean name="student" class="com.bean.Student">
    <property name="list">
        <list>
            <value type="double">100.0</value>
            <value type="double">95.0</value>
            <value type="double">92.5</value>
        </list>
    </property>
</bean>

这个集合在 Student 中可以是数组( int[] ),也可以是 Listname 就是集合在 Student 的属性名。 <value> 标签中,如果集合的类型明确,也可以不需要 type

如果集合内部是 bean 对象:

<bean name="card" class="com.bean.Card" scope="prototype">
    <property name="id" value="1234" />
</bean>
<bean name="student" class="com.bean.Student" scope="prototype">
    <property name="list">
        <list>
            <ref bean="card" />
        </list>
    </property>
</bean>

个人感觉吧,应该不会这么设计,就是把一个在项目中起 entity 的角色的 bean 这么玩。

如果集合是 Map,假设是 Map<String,Double> map

<bean name="student" class="com.bean.Student">
    <property name="map">
        <map>
            <entry key="语文" value="100.0"/>
            <entry key="数学" value="80.0"/>
            <entry key="英语" value="92.5"/>
        </map>
    </property>
</bean>

通过 xml 配置,还可以使用自动装配:

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>

自动装配会根据 set 方法中需要的类型,自动在容器中查找是否存在对应类型或是对应名称以及对应构造方法的 Bean,比如我们上面指定的为 byType ,那么其中的 card 属性就会被自动注入类型为 Card 的 Bean

指定构造方法创建 bean(其实感觉和 property 标签一样)

<bean name="student" class="com.bean.Student">
        <constructor-arg name="name" value="小明"/>
        <constructor-arg index="1" value="18"/>
    </bean>

我们现在用的都是 xml 配置,IoC 还可以是 Java 配置,注解配置,之后会讲。

# IoC 要点

讲原理的话,我自己也一知半解的,所以标题写的就是要点。之后深入学习再填 IoC 原理的坑。我们先快速过一下有哪些重要的类。

Spring Bean 创建是典型的工厂模式,顶层的结构设计主要围绕 BeanFactoryxxxRegistry

  • BeanFactory 定义功能规范:获取 bean 对象,判断是否是单例,判断容器是否包含 bean。
  • BeanRegistry 将 Bean 注册到 BeanFactory 中。

一个 IoC 容器最基本的行为在 BeanFactory 接口已经被定义好了,也就是说,所有 BeanFactory 实现类都应该具备管理 Bean 的基本能力。

# BeanDefinition

Bean 对象存在依赖嵌套等关系,所以设计了 BeanDefinition 用来对 Bean 对象及关系:

  • BeanDefinition 定义各种 Bean 对象及其互相关系。
  • BeanDefinitionReaderBeanDefinition 解析器。
  • BeanDefinitionHolderBeanDefinition 包装类,用来存储 BeanDefinitionname

Bean 对象在 Spring 实现中是以 BeanDefinition 来描述的。

一个 Bean 的加载流程为:

先拿到 BeanDefinition 定义,选择对应的构造方法,通过反射进行实例化,然后进行属性填充(依赖注入),完成之后再调用初始化方法(init-method),最后如果存在 AOP,则会生成一个代理对象,最后返回的才是我们真正得到的 Bean 对象。

# ApplicationContext

ApplicationContext 是 IoC 接口设计和实现,它必须继承 BeanFactory 对 Bean 规范进行定义,除了对 Bean 的管理,还应该包含:

  • 访问资源:对不同方式的 Bean 配置(资源)进行加载。(实现 ResourcePatternResolver 接口)。
  • 国际化:支持信息源,可以实现国际化(实现 MessageSource 接口)
  • 应用事件:支持应用事件。(实现 ApplicationEventPublisher 接口)

从加载源(xml,groovy,annotation 等)来看,接口的实现:

  • FileSystemXmlApplicationContext :从文件系统下的若干个 xml 配置文件加载上下文,也就是系统盘符中加载 xml 配置文件。
  • ClassPathXmlApplicationContext :从类路径下的若干个 xml 配置文件加载上下文,适用为 xml 配置方式。
  • AnnotationConfigApplicationContext :从若干个基于 Java 配置类中加载上下文定义,适用于 Java 注解的方式。
  • ConfigurableApplicationContext :扩展于 ApplicationContext,新增 refres()close() 方法。在应用上下文关闭的情况下调用 refresh () 即可启动应用上下文,在已经启动的状态下,调用 refresh () 则清除缓存并重新装载配置信息,而调用 close () 则可关闭应用上下文。

可以加载多个配置文件:

new ClassPathXmlApplicationContext("aspects.xml", "daos.xml", "services.xml");
// 源码:ClassPathXmlApplicationContext (String... configLocations)

# 循环依赖

A 中需要注入 B,B 中需要注入 A,这时就会出现 A 还未创建完成,就需要 B,而 B 这时也没创建完成,因为 B 需要 A,而 A 等着 B,这样就只能无限循环下去了,所以就出现了循环依赖的问题。

如果对象在原型模式创建就会失败,抛出异常:

if (isPrototypeCurrentlyInCreation(beanName)) {  
	throw new BeanCurrentlyInCreationException(beanName);  
} 
// 如果不抛异常,你猜是先 StackOverflow 还是 OutOfMemory?

但是在单例模式创建,单例模式可以解决这个问题。

如果是单例创建对象, getBean() 最终会去调用 getSingleton() 方法,该方法可以自动解决循环依赖问题:

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
  	// 先从第一层列表中拿 Bean 实例,拿到直接返回
    if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
      	// 第一层拿不到,并且已经认定为处于循环状态,看看第二层有没有
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized(this.singletonObjects) {
              	// 加锁再执行一次上述流程
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                      	// 仍然没有获取到实例,只能从 singletonFactory 中获取了
                        ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                          	// 丢进 earlySingletonObjects 中,下次就可以直接在第二层拿到了
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

处理其实很简单,代码只是因为为了保证并发安全存在一个上锁导致代码量变多了的问题。

单例模式为了保证并发安全,在 getSingleton 都会上锁的,双层 if 保证并发安全。

我们需要先了解三个属性(都是哈希集合):

  • singletonObject :缓存 创建完成单例 Bean 的地方。
  • earlySingleObject :映射 Bean 的早期引用,只是一个初始化的对象,还没有注入
  • singletonFactories :映射创建 Bean 的原始工厂

最后两个都是借用一下,创建完成就清除掉了。假设最终放在 ** singletonObjects 的 Bean 是你想要的一杯 “凉白开”。那么 Spring 准备了两个杯子,即 singletonFactoriesearlySingletonObjects 来回 “倒腾” 几番,把热水晾成 “凉白开” 放到 singletonObjects ** 中。

源码的过程:

  • 当第一层没有获得 Bean,会判断 Bean 是否处于创建状态 isSingletonCurrentlyInCreation() 方法。
  • 然后去第二层获取 Bean,如果没有,说明需要载入或者已经载入到第三层,然后将其放到第二层,并从第三层删除。

getBean 触发依赖注入。

我们还是假设 A,B 互相依赖,通过例子讲解:

  1. 假设 A,B 载入配置,此时都在 singletonFactories 里面,但是都还没有注入。
  2. 通过 getBean() 想要获取 A 的单例对象,此时需要注入 B。
  3. 发现 B 依赖 A,最终会调用 getSingleton ,发现 A 在第三层 singletonFactories ,就将 A 移到第二层并返回给 B,此时 B 就引用到了 A,即完成依赖注入。
  4. B 完成依赖注入后就可以丢进 singletonObjects ,从第二层删除。
  5. 最后将 B 注入到 A,完成 A 的依赖注入,至此循环依赖完成。

我的理解(可能不对):其实最重要的就是 AB 可以处于创建状态以及单例,在创建状态时可以进行相互引用,而不是无限套娃,创建状态时,A 还没有注入,其 B 属性就是 null,同理 B 也是,最后在相互引用。

# 参考

https://www.yuque.com/qingkongxiaguang/spring/rlgcf7#f8881207

https://juejin.cn/post/6844904122160775176

https://pdai.tech/md/spring/spring-x-framework-ioc-source-1.html