0%

Spring MVC系列-(7) IOC初始化流程

Spring.png

7. IOC初始化流程

IoC容器的初始化就是含有BeanDefinition信息的Resource的定位、载入、解析、注册四个过程,最终我们配置的bean,以beanDefinition的数据结构存在于IoC容器即内存中。这里并不涉及bean的依赖注入,只是bean定义的载入。但有例外,在使用Ioc容器时有一个预实例化的配置,即bean定义中的设置了lazyinit属性,那么这个bean在Ioc容器初始化时就预先加载,不需要等到Ioc整个初始化后,第一次getBean时才会触发。其中refresh()启动对Ioc容器的初始化。

主要分为如下三个步骤:

  1. Resource定位过程。

这个Resource定位指的是BeanDefinition的资源定位,它由ResourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用提供了统一接口。对于这些BeanDefinition的存在形式,相信大家都不会感到陌生。比如说,在文件系统中的Bean定义信息可以使用FileSystemResource来进行抽象;在类路径中可以使用前面提到的ClassPathResource来使用,等等。这个过程类似于容器寻找数据的过程,就像用水桶装水先要把水找到一样。

  1. BeanDefinition的载入

该载入过程把用户定义好的Bean表示成IoC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition,下面可以看到这个数据结构的详细定义。总地说来,这个BeanDefinition实际上就是POJO对象在IoC容器中的抽象,这个BeanDefinition定义了一系列的数据来使得IoC容器能够方便地对POJO对象也就是Spring的Bean进行管理。即BeanDefinition就是Spring的领域对象。

  1. 向IoC容器注册这些BeanDefinition的过程

这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的,这个注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。可以看到,在IoC容器内部将BeanDefinition注入到一个HashMap中去,Ioc容器是通过这个HashMap来持有这些BeanDefinition数据的。整个过程可以理解为容器的初始化过程。

容器的初始化是通过AbstractApplicationContext的refresh()实现的,下面将会对这个函数进行详细介绍。

7.1 refresh函数

Spring中会经常使用到AnnotationConfigApplicationContext作为IOC容器的操作入口,可以利用该context进行Bean的管理,从下面的构造函数可以看到,refresh函数中完成了IOC容器的初始化,因此弄清楚refresh函数就理解了IOC的初始化流程。

Screen Shot 2020-02-18 at 4.27.54 PM.png

11111111.png

下面对每个函数仔细分析。

1. prepareRefresh() 预处理

Screen Shot 2020-02-18 at 5.10.12 PM.png

  1. initPropertySources()初始化一些属性设置;子类自定义个性化的属性设置方法
  2. getEnvironment().validateRequiredProperties();校验属性的合法等
  3. earlyApplicationEvents = new LinkedHashSet() 保存容器中一些早期的事件

2. obtainFreshBeanFactory() 获取BeanFactory

这一步重点是refreshBeanFactory(),

Screen Shot 2020-02-18 at 5.15.34 PM.png

Screen Shot 2020-02-18 at 5.16.50 PM.png

  1. refreshBeanFactory()
    刷新【创建】容器,创建了一个this.beanFactory = new DefaultListableBeanFactory();并设置序列化id。
  2. getBeanFactory();
    返回上一步创建的beanFactory对象
  3. 将创建BeanFactory【DefaultListableBeanFactory】返回

3. prepareBeanFactory(beanFactory) BeanFactory的预准备工作

Screen Shot 2020-02-18 at 5.20.33 PM.png

  1. 设置beanFactory的类加载器、支持表达式解析器
  2. 添加部分ApplicationContextAwareProcessor
  3. 设置忽略的自动装配的接口EnvironmentAware、EmbeddedValueResolverAware等等
  4. 注册可以解析的自动装配;我们能直接在任何组件中自动注入
  5. BeanFactory、ResourceLoader、ApplicationEventPublisher、ApplicationContext
  6. 添加BeanPostProcessor【ApplicationListenerDetector】
  7. 添加编译时的AspectJ
  8. 给BeanFactory中注册一些能用的组件 environment【ConfigurableEnvironment】、systemProperties【Map<String, Object>】、systemEnvironment【Map<String, Object>】

4. postProcessBeanFactory(beanFactory) beanFactory准备工作完成后进行的后置处理

子类通过重写这个方法来在beanFactory创建并预准备完成以后做进一步的设置,以上4步是beanFactory的创建以及预准备工作。

