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


    前言

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

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

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

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

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

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

    你是怎么使用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

    什么是依赖注入(DI)

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

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

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

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

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

    手动方式:

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

    自动方式:

    • Autowiring

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

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

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

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

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

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

    • 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,一切原来如此!》

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

    BeanFactory:制造Bean的工厂

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

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

    The root interface for accessing a Spring bean container.

    是SpringBean容器的根接口。

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

    1. Resource resource = new ClassPathResource("helloWorld.xml");
    2. BeanFactory beanFactory = new XmlBeanFactory(resource);
    3. 复制代码

    ApplicationContext

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

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

    1. ApplicationContext resource = new ClassPathXmlApplicationContext("helloWorld");
    2. 复制代码

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

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

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

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

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

    BeanDefiniton

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

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

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

    所以如果说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(资源)。

    1. FileSystemXmlApplicationContext res = new FileSystemXmlApplicationContext("helloWorld.xml");
    2. 复制代码

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

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

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

    ResourceLoader代码如下:

    1. public interface ResourceLoader {
    2. String CLASSPATH_URL_PREFIX = "classpath:";
    3. Resource getResource(String var1);
    4. @Nullable
    5. ClassLoader getClassLoader();
    6. }
    7. 复制代码

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

    1. public FileSystemXmlApplicationContext(ApplicationContext parent) {
    2. super(parent);
    3. }
    4. 复制代码

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

    1. public Resource getResource(String location) {
    2. Assert.notNull(location, "Location must not be null");
    3. for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
    4. Resource resource = protocolResolver.resolve(location, this);
    5. if (resource != null) {
    6. return resource;
    7. }
    8. }
    9. if (location.startsWith("/")) {
    10. return getResourceByPath(location);
    11. }
    12. else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
    13. return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    14. }
    15. else {
    16. try {
    17. // Try to parse the location as a URL...
    18. URL url = new URL(location);
    19. return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
    20. }
    21. catch (MalformedURLException ex) {
    22. return getResourceByPath(location);
    23. }
    24. }
    25. }
    26. 复制代码

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

    1. protected Resource getResourceByPath(String path) {
    2. if (path.startsWith("/")) {
    3. path = path.substring(1);
    4. }
    5. return new FileSystemResource(path);
    6. }
    7. 复制代码

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

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

    BeanDefinition的载入

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

    1. public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
    2. super(parent);
    3. this.setConfigLocations(configLocations);
    4. if (refresh) {
    5. this.refresh();
    6. }
    7. }
    8. 复制代码

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

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

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

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

    注册BeanDefinition

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

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

    在DefaultListableBeanFactory类中提前定义的beanDefinitionMap。

    1. private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
    2. 复制代码

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

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

    1. this.beanDefinitionMap.put(beanName, beanDefinition);
    2. 复制代码

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

    1. @Override
    2. public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
    3. throws BeanDefinitionStoreException {
    4. Assert.hasText(beanName, "Bean name must not be empty");
    5. Assert.notNull(beanDefinition, "BeanDefinition must not be null");
    6. if (beanDefinition instanceof AbstractBeanDefinition) {
    7. try {
    8. ((AbstractBeanDefinition) beanDefinition).validate();
    9. }
    10. catch (BeanDefinitionValidationException ex) {
    11. throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
    12. "Validation of bean definition failed", ex);
    13. }
    14. }
    15. BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
    16. if (existingDefinition != null) {
    17. if (!isAllowBeanDefinitionOverriding()) {
    18. throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
    19. }
    20. else if (existingDefinition.getRole() < beanDefinition.getRole()) {
    21. // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
    22. if (logger.isInfoEnabled()) {
    23. logger.info("Overriding user-defined bean definition for bean '" + beanName +
    24. "' with a framework-generated bean definition: replacing [" +
    25. existingDefinition + "] with [" + beanDefinition + "]");
    26. }
    27. }
    28. else if (!beanDefinition.equals(existingDefinition)) {
    29. if (logger.isDebugEnabled()) {
    30. logger.debug("Overriding bean definition for bean '" + beanName +
    31. "' with a different definition: replacing [" + existingDefinition +
    32. "] with [" + beanDefinition + "]");
    33. }
    34. }
    35. else {
    36. if (logger.isTraceEnabled()) {
    37. logger.trace("Overriding bean definition for bean '" + beanName +
    38. "' with an equivalent definition: replacing [" + existingDefinition +
    39. "] with [" + beanDefinition + "]");
    40. }
    41. }
    42. this.beanDefinitionMap.put(beanName, beanDefinition);
    43. }
    44. else {
    45. if (hasBeanCreationStarted()) {
    46. // Cannot modify startup-time collection elements anymore (for stable iteration)
    47. synchronized (this.beanDefinitionMap) {
    48. this.beanDefinitionMap.put(beanName, beanDefinition);
    49. List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
    50. updatedDefinitions.addAll(this.beanDefinitionNames);
    51. updatedDefinitions.add(beanName);
    52. this.beanDefinitionNames = updatedDefinitions;
    53. removeManualSingletonName(beanName);
    54. }
    55. }
    56. else {
    57. // Still in startup registration phase
    58. this.beanDefinitionMap.put(beanName, beanDefinition);
    59. this.beanDefinitionNames.add(beanName);
    60. removeManualSingletonName(beanName);
    61. }
    62. this.frozenBeanDefinitionNames = null;
    63. }
    64. if (existingDefinition != null || containsSingleton(beanName)) {
    65. resetBeanDefinition(beanName);
    66. }
    67. }
    68. 复制代码

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

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

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

    Spring的扩展性

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

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

    Spring的扩展点:BeanFactoryPostProcessor

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

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

    实例化Bean

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

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

    1. //获取Class对象
    2. Class.forName("bean");
    3. //获取构造器
    4. getDeclareConstructor();
    5. //创建对象
    6. newInstance();
    7. 复制代码

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

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

    IoC容器中的Bean生命周期

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

    Bean的生命周期如下:

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

    实际上在对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。

    在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才是树的主干。

    最后

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

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

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

    最后的最后,

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

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

  • 相关阅读:
    GBASE 8s 数据库的恢复
    【计组】指令和运算1--《深入浅出计算机组成原理》(二)
    C++:容器list的介绍及使用
    Python、Rust中的协程
    力扣每日一题60:排序序列
    uni-app实现点击显示隐藏列表,兼容微信小程序
    vue 预览视频
    java142-file类的基本创建
    Spring整合Mybatis
    opencv_5_图像像素的算术操作
  • 原文地址:https://blog.csdn.net/YYniannian/article/details/126068982