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

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的循環依賴。

下面的例子中, 會發生這種循環依賴的情況:

@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爲了解決單例的循環依賴問題,使用了三級緩存。

三級緩存主要指:

/** 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。主要調用方法就就是:

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()(三級緩存)獲取,如果獲取到了則:

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

從singletonFactories中移除,並放入earlySingletonObjects中。其實也就是從三級緩存移動到了二級緩存。

從上面三級緩存的分析,我們可以知道,Spring解決循環依賴的訣竅就在於singletonFactories這個三級cache。這個cache的類型是ObjectFactory,定義如下:

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

這個接口在下面被引用

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將會在第一次被使用的時候初始化。

@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中進行注入,例子如下:

@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。

具體例子如下:

@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方法或配置方法上,用於實現依賴自動注入。

@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類中包含了哪些需要注入的元素及元素要注入到哪個目標類中。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章