5. invokeBeanFactoryPostProcessors(beanFactory) 调用beanFactory后置处理器

这部分主要执行实现了如下两个接口的类方法:BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor

1. 先执行BeanDefinitionRegistryPostProcessor的方法

1)获取所有的BeanDefinitionRegistryPostProcessor

2)先执行实现了PriorityOrdered接口的获取所有的BeanDefinitionRegistryPostProcessor

1
postProcessor.postProcessBeanDefinitionRegistry(registry)

3)在执行实现了Ordered顺序接口的BeanDefinitionRegistryPostProcessor

4)最后执行没有任何优先级或者是顺序接口的BeanDefinitionRegistryPostProcessor

Screen Shot 2020-02-18 at 6.57.34 PM.png

2. 在执行BeanFactoryPostProcessor的方法

1)获取所有的BeanFactoryPostProcessor

2)看先执行实现了PriorityOrdered接口的获取所有的BeanFactoryPostProcessor

3)在执行实现了Ordered顺序接口的BeanFactoryPostProcessor

4)最后执行没有任何优先级或者是顺序接口的BeanFactoryPostProcessor

Screen Shot 2020-02-18 at 7.58.17 PM.png

6. registerBeanPostProcessors(beanFactory);注册BeanPostProcessor(bean的后置处理器)

将不同类型的BeanPostProcessor加入到BeanFactory中,注意到这里是依据优先级依次注册。

1)获取所有的BeanPostProcessor;后置处理器都默认可以通过PriorityOrdered、Ordered接口执行优先级。

2)先注册PriorityOrdered优先级的BeanPostProcessor;把每一个BeanPostProcessor添加到BeanFactory中。

3)接着注册Ordered接口的。

4)然后注册没有任何优先级接口的。

5)注册一个ApplicationListenerDetector;来在bean创建完成后检查是否是ApplicationListener如果是监听器。

这个步骤中的所有后置处理器,都是通过下面的getBean方法来进行实例化的,具体流程在之前AOP中有介绍。实例化之后,在后续注册Bean的时候,就可以对Bean的生成进行定制化。

Screen Shot 2020-02-18 at 9.43.12 PM.png

7. initMessageSource() 初始化messageSource组件

1)获取BeanFactory

2)看容器中是否有id为messageSource的,类型是MessageSource的组件
如果有复制给messageSource,如果没有创建一个DelegatingMessageSource

3)把创建好的MessageSource注册到容器中,以后获取国际化配置文件的值的时候可以自动注入MessageSource
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);

8. initApplicationEventMulticaster()

1)获取BeanFactory

2)从BeanFactory中获取ApplicationEventMulticaster

3)如果没有上一步配置,那就创建一个SimpleApplicationEventMulticaster

4)将创建的ApplicationEventMulticaster添加到BeanFactory中,以后其他组件直接自动注入
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);

9. onRefresh()

空函数,子类可以重写这个方法,在容器刷新的时候可以自定义逻辑(比如增加组件)

10. registerListeners()

1)从容器中拿到所有的ApplicationListener。

2)将每个监听器添加到时间派发器中
 getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);

3)派发之前步骤产生的事件

11. finishBeanFactoryInitialization(beanFactory);初始化所有剩下的单实例bean

这是所有步骤中,最重要,最复杂的一步,之前AOP中对这部分有过仔细介绍,这里重点梳理正常Bean的初始化流程。

未命名文件 (1).png

12. finishRefresh() 完成BeanFactory的初始化创建工作,IOC容器就创建完成

1.initLifecycleProcessor()初始化和生命周期有关的后置处理器;

LifecycleProcessor默认从容器中找是否有LifecycleProcessor的组件

2.getLifecycleProcessor().onRefresh();

拿到前面定义的生命周期处理器(BeanFactory) 回调onRefresh

3.publishEvent(new ContextRefreshedEvent(this));发布容器刷新完成事件

4.LiveBeansView.registerApplicationContext(this);

7.2 Spring-bean的循环依赖以及解决方式

什么是循环依赖?

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

Screen Shot 2020-02-19 at 3.43.36 PM.png@w=250

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。

Spring中循环依赖场景有:

(1)构造器的循环依赖
(2)field属性或者setter的循环依赖。

下面的例子中, 会发生这种循环依赖的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class CircularDependencyA {

private CircularDependencyB circB;

@Autowired
public CircularDependencyA(CircularDependencyB circB) {
this.circB = circB;
}
}

