Loading...
墨滴

jrh

2021/10/25  阅读:49  主题:橙心

Java 面试八股文之框架篇(一)

前言

这是系列文章【 Java 面试八股文】框架篇的第一期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,带你理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过下方给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多说,我们正式开始吧!

往期文章

框架篇(一)

1、Spring IoC 容器的实现原理是什么?


在介绍什么是 IoC 容器之前,我们先来看一个例子:

假设我们的项目中有这样几个类:

与用户相关的类:

  • User
  • UserDao
  • UserService

与订单相关的类:

  • Order
  • OrderDao
  • OrderService

且这些类的对象之间的依赖关系如下图所示:

如果我们要调用 orderService.createOrder() 方法,就需要确保 orderService 依赖的对象要先于 orderService 创建——否则你就会摊上 NullPointerException 这样的麻烦。所以,我们需要在程序中手动维护这些对象的创建顺序与依赖关系。

上面的示例并不复杂,你可以很快就捋清这些对象的创建顺序与依赖关系。但是如果我们的项目需要管理几十个甚至上百个对象,并且这些对象之间有循环依赖,那么维护这些对象的依赖关系就变成了一件相当头疼的事儿。

所以,Spring 就出现了。

你一定听说过,Spring 框架最核心的两个特性就是 IoC 与 AOP。而 Spring IoC 容器的功能就是帮助我们管理这些 Bean 之间的依赖关系。Spring 将传统方式上由我们手动创建并调用的 Bean 的控制权交了 IoC 容器,帮助我们管理这些 Bean,解放了我们的双手,并简化了开发。

先来说一下什么是 IoC,IoC(Inversion of Control)即:控制反转。控制反转这个名词看起来高大上,实际上并不难理解。我们知道,当对象 A 依赖了对象 B ,要先创建对象 B 再去创建对象 A,这就是 “下层建筑控制上层建筑”。而控制反转则是 “上层建筑控制下层建筑” 的一种思想,当我们需要对象 A 时,IoC 容器会主动创建 A 所依赖的对象,并 “注入” 到 A 中。这里面的 “注入” 就是 DI(Dependency Injection)。很多人认为 IoC 与 DI 是同一个东西,不过二者还是有一定区别的,IoC 是一种思想,DI 是 Spring IoC 的一种实现手段。

下面的代码是我自己实现的一个简易版 IoC 容器:

这个 Demo 的 github 地址为:https://github.com/jinrunheng/simple-ioc-container

MyIocContainer

