注册中心 Eureka 源码解析 —— 网络通信

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 注册中心 Eureka 源码解析 —— 网络通信

摘要: 原创出处 http://www.iocoder.cn/Eureka/transport/ 「芋道源码」欢迎转载,保留摘要,谢谢!

本文主要基于 Eureka 1.8.X 版本

    1. 概述
    2. 2. EurekaHttpClient
  • 2.1 EurekaJerseyClientImpl

  • 2.2 EurekaJerseyClientBuilder

  • 3.1 EurekaHttpResponse

  • 3.2 TransportClientFactory

  • 4.1 JerseyApplicationClient

  • 4.2 JerseyReplicationClient

  • 5.1 MetricsCollectingEurekaHttpClient

  • 5.2 RedirectingEurekaHttpClient

  • 5.3 RetryableEurekaHttpClient

  • 5.4 SessionedEurekaHttpClient

1. 概述

本文主要分享 Eureka 的网络通信部分。在不考虑 Eureka 2.x 的兼容的情况下,Eureka 1.x 主要两部分的网络通信:

  • Eureka-Client 请求 Eureka-Server 的网络通信
  • Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信

本文涉及类在  com.netflix.discovery.shared.transport 包下,涉及到主体类的类图如下( 打开大图 ):

  • 粉色部分 —— EurekaJerseyClient ,对基于 Jersey Server 的 Eureka-Server 的 Jersey 客户端封装。
  • 绿色部分 —— EurekaHttpClient ,Eureka-Server HTTP 访问客户端,定义了具体的 Eureka-Server API 调用方法。如果把 DiscoveryClient 类比成 Service ,那么 EurekaHttpClient 可以类比城 Dao 。
  • 综色部分 —— EurekaHttpClient 实现类,真正实现了具体的 Eureka-Server API 调用方法。
  • 红色部分 —— EurekaHttpClient 委托类,提供了会话、重试、重定向、监控指标收集等特性。
  • 黄色部分 —— EurekaHttpClientFactory,用于创建 EurekaHttpClient 。

类图看起来很复杂,整体调用关系如下( 打开大图 ):

OK ,我们逐层解析,嗨起来。

推荐 Spring Cloud 书籍

  • 请支持正版。下载盗版,等于主动编写低级 BUG 。
  • 程序猿DD —— 《Spring Cloud微服务实战》
  • 周立 —— 《Spring Cloud与Docker微服务架构实战》
  • 两书齐买,京东包邮。

推荐 Spring Cloud 视频

  • Java 微服务实践 - Spring Boot
  • Java 微服务实践 - Spring Cloud
  • Java 微服务实践 - Spring Boot / Spring Cloud

2. EurekaHttpClient

com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient ,EurekaHttpClient 接口。接口代码如下:


public interface EurekaJerseyClient {

    ApacheHttpClient4 getClient();

    void destroyResources();
}
  • com.sun.jersey.client.apache4.ApacheHttpClient4 ,基于 Apache HttpClient4 实现的 Jersey Client 。

2.1 EurekaJerseyClientImpl

com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl ,EurekaHttpClient 实现类。实现代码如下:


// 超过微信字数上限
  • com.netflix.discovery.shared.transport.jersey.ApacheHttpClientConnectionCleaner ,Apache HttpClient 空闲连接清理器,负责周期性关闭处于  half-close 状态的空闲连接。点击 链接 查看带中文注释的 ApacheHttpClientConnectionCleaner。推荐阅读:《HttpClient容易忽视的细节——连接关闭》 。

2.2 EurekaJerseyClientBuilder

EurekaJerseyClientBuilder ,EurekaJerseyClientImpl 内部类,用于创建 EurekaJerseyClientImpl 。

调用  #build() 方法,创建 EurekaJerseyClientImpl ,实现代码如下:


