从Spring为什么要用IoC的支点,我撬动了整个Spring的源码脉络!

 2023-01-17
原文作者:一颗剽悍的种子 原文地址:https://juejin.cn/post/7124482061364609038

采菊东篱下,悠然见南山。

前言

有一天,我翻开源码横看竖看,满屏只看到四个字,我看不懂啊。

所以是不是曾和我一样迷失在毫无头绪的源码里,在各种类和方法里翻山越岭,却如同管中窥豹。是的话,要不今晚早点睡?

呸,扯远了,写文章怎么能分神呢。

所以正当我苦恼的时候,我想起了一句话“给我一个支点我就可以撬动整个地球”,阿基米德说的。

咦,突然让我灵机一动,那撬动整个Spring源码的支点是什么呢?或者说Spring作者在开发Spring时的支点是什么呢?这么想让我开始觉得有点意思了。

如果尝试先从使用Spring的角度来看源码,好像事情也并没有那么错综复杂。

202301012015149921.png

你是怎么使用Spring的,你就怎么看Spring源码!

先把镜头拉回到使用Spring后最大变化是什么?毫无疑问是改变了我们编程方式,原来是程序员自己既要定义对象,然后还要通过new的方式创建出对象。而在Spring中则我们只需要定义Bean,然后交给Spring来创建对象(从甲乙方的角度,定义对象是提需求的甲方,实现对象是执行的乙方。那么new的方式是即做甲方又做乙方。而Spring让程序员当了一回甲方)。

而这个动作就是 IOC(Inversion of Control)控制反转 。不过IoC不是一种具体的技术,而是一种设计思想。

那问题来了,IoC是怎么来的?

其实IoC可以追溯到一个原则,好莱坞原则。把IOC对应到这一原则,一切都很好理解,就是在IoC中,所有的对象都是被动的,对象初始化和调用都由Spring负责。

“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞原则。在好莱坞,把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项的完全控制,演员只能被动式的接受公司的差使,在需要的环节中,完成自己的演出。—— 好莱坞原则

Bean是Spring中的图纸

为什么要有Bean,或者说Bean是什么呢?

我们知道无论是控制反转还是依赖注入都是围绕Object,而对应到Spring中也就是Bean;Bean其实是包装了的Object(对象)

