Spring解决循环依赖原理分析-Spring如何解决循环依赖的源码分析-《Java笔记》

admin 2025-10-19 04:29:24 编程 来源:ZONE.CI 全球网 0 阅读模式

Java Spring

一、什么是循环依赖

就是有两个服务,A服务,B服务,然后在A里注入了B,然后在B里注入了A,这就是循环依赖了,这种情况如果不解决的话,那就会出现一个相互依赖注入的死循环。

二、循环依赖的解决方案 - 三级缓存

2.1 什么是三级缓存

  1. /** 一级缓存 单例缓存池 用于保存我们所有的单实例bean */
  2. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  3. /** 二级缓存 保存半成品bean实例,当对象需要被AOP切面代时,保存代理bean的实例beanProxy*/
  4. private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
  5. /** 三级缓存 存放ObjectFactory,传入的是匿名内部类,ObjectFactory.getObject() 方法最终会
  6. 调用getEarlyBeanReference()进行处理,返回创建bean实例化的lambda表达式。*/
  7. private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

2.2 三级缓存如何解决循环依赖的问题

前置知识:Spring的单例对象的初始化主要分为三步:(1)createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象(2)populateBean:填充属性,这一步主要是多bean的依赖属性进行填充(3)initializeBean:调用spring xml中的init 方法。

2.2.1 重点方法

循环依赖涉及的重点方法是DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

  1. //一个纯bean获取流程,这里不进行创建
  2. protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  3. // 从一级缓存singletonObjects获取bean
  4. Object singletonObject = this.singletonObjects.get(beanName);
  5. // 一级缓存没有,判断该bean是否在创建中,通过Set的contains来判断
  6. if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  7. synchronized (this.singletonObjects) {
  8. // 从二级缓存中获取bean
  9. singletonObject = this.earlySingletonObjects.get(beanName);
  10. // 二级缓存没有&&允许提前引用
  11. if (singletonObject == null && allowEarlyReference) {
  12. // 从三级缓存中获取lambda表达式
  13. ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
  14. // 如果获取到该lambda表达式,进行回调填充
  15. if (singletonFactory != null) {
  16. // 调用三级缓存的lambda表示获取早期不完整对象
  17. singletonObject = singletonFactory.getObject();
  18. // 写入二级缓存
  19. this.earlySingletonObjects.put(beanName, singletonObject);
  20. // 三级缓存移除该bean的lambda表达式
  21. this.singletonFactories.remove(beanName);
  22. }
  23. }
  24. }
  25. }
  26. return singletonObject;
  27. }
  28. public boolean isSingletonCurrentlyInCreation(String beanName) {
  29. return this.singletonsCurrentlyInCreation.contains(beanName);
  30. }

2.2.2 提前透知下AB存在循环依赖的情况大概是怎样一个加载流程,源码解读放下面