// EurekaJerseyClientBuilder.java
public EurekaJerseyClient build() {
    MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
    try {
        return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
    } catch (Throwable e) {
        throw new RuntimeException("Cannot create Jersey client ", e);
    }
}
  • MyDefaultApacheHttpClient4Config ,继承自 `com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config` ,实现**自定义配置**。点击 链接 查看带中文注释的 MyDefaultApacheHttpClient4Config。例如 :
      - 自定义的**请求、响应的编解码器** `com.netflix.discovery.provider.DiscoveryJerseyProvider` 。 - 禁用**重定向**,使用 RedirectingEurekaHttpClient 实现该特性。 - 自定义 UserAgent 。 - 自定义 Http Proxy 。 - SSL 功能的增强。ApacheHttpClient4 使用的是 Apache HttpClient 4.1.1 版本,`com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter` 将 Apache HttpClient 4.3.4 对 SSL 功能的增强适配到老版本 API 。点击 链接 查看带中文注释的 SSLSocketFactoryAdapter。

      3. EurekaHttpClient

      com.netflix.discovery.shared.transport.EurekaHttpClient ,Eureka-Server HTTP 访问客户端,定义了具体的 Eureka-Server API 调用方法 。点击 链接 查看带中文注释的 EurekaHttpClient。

      3.1 EurekaHttpResponse

      com.netflix.discovery.shared.transport.EurekaHttpResponse ,请求响应对象,实现代码如下:

      
      public class EurekaHttpResponseT {
      
          /**
           * 返回状态码
           */
          private final int statusCode;
          /**
           * 返回对象( Entity )
           */
          private final T entity;
          /**
           * 返回 header
           */
          private final MapString, String headers;
          /**
           * 重定向地址
           */
          private final URI location;
      
          // ... 省略 setting / getting 和 Builder
      }
      

      3.2 TransportClientFactory

      com.netflix.discovery.shared.transport.TransportClientFactory ,创建 EurekaHttpClient 的工厂接口。接口代码如下:

      
      public interface TransportClientFactory {
      
          /**
           * 创建 EurekaHttpClient
           *
           * @param serviceUrl Eureka-Server 地址
           * @return EurekaHttpClient
           */
          EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
      
          /**
           * 关闭工厂
           */
          void shutdown();
      
      }
      

      大多数 EurekaHttpClient 实现类都有其对应的工厂实现类

      4. AbstractJerseyEurekaHttpClient

      com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient ,实现 EurekaHttpClient 的抽象类真正实现了具体的 Eureka-Server API 调用方法。实现代码如下:

      
      // 超过微信字数上限
      
    • `jerseyClient` 属性,Jersey Client ,使用上文的 `EurekaHttpClient#getClient(…)` 方法,获取 ApacheHttpClient4 。
    • `serviceUrl` 属性,请求的 Eureka-Server 地址。
    • `#register()` 方法,实现向 Eureka-Server 注册应用实例。**其他方法代码类似**。
        - x
      • 第 22 至 26 行 :设置请求地址。
      • 第 28 行 :调用 `#addExtraHeaders(...)` 方法,设置请求头( header )。该方法是**抽象方法**,提供子类实现自定义的请求头。代码如下:
        protected abstract void addExtraHeaders(Builder webResource);
        
      • serviceUrl 属性,请求的 Eureka-Server 地址。

        第 22 至 26 行 :设置请求地址。

        第 29 至 34 行 :请求 Eureka-Server 。

        第 35 至 36 行 :解析响应结果,创建 EurekaHttpResponse 。

        4.1 JerseyApplicationClient

        com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient ,实现 Eureka-Client 请求 Eureka-Server 的网络通信。点击 链接 查看带中文注释的 JerseyApplicationClient。

        4.1.1 JerseyEurekaHttpClientFactory

        com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory ,创建 JerseyApplicationClient 的工厂类。实现代码如下:

        
        // 超过微信字数上限
        

        4.1.2 JerseyEurekaHttpClientFactoryBuilder

        JerseyEurekaHttpClientFactoryBuilder ,JerseyEurekaHttpClientFactory 内部类,用于创建 JerseyEurekaHttpClientFactory 。点击 链接 查看带中文注释的 JerseyEurekaHttpClientFactory。

        调用  JerseyEurekaHttpClientFactory#create(...) 方法,创建 JerseyEurekaHttpClientFactory ,实现代码如下:

        
        // 超过微信字数上限
        

        4.2 JerseyReplicationClient

        com.netflix.eureka.transport.JerseyReplicationClient ,Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信。

      • 实现 `AbstractJerseyEurekaHttpClient#addExtraHeaders()` 方法,添加自定义头 `x-netflix-discovery-replication=true` ,代码如下:
        @Override
        protected void addExtraHeaders(Builder webResource) {
           webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
        }
        
      • 重写了 `#sendHeartBeat(…)` 方法,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。
      • 实现 `com.netflix.eureka.cluster.HttpReplicationClient` 接口,实现了 `#submitBatchUpdates(…)` 方法,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。
      • 重写了  #sendHeartBeat(…) 方法,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。

        4.2.1 没有工厂

        JerseyReplicationClient 没有专属的工厂

        调用  JerseyReplicationClient#createReplicationClient(...) 静态方法,创建 JerseyReplicationClient 。点击 链接 查看带中文注释的方法代码。

        5. EurekaHttpClientDecorator

        com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator,EurekaHttpClient 委托者抽象类。实现代码如下:

        
        // 超过微信字数上限
        
      • `#execute(…)` **抽象方法**,子类实现该方法,实现自己的特性。
      • `#register()` 方法,实现向 Eureka-Server 注册应用实例。**其他方法代码类似**。
          - 调用 `#execute(…)` 方法,并将原有的注册实现通过 RequestExecutor 传递进去。 - 子类在实现的 `#execute(…)` 方法,可以调用 `RequestExecutor#execute(…)` 方法,继续执行原有逻辑。 - 参考设计模式:《设计模式 ( 十九 ) 模板方法模式Template method(类行为型)》 。

          #register() 方法,实现向 Eureka-Server 注册应用实例。其他方法代码类似

          RequestType ,请求类型枚举类。代码如下:

          
          // EurekaHttpClientDecorator.java
          public enum RequestType {
             Register,
             Cancel,
             SendHeartBeat,
             StatusUpdate,
             DeleteStatusOverride,
             GetApplications,
             GetDelta,
             GetVip,
             GetSecureVip,
             GetApplication,
             GetInstance,
             GetApplicationInstance
          }
          

          RequestExecutor ,请求执行器接口。接口代码如下:

          
          // EurekaHttpClientDecorator.java
          // 超过微信字数上限
          

          EurekaHttpClientDecorator 的每个实现类实现一个特性,代码非常非常非常清晰。

          FROM 《委托模式》  委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin。

          我们在上图的基础上,增加委托的关系,如下图( 打开大图 ):

          • 请注意,每个委托着实现类,上面可能有类型为 EurekaHttpClientFactory 的属性,用于创建其委托的 EurekaHttpClient 。为什么会有 Factory ?例如,RetryableEurekaHttpClient 重试请求多个 Eureka-Server 地址时,每个 Eureka-Server 地址会创建一个 EurekaHttpClient 。所以,下文涉及到 EurekaHttpClientFactory 和委托的 EurekaHttpClient 的地方,你都需要仔细理解。

          5.1 MetricsCollectingEurekaHttpClient

          com.netflix.discovery.shared.transport.decorator.MetricsCollectingEurekaHttpClient ,监控指标收集 EurekaHttpClient ,配合 Netflix Servo 实现监控信息采集。

          #execute() 方法,代码如下:

          
          // 超过微信字数上限
          
        • 第 10 行 :调用 `RequestExecutor#execute(…)` 方法,继续执行请求。
            - `delegate` 属性,对应 JerseyApplicationClient 。

            5.2 RedirectingEurekaHttpClient

            com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient ,寻找非 302 重定向的 Eureka-Server 的 EurekaHttpClient 。

            #execute() 方法,代码如下:

            
            // 超过微信字数上限
            
            •  

            • 注意:和我们理解的常规的 302 状态返回处理不同!!!

            • 整个分成两部分:【第 4 至 15 行】、【第 16 至 24 行】。
              • 当返回非 302 状态码时,找到非返回 302 状态码的 Eureka-Server 。

              • 当返回 302 状态码时,向新的重定向的 Eureka-Server 执行请求直到成功找到或超过最大次数。

              • 前者,意味着未找到非返回 302 状态码的 Eureka-Server ,此时通过在原始传递进来的 serviceUrls 执行请求,寻找非 302 状态码返回的 Eureka-Server。

              5.2.1 工厂

              RedirectingEurekaHttpClient 提供  #createFactory(...) 静态方法获得创建其的工厂,点击 链接 查看。

              5.3 RetryableEurekaHttpClient

              com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient ,支持向多个 Eureka-Server 请求重试的 EurekaHttpClient 。

              #execute() 方法,代码如下:

              
              // 超过微信字数上限
              
            • 第 10 行 :当前 `currentHttpClient` 不存在,意味着原有 `delegate` 不存在向 Eureka-Server 成功请求的 EurekaHttpClient 。
                - 此时需要从配置中的 Eureka-Server 数组重试请求,获得可以请求的 Eureka-Server 。 - 如果已经存在请求成功的 `delegate` ,直接使用它进行执行请求。

                第 11 至 17 行 :调用  #getHostCandidates() 方法,获得候选的 Eureka-Server  serviceUrls 数组。实现代码如下:

                
                // 超过微信字数上限
                
                • 第 10 行 :最小可用的阀值,配置  eureka.retryableClientQuarantineRefreshPercentage 来设置百分比,默认值: 0.66 。
                • 最 13 至 15 行 : quarantineSet 数量超过阀值,清空  quarantineSet ,全部  candidateHosts重试。
                • 第 17 至 24 行 : quarantineSet 数量未超过阀值,移除  candidateHosts 中在  quarantineSet的元素。
                • 第 3 行 :调用  ClusterResolver#getClusterEndpoints() 方法,获得候选的 Eureka-Server 地址数组(  candidateHosts )。注意:该方法返回的 Eureka-Server 地址数组,使用以本机 IP 为随机种子,达到不同 IP 的应用实例获得的数组顺序不同,而相同 IP 的应用实例获得的数组顺序一致,效果类似基于 IP HASH 的负载均衡算法。实现该功能的代码,在 《Eureka 源码解析 —— EndPoint 与 解析器》搜索关键字【ResolverUtils#randomize(…)】 详细解析。
                • 第 6 行 :调用  Set#retainAll() 方法,移除隔离的故障 Eureka-Server 地址数组(  quarantineSet ) 中不在  candidateHosts 的元素。
                • 第 8 至 24 行 :在保证最小可用的  candidateHosts,移除在  quarantineSet 的元素。

                第 19 至 22 行 :超过  candidateHosts 上限,全部 Eureka-Server 请求失败,抛出异常。

                第 24 至 26 行 :创建委托的 EurekaHttpClient ,用于下面请求执行。

                第 31 行 :执行请求。

                第 33 行 :调用  ServerStatusEvaluator#accept() 方法,判断响应状态码和请求类型是否能够接受。实现代码如下:

                
                // ServerStatusEvaluators.java
                // 超过微信字数上限
                

                第 34 行 :请求成功,设置  delegate 。下次请求,优先使用  delegate ,失败才进行候选的 Eureka-Server 地址数组重试。

                第 47 行 :请求失败, delegate 若等于  currentHttpClient ,进行清除。

                第 50 至 52 行 :请求失败,将请求的 Eureka-Server 地址添加到  quarantineSet 。

                总结来说:

                • 【第一步】若当前有请求成功的 EurekaHttpClient ,继续使用。若请求失败,执行【第二步】。
                • 【第二步】若当前无请求成功的 EurekaHttpClient ,获取候选的 Eureka-Server 地址数组顺序创建新的 EurekaHttpClient,直到成功,或者超过最大重试次数。当请求成功,保存该 EurekaHttpClient ,下次继续使用,直到请求失败。

                5.3.1 工厂

                RetryableEurekaHttpClient 提供  #createFactory(...) 静态方法获得创建其的工厂,点击 链接 查看。

                5.4 SessionedEurekaHttpClient

                com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient ,支持会话的 EurekaHttpClient 。执行定期的重建会话,防止一个 Eureka-Client 永远只连接一个特定的 Eureka-Server 。反过来,这也保证了 Eureka-Server 集群变更时,Eureka-Client 对 Eureka-Server 连接的负载均衡。

                #execute(...) ,代码如下:

                
                // 超过微信字数上限
                
              • 第 7 至 12 行 :超过当前会话时间,关闭当前委托的 EurekaHttpClient 。
                  - 增加会话过期的随机性,实现所有 Eureka-Client 的会话过期重连的发生时间更加离散,避免集中时间过期。目前猜测这么做的目的和 TODO[0028]:写入集群和读取集群 有关,即返回 302 。关联 1.x new transport enhancements 。
                • 第 10 行 :调用 `#randomizeSessionDuration(...)` 方法,计算计算下一次会话超时时长,公式为 `sessionDurationMs * (0.5, 1.5)` ,代码如下:
                  protected long randomizeSessionDuration(long sessionDurationMs) {
                     long delta = (long) (sessionDurationMs * (random.nextDouble() - 0.5));
                     return sessionDurationMs + delta;
                  }
                  
                • 第 10 行 :调用  #randomizeSessionDuration(...) 方法,计算计算下一次会话超时时长,公式为  sessionDurationMs * (0.5, 1.5) ,代码如下:

                  第 15 至 18 行 :获得委托的 EurekaHttpClient 。若不存在,创建新的委托的 EurekaHttpClient 。 TransportUtils#getOrSetAnotherClient(...) 方法代码如下:

                  
                  // 超过微信字数上限
                  
                • 该方法实现,获得 `eurekaHttpClientRef` 里的 EurekaHttpClient 。若获取不到,将 `another` 设置到 `eurekaHttpClientRef` 。当有多个线程设置时,有且只有一个线程设置成功,另外的设置失败的线程们,意味着当前 `eurekaHttpClientRef` 有 EurekaHttpClient ,返回 `eurekaHttpClientRef` 。
                • 目前该方法存在 BUG ,失败的线程直接返回 `existing` 的是 `null` ,需要修改成 `return eurekaHttpClientRef.get()` 。模拟重现该 BUG 代码如下 :
                  注册中心 Eureka 源码解析 —— 网络通信
                • 目前该方法存在 BUG ,失败的线程直接返回  existing 的是  null ,需要修改成  return eurekaHttpClientRef.get() 。模拟重现该 BUG 代码如下 :

                  第 19 行 :执行请求。

                  5.4.1 没有工厂

                  在 SessionedEurekaHttpClient 类里,没有实现创建其的工厂。在 「6. 创建网络通讯客户端」搜索  canonicalClientFactory ,可以看到  EurekaHttpClients#canonicalClientFactory(...) 方法,内部有 SessionedEurekaHttpClient 的创建工厂。

                  6. 创建网络通讯客户端

                  对于 Eureka-Server 来说,调用  JerseyReplicationClient#createReplicationClient(...) 静态方法即可创建用于 Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信客户端。

                  对于 Eureka-Client 来说,分成用于注册应用实例(  registrationClient )查询注册信息(  newQueryClient )两个不同网络通信客户端。在 DiscoveryClient 初始化时进行创建,代码如下:

                  
                  // DiscoveryClient.class
                  // 超过微信字数上限
                  
                • 第 18 至 27 行 :调用 `Jersey1TransportClientFactories#newTransportClientFactory(...)` 方法,创建 `registrationClient` 和 `queryClient` 公用的委托的 EurekaHttpClientFactory ,代码如下:
                  // Jersey1TransportClientFactories.java
                  // 超过微信字数上限
                  
                    - 在 TransportClientFactory 里**委托** JerseyEurekaHttpClientFactory 。

                    第 34 至 49 行 :调用  EurekaHttpClients#registrationClientFactory(...) 方法,创建  registrationClient 的 EurekaHttpClientFactory ,代码如下 :

                    
                    // EurekaHttpClients.java
                    // 超过微信字数上限
                    

                    第 51 至 71 行 :调用  EurekaHttpClients#queryClientFactory(...) 方法,创建  queryClient 的 EurekaHttpClientFactory ,代码如下 :

                    
                    // EurekaHttpClients.java
                    // 超过微信字数上限
                    

                    666. 彩蛋

                    这次真的是彩蛋,我们将整体调用关系调整如下如下( 打开大图 ):

                    胖友,你学会了么?

                    胖友,分享我的公众号( 芋道源码 ) 给你的胖友可好?

                    如果你对 Dubbo 感兴趣,欢迎加入我的知识星球一起交流。

                    注册中心 Eureka 源码解析 —— 网络通信

                    目前在知识星球(https://t.zsxq.com/2VbiaEu)更新了如下 Dubbo 源码解析如下:

                  • 01. 调试环境搭建

                    02. 项目结构一览

                    1. 配置 Configuration

                    04. 核心流程一览

                    • 05. 拓展机制 SPI
                      1. 线程池
                    • 07. 服务暴露 Export
                    • 08. 服务引用 Refer
                      1. 注册中心 Registry
                      1. 动态编译 Compile
                      1. 动态代理 Proxy
                      1. 服务调用 Invoke
                      1. 调用特性
                      1. 过滤器 Filter
                      1. NIO 服务器
                      1. P2P 服务器
                      1. HTTP 服务器
                      1. 序列化 Serialization
                      1. 集群容错 Cluster
                      1. 优雅停机
                      1. 日志适配
                      1. 状态检查
                      1. 监控中心 Monitor
                      1. 管理中心 Admin
                      1. 运维命令 QOS
                      1. 链路追踪 Tracing
                      2. ...

                    一共 60 篇++

  • 本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    转载声明:转载请注明出处,本技术博客是本人原创文章

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 注册中心 Eureka 源码解析 —— 网络通信


     上一篇
    注册中心 Eureka 源码解析 —— Eureka-Server 集群同步 注册中心 Eureka 源码解析 —— Eureka-Server 集群同步
    源码精品专栏 [精尽 Dubbo 原理与源码专栏( 已经完成 69+ 篇,预计总共 75+ 篇 )](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484647&idx=
    2021-04-05
    下一篇 
    注册中心 Eureka 源码解析 —— EndPoint 与 解析器 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
    精尽 Dubbo 原理与源码专栏( 已经完成 69+ 篇,预计总共 75+ 篇 ) 中文详细注释的开源项目 Java 并发源码合集 RocketMQ 源码合集 Sharding-JDBC 源码解析合集 Spring MVC 和 Secur
    2021-04-05