import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MyIocContainer {

    private final Map<String, Object> beans = new HashMap<>();
    private final Properties properties = new Properties();

    public static void main(String[] args) throws IOException {
        MyIocContainer iocContainer = new MyIocContainer();
        iocContainer.init();
        OrderService orderService = (OrderService) iocContainer.getBean("orderService");
        orderService.createOrder();
    }

    // 启动容器,从 beans.properties 配置文件中加载 bean 的定义
    public void init() throws IOException {
        properties.load(MyIocContainer.class.getResourceAsStream("/beans.properties"));
    }

    // 从 IoC 容器中获取一个 bean
    // 实质是工厂模式
    public Object getBean(String beanName) {
        String fullBeanName = (String) properties.get(beanName);
        if (beans.get(fullBeanName) != null)
            return beans.get(fullBeanName);

        try {
            Class<?> aClass = Class.forName(properties.getProperty(beanName));
            Object beanInstance = aClass.getConstructor().newInstance();
            // DI
            dependencyInject(aClass, beanInstance);
            beans.put(fullBeanName,beanInstance);
            return beanInstance;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // DI
    private void dependencyInject(Class<?> aClass, Object beanInstance) throws Exception {
        Field[] declaredFields = aClass.getDeclaredFields();

        List<Field> fieldsToBeAutowired = Stream.of(declaredFields)
                .filter(field -> field.getAnnotation(Autowired.class) !null)
                .collect(Collectors.toList());

        for (Field field : fieldsToBeAutowired) {
            String fieldName = field.getGenericType().getTypeName();
            Object dependencyBeanInstance = beans.get(fieldName);
            if (dependencyBeanInstance == null) {
                Class<?> fieldClass = Class.forName(fieldName);
                dependencyBeanInstance = fieldClass.getConstructor().newInstance();
                dependencyInject(fieldClass, dependencyBeanInstance);
            }
            field.setAccessible(true);
            field.set(beanInstance, dependencyBeanInstance);
            beans.put(fieldName, dependencyBeanInstance);
        }
    }
}

beans.properties

orderDao=com.example.ioccontainer.OrderDao
userDao=com.example.ioccontainer.UserDao
userService=com.example.ioccontainer.UserService
orderService=com.example.ioccontainer.OrderService

User

public class User {
    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

UserDao

public class UserDao {
    public User getUserById(Integer id) {
        return new User(id, "user" + id);
    }
}

UserService

import org.springframework.beans.factory.annotation.Autowired;

public class UserService {
    @Autowired private UserDao userDao;

    public User getCurrentLoginUser() {
        return userDao.getUserById(1);
    }
}

OrderDao

public class OrderDao {
    public void createOrder(User currentLoginUser) {
        System.out.println("User " + currentLoginUser.getName() + " creates an order!");
    }
}

OrderService

import org.springframework.beans.factory.annotation.Autowired;

public class OrderService {
    @Autowired private OrderDao orderDao;
    @Autowired private UserService userService;

    public void createOrder() {
        orderDao.createOrder(userService.getCurrentLoginUser());
    }
}

该程序首先读取 beans.properties 配置文件,获取到这些 Bean 的创建信息,所谓的创建信息就是它们的全限定类名,我们要使用反射来创建这些 Bean 的实例。

当我们需要某个 Bean 时,可以调用该容器的 getBean 方法。getBean 方法中,首先我们会在 beans 这个 HashMap 里判断该 Bean 是否已经创建(beans 的 key 存储的是 BeanName,value 是 Bean 的实例,如果查看过 BeanFactory 源码的童鞋就知道,beans 的定义和 BeanFactory 单例池的定义是类似的),如果已经创建过则直接返回,否则程序会通过 properties 的创建信息通过反射的方式创建这个 Bean。那么如何解决这个 Bean 的依赖呢?答案就是通过方法 dependencyInject

dependencyInject 方法会读取 Bean 的所有字段信息,然后再去判断这些字段中有哪些具有 @Autowired 注解。 凡是被标注 @Autowired 注解的 field,我们就认为是该 Bean 依赖的那些 Bean。在 dependencyInject 方法中,我使用了类似于 DFS 深度优先遍历的思想,自上而下地寻找当前的 Bean 所依赖的 Bean,实现了“控制反转”。

所以,简而言之,IoC 容器实现的原理是啥?

总结起来其实就是两点:工厂模式 + 反射

IoC 容器本身就是一个单例工厂,工厂需要生产的 Bean 在配置文件中已经被定义好了,IoC 容器通过反射,根据配置文件里给出的全限定类名实例化为对象,并使用 DI 依赖注入的方式来解决各个 Bean 之间的依赖关系。

总结

Spring 的 IoC 容器负责创建对象,装配对象,并且管理着这些 Bean 的整个生命周期。

大家不妨参考我的代码,自己尝试着写一个 IoC 容器,加深下对 IoC 思想的理解,整个实现步骤如下:

  1. 加载配置文件,放进 Properties 的对象中(你可以把它命名为 beanDefinition,我的代码里直接就叫 properties 了。另外,Properties 本身就是一个 HashTable,你觉得用不惯可以自定义一个 HashMap )
  2. 在调用 getBean 时,我们先从单例池 beans 中取,如果没有就在 properties 里取出 Bean 的全限定类名并使用反射进行实例化。如果有依赖关系,就递归地调用 getBean 方法(或者像我这样,自己定义一个 dependencyInject 方法)完成依赖注入

2、Spring IoC 是如何解决循环依赖的?


我们先来说一下,什么是循环依赖?

当两个或两个以上的 Bean 互相持有对方的引用,最终形成一个闭环就叫做循环依赖或循环引用。譬如上图中,Object A 持有 B 的一个引用,同时地,Object B 也持有 A 的一个引用,这样就产生了一个循环依赖关系。

使用代码来表示就是这样的:

A

public class A {
  B b;
}

B

public class B {
  A a;
}

我们在代码中手动解决循环依赖是非常简单的:

public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;
    }
}

但是在 Spring IoC 容器中解决循环依赖这件事儿就不那么简单了。试想一下,如果我们使用本篇文章第一问里,我自己实现的简易版 IoC 容器会有什么问题?

答案是——死循环,这一点相信大家都不难想到。那 Spring IoC 究竟使用了什么样的魔法来解决循环依赖呢?先说结论,Spring IoC 使用三级缓存来解决循环依赖问题。 什么是三级缓存?为什么要这么做?接下来,我们带着疑问,来正式地看一下在 Spring IoC 中 getBean 的整个流程:

这张图片在手机移动端可能不太方便查看,大家可以在 PC 端浏览,应该还是比较清晰的。

我就继续使用上面 A 和 B 循环引用的例子,带领大家走一遍上图的整个流程。

首先,Spring IoC 容器会调用 AbstractBeanFactory#getBean 方法来获取一个 Bean。该方法在内部会调用 AbstractBeanFactory#doGetBean

doGetBean 内部会调用 DefaultSingletonBeanRegistry#getSingleton 方法:

getSingleton 方法中,我们已经可以看到所谓的三级缓存究竟是什么了。

  • 第一级缓存为:singletonObjects
  • 第二级缓存为:earlySingletonObjects
  • 第三级缓存为:singletonFactories

而它们的本质是这三种东西:

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

getSingleton 的流程是这样的:

首先,我们会从一级缓存 singletonObjects 中获取一个 Bean,如果从缓存中没有获取到,就会继续在二级缓存中获取;二级缓存还是没有的话,就在三级缓存中获取;三级缓存里,我们通过 beanName 会得到一个 ObjectFactory 对象,如果它不为空,就可以调用这个工厂对象的 getObject 方法来获取到缓存中的单例,并将它添加到二级缓存,同时将三级缓存里的内容销毁;如果三级缓存还是为空,getSingleton 方法则返回空。

假设,IoC 容器初始化以后,首先获取 A 这个 Bean,此时三个缓存中都没有 A 的任何信息,所以 getSingleton 方法会返回空。

我们继续顺着 doGetBean 方法的逻辑向下走,就会来到这一步——创建 Bean:

此时我们仍然会执行 getSingleton 方法,只不过这里面的 getSingleton 方法的方法签名为:

Object getSingleton(String beanName, ObjectFactory<?> singletonFactory)

除了 beanName 以外,方法签名中还有一个 ObjectFactory 接口属性。ObjectFactory 接口是一个标准的函数式接口,其中只有一个 getObject 方法,所以,我们可以在 getSingleton 里,直接使用 lambda 表达式来简化它。lambda 表达式中,createBean 这个方法一看就知道其功能为创建一个 Bean,它是一个抽象方法,具体创建 Bean 则是通过方法 AbstractAutowireCapableBeanFactory#doCreateBean 来实现的。

创建一个 Bean 的过程是极其繁琐的,不过归纳起来实际上就是以下几步:

  1. 实例化(Instantiation)
  2. 属性赋值(populate)
  3. 初始化(Initialization)

实例化对应的方法为 AbstractAutowireCapableBeanFactory#createBeanInstance,我们将 A 这个类进行实例化,这里对应的实际上就是 JVM 从加载 A 到初始化 A 的全过程。

在实例化之后,属性赋值之前,会执行一个方法,DefaultSingletonBeanRegistry#addSingletonFactory

getEarlyBeanReference 这个方法我们等会再说,不过你可以大概猜测出这个方法返回的是什么——即:一个 Bean 的提前引用。而 addSingletonFactory 方法的含义是将这个 Bean 的提前引用放到三级缓存中,对应到我们的示例,则是将 A 的提前引用放入到三级缓存。

然后就是属性赋值,对应的方法为 AbstractAutowireCapableBeanFactory#populateBean。所谓的属性赋值就是注入 A 对象的属性,我们的 A 类中有一个 B 的引用,所以我们就要获取到 B 这个 Bean 的引用。聪明如你,肯定就知道了,我们还要从 getBean 开始一直向后执行之前的流程,来获取 B 的实例~

我们来一起复习一下。

首先,Spring IoC 会从一级缓存 singletonObjects 中获取 B,获取不到;然后从二级缓存 earlySingletonObjects 中获取,获取不到;最后从第三级缓存 singletonFactories 中获取,还是获取不到,于是开始执行创建 Bean 的流程。

一开始,我们会对 B 进行实例化,然后将 B 的提前引用放入到三级缓存中。接下来开始注入属性,此时 IoC 容器发现,B 中引用了 A,于是再次触发 doGetBean(A)。这个时候,getSingleton 方法中我们就可以从三级缓存里拿到 A 的提前引用了,接着我们将三级缓存里的提前引用注入到 B 中,完成了 B 的属性赋值,同时将三级缓存里的内容删除,放入到二级缓存,我们称二级缓存内的东西叫 “半成品”。这样,Spring IoC 就解决了循环依赖导致的死循环问题。

当我们执行完对 B 的属性赋值后,就要开始执行 Bean 的初始化操作。

初始化方法为:AbstractAutowireCapableBeanFactory#initializeBean,主要做了以下四件事:

  1. invokeAwareMethods
  2. applyBeanPostProcessorsBeforeInitialization
  3. invokeInitMethods
  4. applyBeanPostProcessorsAfterInitialization

invokeAwareMethods 会处理与 Aware 相关的接口,譬如 BeanNameAwareBeanClassLoaderAware 等等;applyBeanPostProcessorsBeforeInitializationBeanPostProcessor 的前置处理;invokeInitMethods 是调用初始化方法;applyBeanPostProcessorsAfterInitialization 是对 BeanPostProcessor 的后置处理。我们主要关心的就是后置处理这一步,因为后置处理包含 AOP 的动态代理,它也是 Spring IoC 为什么要用三级缓存来解决循环依赖的关键。

我们先继续将这个流程走完。当 IoC 容器将 B 初始化完成后,原本 populateBean(A) 阻塞的线程会继续工作,完成对 A 的属性赋值,再对 A 进行初始化,这个时候二级缓存内的“半成品”也就没啥用了,所以我们将二级缓存内的半成品删除,将 Bean 放入到一级缓存池当中,并将 A 这个 Bean 返回给用户。为了加深大家对整个流程的印象,我将文字描述的过程做成了一个图提供给大家参考:

很多人可能会不解,一级缓存,二级缓存,三级缓存它们的作用是什么呢?似乎二级缓存就够了,为啥非得是三级缓存?

首先,先跟大家解释一下,一级缓存的作用。我们都知道 getBean 方法如果获取不到一个 Bean,便会进入到 createBean 的流程。如果有多个线程操作 createBean 时,我们就要进行加锁,不过当我们设置一个线程安全的单例缓存池时,如果缓存里有这个 Bean,那么将其返回即可,因为线程安全所以也不用考虑加锁的问题。事实上 Spring 也是这么做的,singletonObjects 本身就是一个 ConcurrentHashMap,有了这样一个缓存池可以大大地提升性能。

二级缓存的作用就不用我解释了吧,如果不设置二级缓存,那么循环依赖问题将不可解决。事实上,你可以认为真正的解决循环依赖是靠二级缓存的。

那为什么要设置第三级缓存呢?

答案就是为了处理 AOP 动态代理的 Bean。试想一下,如果需要 AOP 增强的 Bean A 遇到了循环依赖的问题,我们的缓存中存储的应该是 AOP 增强的 Proxy$A!不过刚刚我也特意强调了,AOP 的处理发生在 Bean 的初始化阶段,在初始化阶段的 applyBeanPostProcessorsAfterInitialization 中,会循环这些定义的 BeanPostProcessor,然后调用 postProcessAfterInitialization 方法:

而对于 AOP 来说,就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来完成的。在三级缓存中是可以处理 AOP 的,还记得我们的 getEarlyBeanReference 么?

AbstractAutoProxyCreator (AnnotationAwareAspectJAutoProxyCreator 的继承类) 中是这样重写该方法的:

也就是说,如果这个 BeanPostProcessor 是负责 AOP 的话,就会通过 wrapIfNecessary 方法返回一个代理对象,而其他的 BeanPostProcessor 则不会进行处理。

这也就是三级缓存的意义所在。

如果真的只有两级缓存,如何处理 AOP 增强的 Bean?答案就是不管这个 Bean 有没有循环依赖,只要它被代理,都要提前创建好代理对象,并将代理对象放进缓存中。而 Spring IoC 选择的做法是使用三级缓存,这样的话,就不会提前创建代理对象,当出现循环依赖被其他对象注入时,才在 Bean 实例化之后,属性赋值之前提前生成 AOP 代理。这种设计理念才符合 Spring 的设计原则,即:让 Bean 在生命周期的最后一步完成代理,而不是在实例化之后就立马完成代理。

总结

关于 Spring IoC 如何解决循环依赖这个问题,强烈建议大家自己去 debug 一下源代码,一定是受益匪浅的。

Tips:

因为 ApplicationContext 在容器启动后就一次性创建好所有的 Bean,所以,我们可以在一个 Bean 上标注 @Lazy 注解,这样即可以懒加载的方式创建这个 Bean。

3、BeanFactory 与 ApplicationContext 有什么区别?


BeanFactory 是比较原始的 Spring IoC 容器的核心接口,具有读取 Bean 的配置文档;管理 Bean 的加载,实例化;控制 Bean 的生命周期;维护 Bean 之间的依赖关系等功能。可以说 BeanFactory 是 Spring 最底层的接口,是 Spring 框架的基础设施。

而 ApplicationContext 接口则是 BeanFactory 的派生,它不仅继承了 BeanFactory 的全部功能,还提供了更加完整的框架功能。从名字上来看,BeanFactory 非常直观,它就是一个 Bean 的工厂,而 ApplicationContext 则代表了应用上下文,具备了更多的功能譬如它继承了 MessageSource,支持国际化;支持 AOP,Web 等 Spring 插件等等。

BeanFactory 与 ApplicationContext 还有一个比较重要的区别,那就是它们加载 Bean 的方式有所不同。

BeanFactory 采用了懒加载方式,只有在调用 getBean 方法时才会对一个 Bean 进行加载实例化并注入依赖;而 ApplicationContext 则是在容器启动时就一次性创建好所有的 Bean。

ApplicationContext 这样设计的好处是我们可以提前发现 Spring 中存在的配置错误,因为它一次性创建并加载了所有的 Bean,我们在容器启动后便可以确保这些 Bean 的配置信息无误,而不会出现延迟加载带来的第一次使用时才会抛异常的问题。

总结

关于 Spring 框架的部分会有很多,只能慢慢更新了...

感谢您的阅读与支持~

好啦,至此为止,这篇文章就到这里了~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!

jrh

2021/10/25  阅读:49  主题:橙心

作者介绍

jrh