@Component
public class CircularDependencyB {

private CircularDependencyA circA;

@Autowired
public CircularDependencyB(CircularDependencyA circA) {
this.circA = circA;
}
}

怎么检测是否存在循环依赖?

检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean加标记,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

Spring怎么解决field属性和setter的循环依赖?

Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,对象的field或则属性是可以延后设置的(但是构造器必须是在获取引用之前)。

Spring的单例对象的初始化主要分为三步:

(1)createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象

(2)populateBean:填充属性,这一步主要是多bean的依赖属性进行填充

(3)initializeBean:调用spring xml中的init 方法。

从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一、第二部。也就是构造器循环依赖和field循环依赖。

那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。

三级缓存主要指:

1
2
3
4
5
6
7
8
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

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

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

这三级缓存分别指:
singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前曝光的单例对象的Cache
singletonObjects:单例对象的cache

我们在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。主要调用方法就就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

上面的代码需要解释两个参数:

  • isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象, 或则在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)
  • allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象

分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:

1
2
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory,定义如下:

1
2
3
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}

这个接口在下面被引用

1
2
3
4
5
6
7
8
9
10
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

这里就是解决循环依赖的关键,这段代码发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。

下面是整段代码:

Screen Shot 2020-02-19 at 5.23.30 PM.png

这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。

总结来讲,Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

如何解决构造器中的循环依赖?

1. 使用@Lazy注解

最简单的方法是使用@Lazy声明其中的一个Bean,这样的话Spring将会创建代理对象,并注入到其他依赖于它的Bean中,这个注入的Bean将会在第一次被使用的时候初始化。

1
2
3
4
5
6
7
8
9
10
@Component
public class CircularDependencyA {

private CircularDependencyB circB;

@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}

2. 替换构造器依赖,改为setter/Field注入

上面提到了Spring可以解决setter/Field中的循环依赖,因此可以将构造器中的依赖Bean,改为在setter/Field中进行注入,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class CircularDependencyA {

private CircularDependencyB circB;

@Autowired
public void setCircB(CircularDependencyB circB) {
this.circB = circB;
}

public CircularDependencyB getCircB() {
return circB;
}
}

@Component
public class CircularDependencyB {

private CircularDependencyA circA;

private String message = "Hi!";

@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}

public String getMessage() {
return message;
}
}

3. 实现ApplicationContextAware和InitializingBean

让其中的一个类实现ApplicationContextAware和InitializingBean,来手动设置依赖的Bean。

ApplicationContextAware发生在调用初始化方法之前,也就是下面的第二步,因此可以获取到ApplicationContext。

Screen Shot 2020-02-19 at 5.27.51 PM.png

实现InitializingBean需要重写其的afterPropertiesSet方法,这发生在第3步,此时类中已经有了ApplicationContext,所以直接拿到对应的Bean实例即可。

因为这两个方法都发生在createBeanInstance之后,所以在缓存singletonFactories中拿到对应的Bean。

具体例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {

private CircularDependencyB circB;

private ApplicationContext context;

public CircularDependencyB getCircB() {
return circB;
}

@Override
public void afterPropertiesSet() throws Exception {
circB = context.getBean(CircularDependencyB.class);
}

@Override
public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
context = ctx;
}
}

@Component
public class CircularDependencyB {

private CircularDependencyA circA;

private String message = "Hi!";

@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}

public String getMessage() {
return message;
}
}

参考:

7.3 spring依赖注入注解的实现原理

7.3.1 @Autowired的工作原理

以下是@Autowired注解的源码,从源码中看到它可以被标注在构造函数、属性、setter方法或配置方法上,用于实现依赖自动注入。

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

/**
* Declares whether the annotated dependency is required.
* <p>Defaults to {@code true}.
*/
boolean required() default true;
}

@Autowired注解的作用是由AutowiredAnnotationBeanPostProcessor实现的,查看该类的源码会发现它实现了MergedBeanDefinitionPostProcessor接口,进而实现了接口中的postProcessMergedBeanDefinition方法,@Autowired注解正是通过这个方法实现注入类型的预解析,将需要依赖注入的属性信息封装到InjectionMetadata类中,InjectionMetadata类中包含了哪些需要注入的元素及元素要注入到哪个目标类中。

1
2
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware

@Autowired发生在refresh方法的finishBeanFactoryInitialization(beanFactory)阶段,在此之前,在registerBeanPostProcessors(beanFactory)已经完成了对AutowiredAnnotationBeanPostProcessor的注册。