所以Bean是Spring中最核心的东西,Bean`好比有了图纸,才能在Spring上建各种不同的的房子、车子。

可以说一个Bean就是经过了我们定义最后由Spring所创建的Object

(所以对于程序而言是由一堆的Object构成,那么在Spring所构成的程序里就是一堆Bean

202301012015155632.png

什么是依赖注入(DI)

前面说Bean是图纸,那么我们就是拿着图纸的设计师,因为在Spring中创建Bean的工作是由Spring来承担的,所以我们要做的是定义Bean信息。如下图所示,我们通过XML,注解的方式来定义Bean的信息。

202301012015160903.png

202301012015165774.png

而这个我们手动的方式定义Bean的方式,我们称为是依赖注入(DI)的方式。

将程序运行需要依赖外部的资源,从外部注入到内部,而容器加载了外部的文件、对象、数据怼到程序内的对象,此时程序内外对象之间的依赖关系,就是 依赖注入

所以说IoC是设计思想,那么DI就是具体的实现方式。

上面我们是我们常定义的是手动方式,而汽车都有分手动挡和自动挡,那么依赖注入方式也有分。

手动方式:

  • XML 资源配置元信息
  • Java 注解配置元信息
  • API 配置元信息

自动方式:

  • Autowiring

依赖除了有按方式,也有按注入类型区分:

  • Setter注入
  • 构造器注入
  • 接口注入
  • 方法注入

但是我们在开发中定义Bean信息的方式还可以有properties,yaml等等。

202301012015170085.png

那么如果我们想要更多配置方式怎么呢?

其实Spring已经想好了,通过定义规范来约束所有需要解析操作的接口BeanDefinitionReader

202301012015176706.png

如下图BeanDefinitionReader类结构图所示,可以看到那些实现BeanDefinitionReader接口的类。

202301012015180477.png

  • XmlBeanDefinitionReader:读取XML文件定义的Bean。
  • PropertiesBeanDefinitionReader:可以从属性文件Resource,Property对象等读取Bean。
  • GroovyBeanDefinitionReader:可以读取Groovy语言定义的Bean。

所以你会发现Spring中的Bean其实就是用来统一规范的格式,纵使你有不同的Bean定义方式,其原理也只不过解析方式不一样,那么只要定义Bean方式能解析成Spring读的懂得“Bean格式”,那么就能在Spring上运行。

此时如果你回望Java虚拟机中通过Class Loader(类装载器子系统)只能加载.class(字节码)文件格式的想法简直如出一辙。

这里不再赘述,感兴趣的朋友可以去看前不久更新的 《我从冯·诺依曼计算机体系,追溯到了JVM,一切原来如此!》

JVM通过字节码存储格式统一了所有平台,而字节码就是构成了平台无关性的基石 。如:Python、Ruby、Scala等语言只要能转为字节码格式,那么就都能运行在JVM上面。(可以说JVM野心真大!)

——《我从冯·诺依曼计算机体系,追溯到了JVM,一切原来如此!》

202301012015184068.png

(所以Spring的胃口也不小,也许在不久的将来,Spring如此生猛的生态下,会不会演化成一门面向Spring开发的语言)

BeanFactory:制造Bean的工厂

当我们自定义了Bean的信息后就是来到给Spring制造Bean时,Spring提供了暴露在外获取bean的入口BeanFactory容器,顾名思义是Bean的工厂,负责生产和管理各个bean实例。

202301012015188189.png

我们进入源码可以看到开头一句注释:

The root interface for accessing a Spring bean container.

是SpringBean容器的根接口。

BeanFactory常见的实现方式类XmlBeanFactory,可以从classpath或文件系统等获取资源。

    Resource resource = new ClassPathResource("helloWorld.xml"); 
    BeanFactory beanFactory = new XmlBeanFactory(resource);

ApplicationContext

除了BeanFactory你还会注意到一样具有定位Bean功能的ApplicationContext

ApplicationContextClassPath获取XML配置文件资源是通过XmlApplicationContext

    ApplicationContext resource = new ClassPathXmlApplicationContext("helloWorld");

另外还有两种常见的实现方式:

  • FileSystemXmlApplicationContext:从文件系统中的XML配置文件读取上下文。
  • XmlWebApplicationContext:从Web应用的XML文件读取上下文。

那直接来到源码看ApplicationContext的类结构图,可以发现它继承了多个接口,同时也继承BeanFactory

2023010120151934010.png

所以BeanFactory可以说是最基本的IoC容器,而ApplicationContext是它的子类,成为子类实际上就是为了有更丰富的特性和功能。

那么又会有一个问题,Bean有这么多信息,我们怎么方便的在生产Bean的时候直接使用?

BeanDefiniton

其实在到BeanFactory还会使用到BeanDefinition来描述Bean实例的信息。

在使用<bean>标签的时,你会发现有class、scope、lazy-init等的配置属性,其实是Spring对所管理的Bean的一系列描述,而BeanDefinition中就提供了与之相对应的beanClass、scope、lazylnit属性。

(简单的说,BeanDefinition就是存储着我们给Bean定义的元数据)

2023010120151980111.png

所以 如果说Class描述的是类的信息,那么BeanDefinition描述的是对象的信息。

那么有了BeanDefinition再制造Bean的时候就可以直接使用,比如getBeanClassName

BeanDefinition类中开头注释的可以看到这么一段话。

A BeanDefinition describes a bean instance

BeanDefinition描述bean实例。

IoC容器的初始化过程

从上面我们知道了,BeanDefinition描述了Bean的信息,那么意味BeanDefinition装载着Bean的信息,从而加载到BeanFactory。

那么问题又来了,在IoC容器的初始化过程中BeanDefinition是怎么装载进容器里的?

BeanDefinition的定位过程

而这就得把镜头拉到Bean容器的初始化过程。而第一个过程是 定位的过程 ,我们直接来看常用的ApplicationContext的FileSystemXmlApplicationContext可以从文件读入Resource(资源)。

    FileSystemXmlApplicationContext res = new FileSystemXmlApplicationContext("helloWorld.xml");

我们可以来看ApplicationContext的实现是怎么完成这个Resource定位过程的。

继续打开类结构图一探究竟 (一颗剽悍的种子温馨提示:下图如果看不清,可以右键打开图片浏览)

2023010120152027312.png

可以发现很醒目的提供了ResourceLoader对外部资源的访问接口。从而可以加载文件、网络、流中的资源。

2023010120152075113.png

ResourceLoader代码如下:

    public interface ResourceLoader {
        String CLASSPATH_URL_PREFIX = "classpath:";
    
        Resource getResource(String var1);
    
        @Nullable
        ClassLoader getClassLoader();
    }

FileSystemXmlApplicationContext继承了AbstractApplicationContext具备了ResourceLoader读入Resource定义的BeanDefinition的能力。

    public FileSystemXmlApplicationContext(ApplicationContext parent) {
        super(parent);
    }

对于获取Resource的具体过程,可以看DefaultResourceLoader的getResource

    public Resource getResource(String location) {
       Assert.notNull(location, "Location must not be null");
    
       for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
          Resource resource = protocolResolver.resolve(location, this);
          if (resource != null) {
             return resource;
          }
       }
    
       if (location.startsWith("/")) {
          return getResourceByPath(location);
       }
       else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
          return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
       }
       else {
          try {
             // Try to parse the location as a URL...
             URL url = new URL(location);
             return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
          }
          catch (MalformedURLException ex) {
    
             return getResourceByPath(location);
          }
       }
    }

getResource最后返回的getResourceByPath会被子类FileSystemXmlApplicationContext实现。而方法返回的是一个FileSystemResource对象。

    protected Resource getResourceByPath(String path) {
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
    
        return new FileSystemResource(path);
    }

通过这个FileSystemResource,Spring进行了相关的I/O操作,完成了BeanDefinition的定位。

而在定位过程完成后,就为BeanDefinition创作了载入的条件,定位过程还没有将具体数据读入,而就到了需要BeanDefinitoin载入和解析中完成。以甲乙方为例,甲方提出需求,乙方执行。乙方执行前,要先拿到甲方提出需求的具体资料才行。定位过程就好比已经拿到甲方需求资料。

BeanDefinition的载入

在前面FileSystemXmlApplicationContext的初始化过程中,其实会发现有一个refresh方法,这个方法就是在当调用容器时载入BeanDefinition的入口。

    public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
        super(parent);
        this.setConfigLocations(configLocations);
        if (refresh) {
            this.refresh();
        }
    }

refresh对容器的启动来说是很重要的方法,可以在AbstractApplicationContext抽象类中找到refresh()。如下所示,它实际上描述了ApplicationContext的初始化过程。有BeanFactory的更新等等。

2023010120152121114.png

refresh可以说是对ApplicationContext初始化的模板。

所以整个载入的过程是把用户定义的Bean转化为IoC容器内部数据结构,而容器内部的数据结构就是BeanDefinition。

refresh就像重启电脑主机一样的重启容器,在建立IoC容器后,对容器的一系列准备工作,也就是初始化过程,其中就包括BeanDefinition载入。

注册BeanDefinition

第三个过程是IoC容器注册BeanDefinition的过程,我们定义的BeanDefinition信息虽然建立起了容器对应的数据形式,但此时这些数据还不能供IoC容器直接使用,还需在IoC容器中对BeanDefinition数据进行注册。

而注册的方式实际上就是通过一个Map来存储BeanDefinition。(可以看出所谓的注册,其实是为了让IoC容器更方便获取和使用)

在DefaultListableBeanFactory类中提前定义的beanDefinitionMap。

    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

DefaultListableBeanFactory中实现了 BeanDefinitionRegistry的接口。这个接口定义了 registerBeanDefinition方法来将BeanDefinition向容器的注册。

关键代码并不复杂,其实就是将准备好的BeanDefinition加到Map中。(一颗剽悍的种子翻阅到这行关键代码是被遮掩在各种if判断中,最后发现就在末尾处后不由感到欣慰的笑,不愧是你)

    this.beanDefinitionMap.put(beanName, beanDefinition);

完整的registerBeanDefinition方法如下,包括了处理遇到其他情况,例如遇到同名的BeanDefinition的处理。

    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
          throws BeanDefinitionStoreException {
    
       Assert.hasText(beanName, "Bean name must not be empty");
       Assert.notNull(beanDefinition, "BeanDefinition must not be null");
    
       if (beanDefinition instanceof AbstractBeanDefinition) {
          try {
             ((AbstractBeanDefinition) beanDefinition).validate();
          }
          catch (BeanDefinitionValidationException ex) {
             throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                   "Validation of bean definition failed", ex);
          }
       }
    
       BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
       if (existingDefinition != null) {
          if (!isAllowBeanDefinitionOverriding()) {
             throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
          }
          else if (existingDefinition.getRole() < beanDefinition.getRole()) {
             // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
             if (logger.isInfoEnabled()) {
                logger.info("Overriding user-defined bean definition for bean '" + beanName +
                      "' with a framework-generated bean definition: replacing [" +
                      existingDefinition + "] with [" + beanDefinition + "]");
             }
          }
          else if (!beanDefinition.equals(existingDefinition)) {
             if (logger.isDebugEnabled()) {
                logger.debug("Overriding bean definition for bean '" + beanName +
                      "' with a different definition: replacing [" + existingDefinition +
                      "] with [" + beanDefinition + "]");
             }
          }
          else {
             if (logger.isTraceEnabled()) {
                logger.trace("Overriding bean definition for bean '" + beanName +
                      "' with an equivalent definition: replacing [" + existingDefinition +
                      "] with [" + beanDefinition + "]");
             }
          }
          this.beanDefinitionMap.put(beanName, beanDefinition);
       }
       else {
          if (hasBeanCreationStarted()) {
             // Cannot modify startup-time collection elements anymore (for stable iteration)
             synchronized (this.beanDefinitionMap) {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
                updatedDefinitions.addAll(this.beanDefinitionNames);
                updatedDefinitions.add(beanName);
                this.beanDefinitionNames = updatedDefinitions;
                removeManualSingletonName(beanName);
             }
          }
          else {
             // Still in startup registration phase
             this.beanDefinitionMap.put(beanName, beanDefinition);
             this.beanDefinitionNames.add(beanName);
             removeManualSingletonName(beanName);
          }
          this.frozenBeanDefinitionNames = null;
       }
    
       if (existingDefinition != null || containsSingleton(beanName)) {
          resetBeanDefinition(beanName);
       }
    }

完成了BeanDefinition注册,那么就完成了IoC容器的初始化过程。意味着IoC容器已经建立了整个Bean的配置信息。而且BeanDefinition就可以通过beanDefinitionMap被获取和使用。

(不过需要注意的是,这里只是IoC容器的初始化过程,并不包含Bean依赖注入的实现。在Spring IoC设计中,Bean定义的依赖注入方式和IoC依赖注入是两个独立过程。IoC容器的依赖注入通常发生在getBean向容器索取Bean的时候。)

至此这些信息就成了容器建立依赖反转的基础。

Spring的扩展性

那么就到了IoC实例化Bean,然后使用对象,最后销毁对象的过程。

2023010120152186215.png

但实际上Spring框架强大的地方还在于它的扩展性,所以Spring在这整个制造Bean的过程中加入许多的扩展点,而正是这些扩展点让我们可以即操作Spring也可以在这基础上去做更多的自定义操作(可以说是为所欲为)。

Spring的扩展点:BeanFactoryPostProcessor

BeanFactoryPostProcessor接口是Spring初始化BeanFactory时对外暴露的扩展点。调用BeanFactoryPostProcess方法时,Bean刚被解析成 BeanDefinition对象,所以bean还没有实例化,就可以在bean实例化之前通过BeanFactoryPostProcessor读取配置元数据,并可以根据需要进行修改。

2023010120152236316.png

BeanFactoryPostProcessor是对实例之前的任何Bean之前对其进行更改。

实例化Bean

直到经过BeanFactoryPostProcessor,才终于到了我们的实例化Bean。而通常框架在实例化时并不会使用new的方式,而是使用更灵活的反射。

而反射的具体实现,我们也相当熟悉。

    //获取Class对象
    Class.forName("bean");
    //获取构造器
    getDeclareConstructor();
    //创建对象
    newInstance();

到了实例化后,就是到了真正对象的使用,最后销毁过程。

2023010120152290117.png

但是在这个过程中还有跟应用相关的许多固定工作,如建立数据库的连接,网络的连接等等。同时等到最后程序结束时相应的要销毁这些固定的工作来释放资源。

IoC容器中的Bean生命周期

所以Spring的IoC容器制定了Bean从实例化到销毁的过程。Bean容器的实现过程就是IoC管理的Bean的生命周期。

Bean的生命周期如下:

  • 实例化Bean。
  • 设置Bean实例属性。
  • 调用Bean初始化方法。
  • 应用通过IoC容器使用Bean。
  • 容器关闭时的Bean销毁。

2023010120152339918.png

实际上在对Bean设置完属性后,已经差不多可以使用了,但是在Bean初始化方法的前后还有Bean的前置和后置处理器扩展点。

BeanPostProcessor:AOP的关键

BeanPostProcessor后置处理器有两个方法,一个是BeanPostProcessor:before,另一个是BeanPostProcessor:after分别用于前后执行。BeanPostProcessor跟前面的BeanFactoryProcessor一样是Spring留给我们的扩展点。但是不同是BeanFactoryProcessor是对Bean实例化之前的扩展。而BeanPostProcessor是对Bean实例化之后的扩展。

但你发现这些扩展点的特性跟AOP(面向切面)的代理很相似。e而实际上BeanPostProcessor后置处理器就是AOP实现的关键。

我们可以从继承BeanPostProcessor接口的AbstractAutoProxyCreator抽象类里找到postProcessBeforeInstantiation后置处理器的createProxy()方法,而在此方法中找到调用了关键的代码getProxy()方法,而这个方法定义自AopProxy接口。

AOP中的JDKcglib动态代理都是继承AopProxy。

2023010120152406719.png

在AopProxy接口的上方注释是这样描述的。

Delegate interface for a configured AOP proxy, allowing for the creation of actual proxy objects.

用于配置的AOP代理的委托接口,允许创建实际的代理对象。

所以当你再回过头看Spring,都是由IoC所主导的,纵使Spring的核心还有AOP,但AOP是以IoC来作为基础的。AOP也只不过是IoC中的分支,而IoC才是树的主干。

2023010120152456820.png

最后

或许会如梦初醒,再问Spring为什么要用IOC?

从一开始改变了我们编程的方式,即符合人类永不停息追求效率以及更方便(懒),也符合软件工程永远追求的目标,降低系统之间、模块之间和对象之间的耦合度。包括了在扩展性方面提供让整个Spring生态生机勃勃的扩展点。

可以说 Spring改变了你以IoC的方式开发的同时,你的代码也交由了Spring进行管理。

最后的最后,

如果纯粹盲目的去看Spring源码,你对Spring的知识就是一块一块的碎片,既不系统,也不结构化,更别说融会贯通了。你会觉得自己需要好好地梳理脉络,系统地掌握知识。你的这种感觉一定很强烈吧。 源码本身只不过是解决问题的细节而已,而IoC思想才是主导Spring脉络的支点。不要为了捡芝麻,而丢了西瓜。

好了,这些天,就到这里了。

我是一颗剽悍的种子, 怕什么真理无穷,进一寸,有进一寸的欢喜 。感谢各位朋友的: 关注点赞收藏评论 ,我们下回见!

创作不易,勿白嫖。

一颗剽悍的种子 | 文 【原创】