• SpringIOC 容器源码面试


    1、 Spring 源码环境搭建过程遇到什么问题

      Spring 源码使用 gradle 管理, 必须先安装 gradle, gradle 版本不能太低。不能低于Spring 项目的 gradle 版本。

      JDK 版本不能太低,比如 jdk8, 后面使用 JDK 11 才成功。

      IDE 根据项目的依赖配置文件 从中央仓库拉去 jar 包很慢, gradle 使用 阿里镜像比较快。

    2、Spring 容器启动流程(一)beanDeinition 注册

    答 :

         首先加载配置文件,可以是xml 或者 Java 配置类, Spring 提供了同意的抽象接口 BeanDefinitationReader 去处理这些配置信息。

       对于不同的载体的配置有不同的实现类, 比如 xml 就有 XmlBeanDefinitionReader 的实现类。

       通过实现一层抽象,实现了配置并解析,注册到容器内。 

    问:

          Spring 将配置解析成什麽后注册到容器。

    答:

          包含 class 名, id, 别名信息,properties属性,还有是否懒加载,是否原型类型初始化init-method配置等 方法名。

    问:

          核心的基本已经有了。

    3. Spring 容器启动流程(二)beanDeinition 注册

    问:

         接下聊 容器的启动流程 哈, 刚才说到 BeanDeinfition 注册到容器了。

         刚说到这个别名信息,这个别名 怎么处理呢

    答:

         正常情况下注册,就是向容器保存的 beanDefinition 信息的 map 中 添加一个键值对, key 是 beanName, value是 BeanDefinition对象。

       然后别名,在 Spring 容器中使用一个map 去保存映射关系。

    4. Spring 容器启动流程(三)BeanFactory容器后处理器

    问:

         继续吧

    答:

         再后面就是 注册和执行 BeanFactory容器后处理器 实例。

    问:

         稍等, 既然说到这个 “容器后处理器”

         你能说下, BeanFactoryPostProcessor 接口类,能做什么事吗?

    答:

         这个处理器,有一个接口方法,这个接口方法会将当前容器引用传递进去。 

       然后,拿到这个容器引用之后,能做的事情,基本不受限制。这一步正常流程的单例实例化还未执行。

       在这一步可以手动注册 BeanDefinition 或者 修改,移除,现有的 BeanDefinition 信息。

       这个接口是 Spring  留给我们的扩展点, 可以面向扩展开放又面向修改关闭。

    问:

         ok 哈,没有问题。

         那怎么去提供 容器后处理器呢? BeanFactoryPostProcessor 该怎么提供给框架呢

    答:

         方式有几种, 最简单的就是,实现 BeanFactoryPostProcessor 接口,然后将实现的类,通过 xml 配置 bean 的方式写到配置文件就可以了。

       容器启动的时候,它会检查 BeaanDefinition 信息, 会将“ 容器后处理器” 接口实现类的配置 抽象出来,然后反射创建实例,并且执行接口方法。

    4. Spring 容器启动流程(四)

    问:

         刚才说到注册 bean 还有执行容器后处理的逻辑,再往后说吧

         再往后就是注册 “Bean后处理器” 的流程了。

    问:

         “Bean后处理器”  是吧。

         那你能给我说下,“Bean 后处理器” 和 “容器后处理器” 的有啥区别吗?

    答:

         "容器后处理器" 是在实例化单例之前就执行的,然后 “Bean后处理器” 是实例化,每个实例的过程中穿插执行的。

       “Bean后处理器” 有两个接口方法,调用点是 实例 init-method 初始化方法执行的前后。“Bean后处理器” 接口还有一些子接口,子接口还提供了实例化前后增强的方法入口等。

       “Bean后处理器” 是Spring 容器和兴拓展点技术,Spring本身提供了很多“Bean后处理器”接口的实现类。

    问:

         那你说下,“Bean后处理器” 技术实现了哪些稍微核心点的功能?

    答:

         如果想获取到 ApplicationContext 实例的话,会去实现接口 ApplicationContextAware 接口。这样就能拿到容器实例,其实就是“Bean后处理器” 处理链中的某一个后处理器实例完成    的。

      就是当前BeanDefinition 定义的 Class, 它创建完实例后,在调用初始化方法 init-method 前后,会执行后处理器的方法。

      其中就会有一个处理器的方法,会去检查当前实例对象是否实现 ApplicationContextAware 接口, 如果实现了话,就调用这个接口的 setApplicationContext 将这个

         容器实例注入进去。

    问:

       完了是吧?

       除了这个注入依赖的容器组件功能之外,还有什么功能是靠 BeanPostProcessor 去完成的?

    答:

       还有很多,比如 解析自动装配的注解 @Autowired @Value, 还有 AOP 面向切面编程,也是依靠 BeanPostProcessor 去完成。

    问:

        行。后边再去聊 AOP 吧。 BeanPostProcessor先过吧。

        容器启动的正常流程还没有聊完,继续聊 正常启动流程。

    答:

        接下来,Spring 容器会创建一个 事件传播器对象,也就是 Spring 提供的 拓展的地方。

    问:

        那这个事件传播器,它有什么作用?

    答:

       Spring 的事件传播器,在Spring容器 生命周期内,在关键阶段接受相应事件并传播。 

             接受事件 就是这个事件传播器了。

             比如说,某个 BeanDefinition 信息处理完成之后, 也就是创建出来对应的单实例后,保存到缓存中, 会发出“事件” 到 “事件传播器” 里。

             然后, 事件传播器 可以注册很多 listener 监听者, 传播器把事件广播出去以后, listener 就收到事件了。

        收到事件,想做啥事,就看代码怎么写了,就是个监听者模式。

      那个 监听者怎么提供给事件传播器?

    答:

      只要 java 类实现了 Spring 的 监听者接口,并且在 xml 配置出这个 bean 就可以了。  

      Spring 容器启动过程中,会根据类型去获取这些 BeanDefinition 信息,并且实例化后注册到事件传播器内。

    问:

      再往后说吧.

    答:

       再往后面,就是预先实例化,预先实例化单实例的流程。

    问:

       这块我觉得是重点,重点说一下这块的逻辑。

    答:

       当容器启动时,加载完 xml 或者 JavaConfig 这些配置, 最终都会转化为 BeanDefinition 对象。 

     存储在容器的某个 map 中,这些BeanDefinitation 定义,在默认情况scope 都是 singleton, 除非指定scope 为 原型 prototype 或者时其他的值。

        针对这个singleton, Spring容器采取的时预先实例化的策略,也就是容器启动完毕后,在 get 的 bean 实例, 都是从缓存内拉取过来。 

        还有一种情况,需要排除,就是手动配置这个 bean 是懒加载的, 懒加载的 bean 会等待第一次 get 的时候去实例化。

     大部分情况不会去配置为懒加载,除非这个 bean 比较特殊,耗费资源,在应用声明周期使用的概率很小,这种时候设置为懒加载。

    问:

       这个实例化的核心流程说一下。

       拿到这个 BeanDefiinition 怎么去创建这个单实例的流程

    答:

          首先, 将 beanDefinition 信息转换为 MergedBeanDefinition, 这一步是处理 beanDefinition。

        就创建一个新的 beanDefinition 对象 叫 MergedBeanDefinition, 然后将 parent beanDefinition 信息拷贝到 MergedBeanDeifition 中,

        然后将当前的 BeanDefinition 覆盖到 MergedBeanDefinition里。这样就获取到包含 paren 信息和包含当前 BeanDefinition 信息的 MergedBeanDefinition定义的。

        再往下看就是当前bean 有没有 配置 depends - on 依赖。 如果配置的话,就先实例化 depends - on 指定的 bean。

        再往下,就是根据 bean 定义的 class 信息 还有构造方法信息,去找合适的构造方法。最后会拿到合适的构造方法,并通过反射完成实例化对象的流程。

        到这步就创建出 beanDefinition 对应的对象。

        这个对象还有很多逻辑要处理,比如 依赖注入,还有 “init-method” 配置的初始化方法的调用等等这些逻辑。

    问:

          这里面最复杂的过程就是“循环依赖",  你能说下 “循环依赖”吗。

    答:

          比如,有 A, B 两个类, A的构造方法有一个参数是 B 类型, B的构造方法有一个参数是 A 类型。

          假设构造方法参数不能为空,那这个 A, B 就没办法实例化,要去实例化 A,就要B 对象, 实例化B 就要 A 对象。

         还有就是,A, B, C 三个类, 或者 更多,A 构造方法依赖 B, B 构造方法 依赖 C, C 构造方法依赖 A。 形成闭环,循环依赖,无解。

    问: 

        Spring 循环依赖有几种情况

    答:

        三种,有原型循环依赖,单例构造方法循环依赖,单例 setter 循环依赖。

    问: 

        那 Spring 能解决这种循环依赖问题吗

    答:

        Spring 只能解决 setter 注入导致的 循环依赖, 解决不了 原型和构造方法导致额循环依赖,只能抛出异常。

    问:

        Spring 怎么判断出来,目前已经发生循环依赖这个问题?从创建的角度中去说这个事

    答:

         先说一下创建的过程,首先根据 beanDefinition 拿到 class 的信息,然后再根据配置 或者默认方式找到 class 合适的构造方法。 

      这一步,如果有配置构造方法的话,就拿配置的构造方法,如果没有,就获取无参的构造方法。然后使用这个构造方法进行反射调用实例化。

      再下一步,就是依赖注入,期间有些 ”bean后处理器执行“, 依赖注入这一步,就把当前对象所依赖的数据全部写入了

      再下一步,就是执行 init - method 初始化方法, 前后也会夹杂执行,一些 "bean 后处理器" 去执行。

      这一步完成后,实例就创建完了。

          其中,依赖注入有两个地方。 第一个是使用构造方法,第二个通过构造方法创建出来实例后,再进行 settter 依赖注入的操作。

      循环依赖是这样的,直接到 spring 容器去获取,如果容器中没有的话,就会根据依赖的 beanDefinition 定义 去实例化依赖的对象。

      如果依赖的 beanName 没有对应的 beanDefinition 定义,就会抛错。

    问:

        你可以详细说一下,是怎么循环依赖是怎么判定的,哈

    答:

        emmm,  Spring 为了发现原型产生的循环依赖,在容器上提供了一个 threadLocal 类型的 set, 用来记录当前线程正在创建的 beanName, 每个对象开始执行

      创建逻辑的时候。都会把对应的 beanName 存放到这个 threadLocal - set 中。 假设原型 A 对象执行到依赖注入这个环节,这一步已经直到 threadLocal 的

      set 中已经包含 A 字段。再继续检查依赖, 发现 A 依赖 B, Spring 先检查 缓存中是否有 B 的实例可以用, 假设没有 B 的实例可以用, 这时候,Spring 

      根据 BeanDefinition 创建的 B 的实例。当处理 B 的实例,发现 B 又去依赖 A。 Spring 还回去检查 缓存中是否有 A 实例可以用,但是当 spring 使用 getBean(A)

      获取 A 对象的时候, 发现 A 的 BeanDefinition 是 prototype 类型, 就需要每次 get 都创建新的。所以又开始创建 A 的实例流程。再次创建 A 实例之前,Spring

      会检查 threadLocal -set 是否有 A 这个字符串,如果有 A, 说明当前线程陷入 原型产生的依赖循环, 然后 spring 抛出一个异常。

      

    问:

        普通单实例构造方法产生的循环依赖怎么发现。

    答:

        逻辑和原型没什么区别,但是单实例保存正在创建的 beanName 的 set 不再是 threadLocal 。

      假设 A 构造方法依赖 B, B 构造方法依赖 A ...., 假设 Spring 先去执行创建 A 对象的逻辑, 再执行创建逻辑一开始,将 A 的 BeanName 保存到 set 中。

      然后在执行,创建 A 过程中,执行到获取构造方法,然后再进行反射调用构造方法之前,先处理构造方法的依赖参数,发现依赖参数是 B 类型, 然后就

      触发执行 getBean(B) 的逻辑。因为 spring 缓存中没有实例化 B, 接下来就去构建 B。 B 构造方法又回去 依赖 A,  所以再次触发 getBean(A) 。因为它没有

      实例化完毕,所以缓存中没有 A 实例, 还是会 触发创建 A 流程, 流程执行到 添加 beanName A 到 创建中 set 时, 因为字符串 A 已经在 set 中, 所以

      会添加失败, spring 就抛异常。

    问:

        那么 Spring 怎么解决, 普通单实例 setter 造成的循环依赖。

    答:

        Spring 靠三级缓存去解决这个问题。

       还是 A 和 B 两个 类,A 中有B, B 中有 A, 他们都通过 setter 方法进行 依赖注入。

       假设 Spring 先去实例化 A 的 实例, 根据 A 的 beanDefinition 定义,拿到 A Class 的无参构造方法, 反射创建出 A 实例对象。

       A 实例对象是一个尚未进行 依赖注入 和 init - method 方法调用逻辑的早期实例, 再进行后续加工处理前,会八种到 ObjectFactory 对象内,

       然后存到 spring 第 3 级缓存中, key 是 beanName, value 是这个 ObjectFactory 对象。外部就可以通过Spring 的第三级缓存拿到这个Obejctfacoty.

       然后再通过 ObjectFactory get 方法拿到刚刚创建的早期对象。

         因为这个对象通过 setter 方式依赖的 B 对象,这一步,Spring 通过使用 getBean(B) 的方式去获取 A 的依赖对象 B, 因为 B 对象尚未实例化, 

       getBean(B) 就变为创建 B 的流程。在处理 B 的依赖, 发现 B 又依赖 A 对象,这时候 通过Spring 调用 getBean(A),但是 A 已经封装为 ObjectFacotry 对象

      在Spring 的三级缓存中,所以 处理 B 依赖 A, 可以通过  ObjectFactory , 完成 B 依赖 A 的处理。

        接下来处理 B 对象,调用 B 对象的 init-method 和 执行 ”Bean后处理器“ 逻辑。

        最后,处理完 B 存放到 Spring 一级缓存。A 也在一级缓存。而二级缓存一旦发生循环依赖,则通过依赖的对象立即清出二级缓存。

        

    问:

        Spring 处理循环依赖三级缓存数据,什么时候升级到 二级缓存。

          

    答:

        从三级缓存中获取的Bean 实例,会拿到它的 ObjectFactory, 调用 ObjectFactory get f方法就拿到早期实例, 返回早期实例之前进行一次缓存升级。

      把三级缓存中的 ObjectFactory 干掉,将早期实例放入二级缓存。

    问:

        为什么要有三级缓存的存在,而不把早期对象放到二级缓存。

    答:

        三级缓存的 ObjectFacoty get 方法做了一些处理,返回实例之前会执行 BeanPostProcessor, 会把 AOP 的 BeanPostProcessor 实现代理增强的逻辑给预先执行。

      会返回代理增强后的实例,否则返回原生早期实例。

          ( Spring为什么用三级缓存而不用二级缓存 : https://blog.csdn.net/foxException/article/details/108712397 )

      

        

         

        

      

                                        

      

      

      

  • 相关阅读:
    五 Servlet 技术
    二进制、八进制、十进制、十六进制之间怎样互相转换?
    HTML中怎样添加地图?
    特殊集合
    集合arraylist
    数组

    gif 命令大全
    for 循环与嵌套
    分支语句(switch case)
  • 原文地址:https://www.cnblogs.com/Jomini/p/13820335.html
Copyright © 2020-2023  润新知