在doCreateBean方法,首先会调用applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName),实质上就是调用AutowiredAnnotationBeanPostProcessor类的postProcessMergedBeanDefinition方法,也就是开头介绍的在这个方法中完成了对注入元素注解的预解析。接着,在doCreateBean方法中执行populateBean方法实现对属性的注入。

Screen Shot 2020-02-19 at 9.49.12 PM.png

深入分析populateBean方法,下面是关键部分,这段代码中会遍历所有注册过的BeanPostProcessor接口实现类的实例,如果实例属于InstantiationAwareBeanPostProcessor类型的,则执行实例类的postProcessPropertyValues方法。

Screen Shot 2020-02-19 at 9.52.29 PM.png

从下面的类继承关系可以看到,这里会执行AutowiredAnnotationBeanPostProcessor类的postProcessPropertyValues方法,

spring-annotation-value.png

具体代码如下:

Screen Shot 2020-02-19 at 9.54.30 PM.png

metadata.inject(bean, beanName, pvs)代码的执行会进入如下inject方法中,在这里完成依赖的注入。

Screen Shot 2020-02-19 at 9.55.23 PM.png

上面的InjectedElement有两个子类,分别是AutowiredFieldElement和AutowiredMethodElement,AutowiredFieldElement用于对标注在属性上的注入,AutowiredMethodElement用于对标注在方法上的注入。

两种方式的注入过程都差不多,根据需要注入的元素的描述信息,按类型或名称查找需要的依赖值,如果依赖没有实例化先实例化依赖,然后使用反射进行赋值。

7.3.2 @Resource和@Inject的工作原理

两者的实现原理与@Autowired类似,不过这两者是JDK中提供的annotation,是通过BeanPostProcessor接口的实现类CommonAnnotationBeanPostProcessor来实现的,其中如名字所述,即公共注解CommonAnotation,CommonAnnotationBeanPostProcessor是spring中统一处理JDK中定义的注解的一个BeanPostProcessor。该类会处理的注解还包括@PostConstruct,@PreDestroy等。

7.3.3 注解处理器的激活条件

AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor添加到spring容器的BeanPostProcessor的条件,即激活这些处理器的条件如下:

  1. 基于xml的spring配置
  • 在对应的spring容器的配置xml文件中,如applicationContext.xml,添加<context:annotation-config />和<context:component-scan />,或者只使用<context:component-scan />。

  • 两者的区别是<context:annotation-config />只查找并激活已经存在的bean,如通过xml文件的bean标签生成加载到spring容器的,而不会去扫描如@Controller等注解的bean,查找到之后进行注入;而<context:component-scan />除了具有<context:annotation-config />的功能之外,还会去加载通过basePackages属性指定的包下面的,默认为扫描@Controller,@Service,@Component,@Repository注解的类。不指定basePackages则是类路径下面,或者如果使用注解@ComponentScan方式,则是当前类所在包及其子包下面。

  1. 基于配置类的spring配置
  • 如果是基于配置类而不是基于applicationContext.xml来对spring进行配置,如SpringBoot,则在内部使用的IOC容器实现为AnnotationConfigApplicationContext或者其派生类,在AnnotationConfigApplicationContext内部会自动创建和激活以上的BeanPostProcessor。

  • 如果同时存在基于xml的配置和配置类的配置,而在注入时间方面,基于注解的注入先于基于XML的注入,所以基于XML的注入会覆盖基于注解的注入。

7.3.4 总结

  • @Autowired是Spring自带的,@Inject和@Resource都是JDK提供的,其中@Inject是JSR330规范实现的,@Resource是JSR250规范实现的,而Spring通过BeanPostProcessor来提供对JDK规范的支持。
  • @Autowired、@Inject用法基本一样,不同之处为@Autowired有一个required属性,表示该注入是否是必须的,即如果为必须的,则如果找不到对应的bean,就无法注入,无法创建当前bean。
  • @Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的。如在spring-boot-data项目中自动生成的redisTemplate的bean,是需要通过byName来注入的。如果需要注入该默认的,则需要使用@Resource来注入,而不是@Autowired。
  • 对于@Autowire和@Inject,如果同一类型存在多个bean实例,则需要指定注入的beanName。@Autowired和@Qualifier一起使用,@Inject和@Name一起使用。

参考:


本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处

搜索『后端精进之路』关注公众号,立刻获取最新文章和价值2000元的BATJ精品面试课程

后端精进之路.png