现在来分析一下A B循环依赖的情况

  • A doCreateBean()实例化,由于还未创建,从一级缓存查不到,且不是正在创建,这时候调用bean创建流程,将不完整对象以及BeanDefinition代表的lambda表达式写入三级缓存singletonFactories;
  • A populateBean()进行属性注入时候发现自己需要B对象,但是三级缓存中未发现B,就去创建B同样把创建B bean的lambda表达式放入singletonFactories;
  • B populateBean() 发现自己需要A对象,从一级缓存singletonObjects和二级缓存earlySingletonObjects中未发现A,但是在三级缓存singletonFactories中发现A,执行singletonFactories 里A的回调函数getEarlyBeanReference(),创建不完整的A bean,将其放入二级缓存earlySingletonObjects,同时从三级缓存删除;
  • 将A注入到对象B中,B完成属性填充,执行初始化方法,将自己放入第一级缓存中(此时B是一个完整的对象);
  • 返回,A得到对象B,将B注入到A中,A完成属性填充,初始化,并放入到一级缓存中。
  • 在创建过程中,都是从三级缓存(对象工厂里创建不完整对象),将提前暴露的对象放入到二级缓存,从二级缓存拿到后,完成初始化,放入一级缓存。

    三、原码分析

    3.1.%20BeanServiceA的创建:

    在创建bean时,会调用doGetBean方法,首先通过getSingleton方法从缓存中看是否能获取到该bean

  • a.%20先从一级缓存singletonObjects中获取,发现获取不到,然后看是否在创建中,显然初次创建时不成立,即getSingleton返回null

  • b.%20调用第14行的getSingleton方法触发createBean回调,进行bean的生命周期 ```java %20%20protected%20%20T%20doGetBean(%20%20%20%20%20%20String%20name,%20@Nullable%20Class<T>%20requiredType,%20@Nullable%20Object[]%20args,%20boolean%20typeCheckOnly)
  • %20%20%20%20%20%20throws%20BeansException%20{%20%20...%20...%20...%20%20//先从一级缓存singletonObjects中获取,发现获取不到,然后看是否在创建中,显然初次创建时不成立,即getSingleton返回null%20%20Object%20sharedInstance%20=%20getSingleton(beanName);%20%20if%20(sharedInstance%20!=%20null%20&&%20args%20==%20null)%20{%20%20%20%20%20%20...%20...%20...%20%20%20}else%20{%20%20%20%20%20%20...%20...%20...%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20...%20...%20...%20%20%20%20%20%20%20%20%20%20if%20(mbd.isSingleton())%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20//getSingleton方法触发createBean回调,进行bean的生命周期%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//这里会将当前beanName放入singletonsCurrentlyInCreation,表示当前bean正在创建%20%20%20%20%20%20%20%20%20%20%20%20%20%20sharedInstance%20=%20getSingleton(beanName,%20()%20->%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20createBean(beanName,%20mbd,%20args);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20...%20...%20...%20%20}%20%20return%20(T)%20bean;%20%20}

public%20Object%20getSingleton(String%20beanName,%20ObjectFactory<?>%20singletonFactory)%20{ %20%20%20%20%20%20%20%20Assert.notNull(beanName,%20“Bean%20name%20must%20not%20be%20null”); %20%20%20%20%20%20%20%20synchronized%20(this.singletonObjects)%20{ %20%20%20%20%20%20%20%20%20%20%20%20Object%20singletonObject%20=%20this.singletonObjects.get(beanName); %20%20%20%20%20%20%20%20%20%20%20%20if%20(singletonObject%20==%20null)%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20…%20…%20… %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20会将当前beanName放入singletonsCurrentlyInCreation,表示当前bean正在创建 %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20beforeSingletonCreation(beanName); %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20…%20…%20… %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20try%20{ %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20lambda表达式回到createBean %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20singletonObject%20=%20singletonFactory.getObject(); %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20newSingleton%20=%20true; %20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20%20%20%20%20…%20…%20…%20 %20%20%20%20%20%20%20%20%20%20%20%20} %20%20%20%20%20%20%20%20%20%20%20%20return%20singletonObject; %20%20%20%20%20%20%20%20} %20%20%20%20}

<a%20name="VIaYG"></a>####%203.1.1.%20实例化BeanServiceA不完整对象:-%20一般通过`createBeanInstancec`实例化不完整的`BeanServiceA`对象-%20将不完整对象以及`BeanDefinition`代表的lambda表达式写入三级缓存-%20属性填充BeanServiceB-%20初始化BeanServiceA时调用AOP后置处理器进行AOP处理-%20处理提前暴露的场景,保证返回同一个代理对象```javaprotected%20Object%20doCreateBean(String%20beanName,%20RootBeanDefinition%20mbd,%20@Nullable%20Object[]%20args)%20%20%20%20%20%20%20%20throws%20BeanCreationException%20{%20%20%20%20...%20...%20...%20%20%20%20if%20(instanceWrapper%20==%20null)%20{%20%20%20%20%20%20%20%20//%20a.%20一般通过createBeanInstancec实例化不完整的BeanServiceA对象%20%20%20%20%20%20%20%20instanceWrapper%20=%20createBeanInstance(beanName,%20mbd,%20args);%20%20%20%20}%20%20%20%20...%20...%20...%20%20%20%20//%20默认单例&&默认循环引用&&该bean正在创建,条件成立%20%20%20%20boolean%20earlySingletonExposure%20=%20(mbd.isSingleton()%20&&%20this.allowCircularReferences%20&&%20%20%20%20%20%20%20%20%20%20%20%20isSingletonCurrentlyInCreation(beanName));%20%20%20%20if%20(earlySingletonExposure)%20{%20%20%20%20%20%20%20%20...%20...%20...%20%20%20%20%20%20%20%20//%20b.%20将不完整对象以及BeanDefinition代表的lambda表达式写入三级缓存%20%20%20%20%20%20%20%20addSingletonFactory(beanName,%20()%20->%20getEarlyBeanReference(beanName,%20mbd,%20bean));%20%20%20%20}%20%20%20%20//%20Initialize%20the%20bean%20instance.%20%20%20%20Object%20exposedObject%20=%20bean;%20%20%20%20try%20{%20%20%20%20%20%20%20%20//%20c.%20属性填充BeanServiceB%20%20%20%20%20%20%20%20populateBean(beanName,%20mbd,%20instanceWrapper);%20%20%20%20%20%20%20%20//%20d.初始化时调用AOP后置处理器进行AOP处理%20%20%20%20%20%20%20%20exposedObject%20=%20initializeBean(beanName,%20exposedObject,%20mbd);%20%20%20%20}%20%20%20%20...%20...%20...%20%20%20%20//%20e.处理提前暴露的场景,保证返回同一个代理对象%20%20%20%20if%20(earlySingletonExposure)%20{%20%20%20%20%20%20%20%20//%20见上文第1节的节速,由于第二个参数是false,所以只会查到第二季缓存%20%20%20%20%20%20%20%20//%20所以这里就是查看第二级缓存能不能取到值,取到就意味着涉及提前AOP%20%20%20%20%20%20%20%20Object%20earlySingletonReference%20=%20getSingleton(beanName,%20false);%20%20%20%20%20%20%20%20//%20涉及提前AOP,从二级缓存中获取提前AOP的代理对象%20%20%20%20%20%20%20%20if%20(earlySingletonReference%20!=%20null)%20{%20%20%20%20%20%20%20%20%20%20%20%20if%20(exposedObject%20==%20bean)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20保证循环依赖且涉及AOP时,返回同一个代理对象,下文有结束%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20exposedObject%20=%20earlySingletonReference;%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20%20%20...%20...%20...%20%20%20%20%20%20%20%20}%20%20%20%20}%20%20%20%20return%20exposedObject;}

上面%20addSingletonFactory将不完整对象以及BeanDefinition代表的lambda表达式写入三级缓存

protected%20void%20addSingletonFactory(String%20beanName,%20ObjectFactory<?>%20singletonFactory)%20{%20%20%20%20Assert.notNull(singletonFactory,%20"Singleton%20factory%20must%20not%20be%20null");%20%20%20%20synchronized%20(this.singletonObjects)%20{%20%20%20%20%20%20%20%20if%20(!this.singletonObjects.containsKey(beanName))%20{%20%20%20%20%20%20%20%20%20%20%20%20//%20将不完整对象以及BeanDefinition代表的lambda表达式写入三级缓存%20%20%20%20%20%20%20%20%20%20%20%20this.singletonFactories.put(beanName,%20singletonFactory);%20%20%20%20%20%20%20%20%20%20%20%20this.earlySingletonObjects.remove(beanName);%20%20%20%20%20%20%20%20%20%20%20%20this.registeredSingletons.add(beanName);%20%20%20%20%20%20%20%20}%20%20%20%20}}

3.1.2.%20属性填充BeanServiceB:

关于属性填充方法的详细介绍可参考链接:属性填充populateBean当填充BeanServiceB会重复上文第1节中的内容:

  • a.%20先从一级缓存singletonObjects中获取,发现获取不到,然后看是否在创建中,显然初次创建时不成立,即getSingleton返回null
  • b.%20调用第14行的getSingleton方法触发createBean回调,进行bean的生命周期
  • c.%20实例化BeanServiceB的不完整对象,并将lambda写入三级缓存
  • d.%20属性填充BeanServiceA(见下文1.1.2.1)
  • e.%20initializeBean初始化BeanServiceB

    3.1.2.1.%20上面的d,循环依赖处理属性填充BeanServiceA:

    同样,会重复上文第1节的内容,但此时会有不一样的处理:

  • a.%20先从一级缓存singletonObjects中获取,发现获取不到,然后看是否在创建中,显然正在创建,singletonsCurrentlyInCreation有beanServiceA

  • b.%20从二级缓存中获取%20→%20获取不到%20→%20从三级缓存获取%20→%20lambda表达式回调(见下文1.1.2.1.1)
  • c.%20将不完整对象BeanServiceA写入二级缓存,三级缓存删除该对象lambda表达式(上文1中getSingleton方法)
  • d.%20返回不完整的BeanServiceA对象

    3.1.1.2.1.1.%20AbstractAutoProxyCreator#getEarlyBeanReference:

    这里涉及Aop

    protected%20Object%20getEarlyBeanReference(String%20beanName,%20RootBeanDefinition%20mbd,%20Object%20bean)%20{
  • %20%20Object%20exposedObject%20=%20bean;%20%20if%20(!mbd.isSynthetic()%20&&%20hasInstantiationAwareBeanPostProcessors())%20{%20%20%20%20%20%20//遍历后置处理器%20%20%20%20%20%20for%20(BeanPostProcessor%20bp%20:%20getBeanPostProcessors())%20{%20%20%20%20%20%20%20%20%20%20if%20(bp%20instanceof%20SmartInstantiationAwareBeanPostProcessor)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20SmartInstantiationAwareBeanPostProcessor%20ibp%20=%20(SmartInstantiationAwareBeanPostProcessor)%20bp;%20%20%20%20%20%20%20%20%20%20%20%20%20%20//%20调用后置处理器的getEarlyBeanReference进行提前暴露bean%20%20%20%20%20%20%20%20%20%20%20%20%20%20exposedObject%20=%20ibp.getEarlyBeanReference(exposedObject,%20beanName);%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20}%20%20}%20%20return%20exposedObject;}

    lambda回调会调用后置处理器的getEarlyBeanReference,来获取不完整的bean

  • 如果当前依赖的bean不涉及Aop,则返回实例化时创建的不完整bean对象

  • 如果当前依赖的bean涉及Aop,则返回一个代理该不完整bean的代理对象
  • 本处返回实例化创建的不完整beanServiceA对象

    public%20Object%20getEarlyBeanReference(Object%20bean,%20String%20beanName)%20{
  • %20%20Object%20cacheKey%20=%20getCacheKey(bean.getClass(),%20beanName);%20%20//%20写入earlyProxyReferences,在后面postProcessAfterInitialization会用到%20%20this.earlyProxyReferences.put(cacheKey,%20bean);%20%20//%20判断是否创建代理对象%20%20return%20wrapIfNecessary(bean,%20beanName,%20cacheKey);}

    3.1.2.2%20初始化BeanServiceB时调用AOP后置处理器进行AOP处理

    属性填充beanServiceA后,此时填充的beanServiceA是不完整的对象,在initializeBean初始化调用后置处理器的postProcessAfterInitialization方法由于BeanServiceB不涉及AOP,所以返回原始的B对象,此时填充的beanServiceA还是不完整的对象

  • B不涉及提前AOP,所以earlyProxyReferences没有beanServiceB(见上文3.1.2.1.1)

  • remove返回null,条件成立,执行wrapIfNecessary方法
  • 由于beanServiceB不涉及AOP,所以返回原始的B对象

    3.1.2.3. 处理提前暴露的场景,保证返回同一个代理对象

    BeanServiceB不涉及AOP,getSingleton返回null,所以直接返回原始对象exposedObjectSpring如何解决循环依赖的源码分析 - 图3

    3.1.2.4. BeanServiceB对象写入一级缓存,移除二、三级缓存

    beanServiceB生命周期执行完,返回到3.1节第14行的getSingleton方法,此时返回的还是不完整的beanServiceB对象singletonsCurrentlyInCreation移除bean,表明不再是正在创建的bean

    1. public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    2. Assert.notNull(beanName, "Bean name must not be null");
    3. synchronized (this.singletonObjects) {
    4. Object singletonObject = this.singletonObjects.get(beanName);
    5. if (singletonObject == null) {
    6. ... ... ...
    7. // 会将当前beanName放入singletonsCurrentlyInCreation,表示当前bean正在创建
    8. beforeSingletonCreation(beanName);
    9. ... ... ...
    10. try {
    11. // lambda表达式回到createBean
    12. singletonObject = singletonFactory.getObject();
    13. newSingleton = true;
    14. ... ... ..
    15. }
    16. finally {
    17. if (recordSuppressedExceptions) {
    18. this.suppressedExceptions = null;
    19. }
    20. // singletonsCurrentlyInCreation中移除beanName,表示该bean不是正在创建了
    21. afterSingletonCreation(beanName);
    22. }
    23. if (newSingleton) {
    24. // 将不完整的beanServiceB对象写入一级缓存,移除二、三级缓存
    25. addSingleton(beanName, singletonObject);
    26. }
    27. }
    28. return singletonObject;
    29. }
    30. }

    beanServiceB“不完整对象”写入一级缓存,移除二、三级缓存

    1. protected void addSingleton(String beanName, Object singletonObject) {
    2. synchronized (this.singletonObjects) {
    3. // 写入一级缓存
    4. this.singletonObjects.put(beanName, singletonObject);
    5. // 移除二、三级缓存
    6. this.singletonFactories.remove(beanName);
    7. this.earlySingletonObjects.remove(beanName);
    8. this.registeredSingletons.add(beanName);
    9. }
    10. }

    3.1.3. BeanServiceA属性填充BeanServiceB(完整bean)

    此时ioc容器已经有了beanServiceB,虽然暂时还是不完整的,因为A还没填充初始化完当beanServiceA填充完beanServiceB后,使得互相循环依赖对方,此时二者都变成了完整的bean此时一级缓存的beanServiceB也由不完整的bean变成了完整的bean,因为是同一个地址A涉及AOP,所以循环依赖时,A会进行提前AOP,所以B中填充的是A的代理对象当A填充完B时,构成互相循环依赖对方

    3.1.4. 初始化BeanServiceA时调用AOP后置处理器进行AOP处理

    由于beanServiceA提前AOP了,所以所以earlyProxyReferences有beanServiceA(见上文3.1.2.1.1)remove返回原始的bean,即earlyProxyReferences对应的value,条件不成立,直接==返回原始bean ==

    3.1.5. 处理提前暴露的场景,保证返回同一个代理对象

    beanServiceA涉及提前AOP,getSingleton返回保存在二级缓存中提前AOP的代理对象ASpring如何解决循环依赖的源码分析 - 图4这里面有exposedObject == bean判断,这就是为什么上述3.1.4中AOP后置处理器返回原始bean的原因条件成立,将代理对象赋值放回,保证返回同一个代理对象,即B中的A和A都是同一个代理对象

    3.2. BeanServiceA完整对象写入一级缓存,移除二、三级缓存

    同3.1.2.4节一样,beanServiceA生命周期执行完,会将beanServiceA写入一级缓存,移除二、三级缓存

    四、整体流程

    4.1 涉及循环依赖&&涉及AOP的场景

    Spring如何解决循环依赖的源码分析 - 图5

    4.2 不涉及AOP的循环依赖场景

  • 不涉及AOP,那么A进行lambda表达式回调后返回就A的原始对象,保存到二级缓存中,所以B属性填充后的A也是A的原始对象

  • 当A属性填充完后,A和B相互依赖,使得二者都是完整的对象,可见上文3.1.3的图

    4.3 不涉及循环依赖的AOP场景

  • 不涉及循环依赖,也就不涉及提前AOP,正常A经过实例化–属性填充–初始化

  • 在初始化时通过AOP后置处理器创建代理对象返回,在上文1.1.5节,二级缓存返回null,最终直接返回代理对象A

    4.4 不涉及循环依赖&&不涉及AOP场景

  • 不涉及循环依赖,也就不涉及提前AOP,正常A经过实例化–属性填充–初始化

  • 不涉及AOP,则在初始化时通过AOP后置处理器直接返回原始A对象,在上文1.1.5节,二级缓存返回null,最终返回原始A对象

    五、疑问:

    5.1 为什么三级缓存用HashMap,而不像一级缓存使用ConcurrentHashMap

    现有逻辑是,三级缓存的操作是在synchronized代码块里面操作的,是安全的那为什么要用synchronized而不直接用ConcurrentHashMap来保证线程安全呢?二级缓存put的同时要保证三级缓存remove;三级缓存put时要保证二级缓存remove,也就是说二三级缓存操作要保证原子性因为要保证同一个bean是单例的,不然都会lambda回调创建bean,就不是单例的了如果使用ConcurrentHashMap并不能保证二三级缓存操作的原子性,所以要用synchronized这三级缓存都是在synchronized内操作的,至于一级缓存为什么用ConcurrentHashMap,可能其他场景的原因。

    5.2 什么要第三级缓存?

    主要用于循环依赖的bean需要AOP时提前AOP如果没有第三级缓存,那么getSingleton就返回null,就会再次传教A,导致一直循环创建,现有逻辑就不对.Spring如何解决循环依赖的源码分析 - 图6那如果将实例化的原始对象放入二级缓存呢?没有第三级缓存,就无法提前AOP,则B属性填充完的A为A原始对象而A在属性填充完B后,需要进行AOP,则经过AOP后置处理器会去创建代理对象A返回这就导致B的属性A不是代理对象,而A却是代理对象,这与Spring的单例bean是矛盾的。

    5.3代码里如果出现了循环依赖怎么处理比较好?

  1. 使用@Lazy注解,延迟加载
  2. 使用@DependsOn注解,指定加载先后关系
  3. 修改文件名称,改变循环依赖类的加载顺序
以太坊cppgolang区别 编程

以太坊cppgolang区别

以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
progolang 编程

progolang

Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
golangn个发送者 编程

golangn个发送者

Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
golang技能图谱 编程

golang技能图谱

从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
评论:0   参与:  0