`
357029540
  • 浏览: 737313 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论
阅读更多

    前面说完了一些公共bean的配置引入,我们具体来介绍下git相关代码的实现。通过在EnvironmentRepositoryConfiguration类中,我们可以看到有两处实现git bean的地方,都是使用内部类的方式实现,分别是JGitFactoryConfig和DefaultRepositoryConfiguration,代码如下所示

@Configuration
@ConditionalOnClass(TransportConfigCallback.class)
static class JGitFactoryConfig {

    @Bean
    public MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory(
            ConfigurableEnvironment environment, ConfigServerProperties server,
            Optional<ConfigurableHttpConnectionFactory> jgitHttpConnectionFactory,
            Optional<TransportConfigCallback> customTransportConfigCallback) {
        return new MultipleJGitEnvironmentRepositoryFactory(environment, server, jgitHttpConnectionFactory,
   customTransportConfigCallback);
    }
}

 

@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class)//, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
   @Autowired
   private ConfigurableEnvironment environment;

   @Autowired
   private ConfigServerProperties server;

   @Autowired(required = false)
   private TransportConfigCallback customTransportConfigCallback;

   @Bean
   public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
           MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
         MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
      return gitEnvironmentRepositoryFactory.build(environmentProperties);
   }
}

 

    从上面的代码中,我们可以看到都是通过MultipleJGitEnvironmentRepositoryFactory工厂类进行加载的,由于static类优先加载,所以首先执行的是前面部分代码来初始化工厂类

  我们进入到MultipleJGitEnvironmentRepositoryFactory工厂类可以看到new MultipleJGitEnvironmentRepositoryFactory只是做了一个参数初始化工作,而真正起作用的是bulid()方法,bulid()方法的代码如下

public MultipleJGitEnvironmentRepository build(MultipleJGitEnvironmentProperties environmentProperties)
      throws Exception {
   if (connectionFactory.isPresent()) {
      HttpTransport.setConnectionFactory(connectionFactory.get());
      connectionFactory.get().addConfiguration(environmentProperties);
   }

   MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository(environment,
         environmentProperties);
   repository.setTransportConfigCallback(customTransportConfigCallback
         .orElse(buildTransportConfigCallback(environmentProperties)));
   if (server.getDefaultLabel() != null) {
      repository.setDefaultLabel(server.getDefaultLabel());
   }
   return repository;
}

    首先判断ConfigurableHttpConnectionFactory对象是否存在,通过查看该对象类,我们可以了解到该类主要是读取配置文件中spring.cloud.config.server.git的perfix的值来组成HttpClientConnection连接信息,用于连接git服务器的基础信息,处理完HttpClientConnection信息再初始化MultipleJGitEnvironmentRepository类,一会我们在分析该类,然后给MultipleJGitEnvironmentRepository类设置回调类,回调类不存在的情况下则新增一个回调类用于与git仓库连接,分别使用SshSessionFactory和SshTransport两种方式与git仓库连接,最后判断是否有默认的标签(label,就是一个版本),有就设置。

接下来我们分析下MultipleJGitEnvironmentRepository类,我们先看下它的继承关系图



      进入到这个类,我们首先可以看到它继承自JGitEnvironmentRepository类

public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository

      我们再次进入到JGitEnvironmentRepository类,可以看到该类继承了AbstractScmEnvironmentRepository抽象类和实现了EnvironmentRepository, SearchPathLocator, InitializingBean这3个接口类

public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository
      implements EnvironmentRepository, SearchPathLocator, InitializingBean

       进入到AbstractScmEnvironmentRepository类可以看到它继承了AbstractScmAccessor抽象类和实现了EnvironmentRepository, SearchPathLocator, Ordered这3个接口

public abstract class AbstractScmEnvironmentRepository extends AbstractScmAccessor
      implements EnvironmentRepository, SearchPathLocator, Ordered

     再进入到AbstractScmAccessor抽象类可以看到它实现了ResourceLoaderAware接口类

public abstract class AbstractScmAccessor implements ResourceLoaderAware

     而ResourceLoaderAware接口是用于加载外部资源文件的接口,这里具体使用了DefaultResourceLoader类,这个在这里不多做介绍了,我们从上到下,从最基础的AbstractScmAccessor抽象类介绍到MultipleJGitEnvironmentRepository类的实现,这样有助于我们理解。

1.AbstractScmAccessor抽象类

    从代码中可以看到该类主要定义一些在spring.cloud.config.server出现的属性,同时可以看到在没有定义basedir属性的时候,通过createBaseDir()方法自动生成一个临时文件路径

protected File createBaseDir() {
   try {
      final Path basedir = Files.createTempDirectory("config-repo-");
      Runtime.getRuntime().addShutdownHook(new Thread() {
         @Override
         public void run() {
            try {
               FileSystemUtils.deleteRecursively(basedir);
            }
            catch (IOException e) {
               AbstractScmAccessor.this.logger.warn(
                     "Failed to delete temporary directory on exit: " + e);
            }
         }
      });
      return basedir.toFile();
   }
   catch (IOException e) {
      throw new IllegalStateException("Cannot create temp dir", e);
   }
}

     通过getSearchLocations()这个方法可以了解到主要是到创建配置文件的文件夹路径,把placeholder占位符替换为具体的配置prefix

protected String[] getSearchLocations(File dir, String application, String profile,
      String label) {
   String[] locations = this.searchPaths;
   if (locations == null || locations.length == 0) {
      locations = AbstractScmAccessorProperties.DEFAULT_LOCATIONS;
   }
   else if (locations != AbstractScmAccessorProperties.DEFAULT_LOCATIONS) {
      locations = StringUtils.concatenateStringArrays(AbstractScmAccessorProperties.DEFAULT_LOCATIONS, locations);
   }
   Collection<String> output = new LinkedHashSet<String>();
   for (String location : locations) {
      String[] profiles = new String[] { profile };
      if (profile != null) {
         profiles = StringUtils.commaDelimitedListToStringArray(profile);
      }
      String[] apps = new String[] { application };
      if (application != null) {
         apps = StringUtils.commaDelimitedListToStringArray(application);
      }
      for (String prof : profiles) {
         for (String app : apps) {
            String value = location;
            if (app != null) {
               value = value.replace("{application}", app);
            }
            if (prof != null) {
               value = value.replace("{profile}", prof);
            }
            if (label != null) {
               value = value.replace("{label}", label);
            }
            if (!value.endsWith("/")) {
               value = value + "/";
            }
            output.addAll(matchingDirectories(dir, value));
         }
      }
   }
   return output.toArray(new String[0]);
}

 2.AbstractScmEnvironmentRepository

    我们主要看findOne()方法

public synchronized Environment findOne(String application, String profile, String label) {
   NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(),
         new NativeEnvironmentProperties());
   Locations locations = getLocations(application, profile, label);
   delegate.setSearchLocations(locations.getLocations());
   Environment result = delegate.findOne(application, profile, "");
   result.setVersion(locations.getVersion());
   result.setLabel(label);
   return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(),
         getUri());
}

     在这个方法的3个参数application,profile,lable分别来自客户端配置文件中定义的spring.cloud.config.namespring.cloud.config.profilespring.cloud.config.label,代码中首先定义了一个NativeEnvironmentRepository类对象,后一段代码是通过具体的getLocations()方法实现把placeholder占位符{application}{profile}{label}替换掉为具体的配置值,第4行的NativeEnvironmentRepository.findOne()方法

public Environment findOne(String config, String profile, String label) {
   SpringApplicationBuilder builder = new SpringApplicationBuilder(
         PropertyPlaceholderAutoConfiguration.class);
   ConfigurableEnvironment environment = getEnvironment(profile);
   builder.environment(environment);
   builder.web(WebApplicationType.NONE).bannerMode(Mode.OFF);
   if (!logger.isDebugEnabled()) {
      // Make the mini-application startup less verbose
      builder.logStartupInfo(false);
   }
   String[] args = getArgs(config, profile, label);
   // Explicitly set the listeners (to exclude logging listener which would change
   // log levels in the caller)
   builder.application()
         .setListeners(Arrays.asList(new ConfigFileApplicationListener()));
   ConfigurableApplicationContext context = builder.run(args);
   environment.getPropertySources().remove("profiles");
   try {
      return clean(new PassthruEnvironmentRepository(environment).findOne(config,
            profile, label));
   }
   finally {
      context.close();
   }
}

     进入到该方法,可以看到首先定义了SpringApplicationBuilder类对象builder,后一行代码获取当前的环境变量信息environment并把environment设置到builder中,设置builder在后端启动线程运行模式,在getArgs()方法中初始化一些配置项

private String[] getArgs(String application, String profile, String label) {
   List<String> list = new ArrayList<String>();
   String config = application;
   if (!config.startsWith("application")) {
      config = "application," + config;
   }
   list.add("--spring.config.name=" + config);
   list.add("--spring.cloud.bootstrap.enabled=false");
   list.add("--encrypt.failOnError=" + this.failOnError);
   list.add("--spring.config.location=" + StringUtils.arrayToCommaDelimitedString(
         getLocations(application, profile, label).getLocations()));
   return list.toArray(new String[0]);
}

     然后添加一个ConfigFileApplicationListener的监听类,该监听主要用于配置文件的覆盖的监听,再通过builder.run()方法启动一个新线程来连接rabbit mq,启动后再环境变量中删除propertySource属性的profile变量,最后处理环境变量中propertySources数组name的文件路径为形如file:/D:/baseConfig/publicConfig/merchant1-dev.properties这样的格式,同时取出merchant1-dev.properties文件中的键值对到propertySources属性为source的map集合中

public Environment findOne(String application, String env, String label) {
   Environment result = new Environment(application,
         StringUtils.commaDelimitedListToStringArray(env), label, null, null);
   for (org.springframework.core.env.PropertySource<?> source : this.environment
         .getPropertySources()) {
      String name = source.getName();
      if (!this.standardSources.contains(name)
            && source instanceof MapPropertySource) {
         result.add(new PropertySource(name, getMap(source)));
      }
   }
   return result;

}

      退出NativeEnvironmentRepository.findOne()方法后回到AbstractScmEnvironmentRepository.findOne()方法中,在环境变量environment中加入label和version,最后在EnvironmentCleaner.clean()方法中处理file:/D:/baseConfig/publicConfig/merchant1-dev.properties为https://github.com/422518490/orderSystem/publicConfig/merchant1-dev.properties这种格式的路径

protected Environment clean(Environment value) {
   Environment result = new Environment(value.getName(), value.getProfiles(),
         value.getLabel(), this.version, value.getState());
   for (PropertySource source : value.getPropertySources()) {
      String name = source.getName();
      if (this.environment.getPropertySources().contains(name)) {
         continue;
      }
      name = name.replace("applicationConfig: [", "");
      name = name.replace("]", "");
      if (this.searchLocations != null) {
         boolean matches = false;
         String normal = name;
         if (normal.startsWith("file:")) {
            normal = StringUtils
                  .cleanPath(new File(normal.substring("file:".length()))
                        .getAbsolutePath());
         }
         String profile = result.getProfiles() == null ? null
               : StringUtils.arrayToCommaDelimitedString(result.getProfiles());
         for (String pattern : getLocations(result.getName(), profile,
               result.getLabel()).getLocations()) {
            if (!pattern.contains(":")) {
               pattern = "file:" + pattern;
            }
            if (pattern.startsWith("file:")) {
               pattern = StringUtils
                     .cleanPath(new File(pattern.substring("file:".length()))
                           .getAbsolutePath())
                     + "/";
            }
            if (logger.isTraceEnabled()) {
               logger.trace("Testing pattern: " + pattern
                     + " with property source: " + name);
            }
            if (normal.startsWith(pattern)
                  && !normal.substring(pattern.length()).contains("/")) {
               matches = true;
               break;
            }
         }
         if (!matches) {
            // Don't include this one: it wasn't matched by our search locations
            if (logger.isDebugEnabled()) {
               logger.debug("Not adding property source: " + name);
            }
            continue;
         }
      }
      logger.info("Adding property source: " + name);
      result.add(new PropertySource(name, source.getSource()));
   }
   return result;
}

     在此就讨论完AbstractScmEnvironmentRepository类的处理。

3.JGitEnvironmentRepository

    这个类主要定义了一些初始化配置文件以及从git上获取配置文件的方法。从构造函数可以看出,初始化的信息主要来自配置文件的prefix

public JGitEnvironmentRepository(ConfigurableEnvironment environment, JGitEnvironmentProperties properties) {
   super(environment, properties);
   this.cloneOnStart = properties.isCloneOnStart();
   this.defaultLabel = properties.getDefaultLabel();
   this.forcePull = properties.isForcePull();
   this.timeout = properties.getTimeout();
   this.deleteUntrackedBranches = properties.isDeleteUntrackedBranches();
   this.refreshRate = properties.getRefreshRate();
   this.skipSslValidation = properties.isSkipSslValidation();
}

     在getLocations()方法中,主要是定义Locations类信息以及刷新git提交的版本号,我们可以通过refresh()方法来发现是如何去git获取版本号的

public String refresh(String label) {
   Git git = null;
   try {
      git = createGitClient();
      if (shouldPull(git)) {
         FetchResult fetchStatus = fetch(git, label);
         if (deleteUntrackedBranches && fetchStatus != null) {
            deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git);
         }
         // checkout after fetch so we can get any new branches, tags, ect.
         checkout(git, label);
         tryMerge(git, label);
      }
      else {
         // nothing to update so just checkout and merge.
         // Merge because remote branch could have been updated before
         checkout(git, label);
         tryMerge(git, label);
      }
      // always return what is currently HEAD as the version
      return git.getRepository().findRef("HEAD").getObjectId().getName();
   }
   catch (RefNotFoundException e) {
      throw new NoSuchLabelException("No such label: " + label, e);
   }
   catch (NoRemoteRepositoryException e) {
      throw new NoSuchRepositoryException("No such repository: " + getUri(), e);
   }
   catch (GitAPIException e) {
      throw new NoSuchRepositoryException(
            "Cannot clone or checkout repository: " + getUri(), e);
   }
   catch (Exception e) {
      throw new IllegalStateException("Cannot load environment", e);
   }
   finally {
      try {
         if (git != null) {
            git.close();
         }
      }
      catch (Exception e) {
         this.logger.warn("Could not close git repository", e);
      }
   }
}

     从上面的代码中可以了解到是一个从git获取配置文件的过程,它是以的是eclipse提供的git api方法,先是初始化git的信息,具体初始化信息可以去代码查看,然后在满足重新获取git上的文件信息情况下,重新拉取和覆盖文件信息,删除不需要的分支。 

     在afterPropertiesSet()方法中,它是作为在properties信息加载完成后进行的一些初始化操作,在该方法中initClonedRepository()方法在满足启动的时候prefix属性设置为true是进入该方法

private void initClonedRepository() throws GitAPIException, IOException {
   if (!getUri().startsWith(FILE_URI_PREFIX)) {
      deleteBaseDirIfExists();
      Git git = cloneToBasedir();
      if (git != null) {
         git.close();
      }
      git = openGitRepository();
      if (git != null) {
         git.close();
      }
   }

}

     从该方法可以看出主要是在初始化的时候删除已经存在的文件,以及从git上面拉取配置文件路径下的文件信息到本地重新生成文件信息,具体的逻辑代码可以到代码里面查看。

     在refresh()方法中通过new Locations()方法返回该对象,在new的过程中通过getSearchLocations()拆分形成了形如的file:/D:/baseConfig/和file:/D:/baseConfig/publicConfig/的数组String路径,这个路径是在配置bootstrap.properties中定义的。

4.MultipleJGitEnvironmentRepository

    从构造函数可以看出,当有多个配置中心的时候会通过以下代码进行分解取值

properties.getRepos().entrySet().stream()
      .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(),
            new PatternMatchingJGitEnvironmentRepository(environment, e.getValue())))
      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))

     通过重写afterPropertiesSet()方法在加载完成prefix之后初始化信息。我们在来主要看一下findOne()方法

public Environment findOne(String application, String profile, String label) {
   for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
      if (repository.matches(application, profile, label)) {
         for (JGitEnvironmentRepository candidate : getRepositories(repository,
               application, profile, label)) {
            try {
               if (label == null) {
                  label = candidate.getDefaultLabel();
               }
               Environment source = candidate.findOne(application, profile,
                     label);
               if (source != null) {
                  return source;
               }
            }
            catch (Exception e) {
               if (logger.isDebugEnabled()) {
                  this.logger.debug(
                        "Cannot load configuration from " + candidate.getUri()
                              + ", cause: (" + e.getClass().getSimpleName()
                              + ") " + e.getMessage(),
                        e);
               }
               continue;
            }
         }
      }
   }
   JGitEnvironmentRepository candidate = getRepository(this, application, profile,
         label);
   if (label == null) {
      label = candidate.getDefaultLabel();
   }
   if (candidate == this) {
      return super.findOne(application, profile, label);
   }
   return candidate.findOne(application, profile, label);
}

     在方法中首先通过获取Map中的配置中心信息来循环进行获取配置信息,前提是已经赋值了Map集合信息,在for循环中经过getRepositories()方法进行了一系列的placeholder替换符合操作为正确的字符串以及赋值操作,具体可以在代码中查看,然后通过JGitEnvironmentRepository的findOne()方法获取Environment类信息,存在Environment信息就直接返回,在没有Map集合信息的情况下,直接通过application, profile,label三个参数去JGitEnvironmentRepository类调用findOne()方法,即AbstractScmEnvironmentRepository类的findOne()方法,返回获取到的Environment信息。

    在下一篇文章我们将大体介绍server端的其他类,如ConfigServerEncryptionConfiguration、EncryptionAutoConfiguration。

  • 大小: 50.3 KB
0
0
分享到:
评论

相关推荐

    基于spring cloud项目源码源码.rar

    《Spring Cloud项目源码深度解析》 在当前的微服务架构领域,Spring Cloud以其强大的功能和易用性,成为开发者构建分布式系统的重要选择。本文将深入探讨基于Spring Cloud的项目源码,帮助读者理解其核心原理,提升...

    尚硅谷SpringCloud视频 + 源码 百度网盘

    ### 二、Dubbo与SpringCloud的比较 #### 2.1 Dubbo简介 Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用、智能容错和恢复机制、以及服务自动注册和发现。 #### 2.2 ...

    spring cloud 配置源码.zip

    《Spring Cloud配置源码解析与实战》 Spring Cloud作为微服务架构的重要组件,为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话...

    spring-cloud实战源码

    本教程将从基础入手,逐步深入到源码解析,使读者能更深入地了解和运用Spring Cloud。 首先,我们从入门开始。Spring Cloud提供了一个快速构建分布式系统工具集,包括服务发现(Eureka)、配置中心(Config Server...

    springcloud 源码+解读

    1. **SpringCloud源码解析**: SpringCloud的源码分析有助于开发者了解其实现机制,从而更好地定制和优化自己的服务。源码中包含了Eureka服务发现、Zuul边缘服务、Hystrix断路器、 Ribbon客户端负载均衡、Feign声明...

    尚硅谷SpringCloud的源码及思维导图

    总的来说,通过尚硅谷的SpringCloud源码解析和思维导图,开发者不仅可以深入了解SpringCloud的运作原理,还能提升对微服务架构设计的理解,为实际项目开发提供有力的支持。同时,对源码的学习也有助于培养解决问题和...

    SpringCloud源码-01.zip

    《SpringCloud源码解析——构建微服务架构的关键组件》 SpringCloud作为当今最热门的微服务框架之一,深受广大开发者喜爱。它集成了众多优秀的开源项目,为开发人员提供了便捷的微服务开发工具。本篇将围绕Spring...

    springcloud-config-master.rar

    《SpringCloud Config 源码解析与应用实践》 SpringCloud Config 是一个强大的微服务配置中心,它允许我们在运行时管理应用的配置,并且能够实时地推送到客户端。本篇文章将深入探讨 SpringCloud Config 的核心原理...

    基于springcloud的电商平台源码.zip

    《基于SpringCloud的电商平台源码解析》 在现代互联网应用开发中,微服务架构已经成为主流。Spring Cloud作为一套完整的微服务解决方案,为开发者提供了构建分布式系统所需的工具集合,包括服务发现、配置中心、...

    适合新手入门的springcloud完整项目资源,附带sql和详细的开发文档,可直接导入运行。

    本资源为新手提供了一个完整的SpringCloud入门项目,包括源码、SQL脚本和详细的开发文档,非常适合想要快速了解和学习SpringCloud的新手。 1. **SpringCloud简介** SpringCloud是基于SpringBoot构建的服务治理框架...

    基于springcloud的分布式网上商城系统源码.zip

    《基于SpringCloud的分布式网上商城系统源码解析》 在当今互联网时代,电子商务系统的复杂性和规模日益增大,传统的单体架构难以应对高并发、高可用的业务需求。为了解决这些问题,开发人员开始转向分布式系统架构...

    SpringCloud黑马商城后端代码

    本篇文章将深入探讨“SpringCloud黑马商城后端代码”,解析其中的关键技术和实现细节。 首先,Spring Cloud是基于Spring Boot的一套微服务解决方案,它提供了服务注册与发现、配置中心、API网关、负载均衡、熔断器...

    springcloud学习源码-yuanma.zip

    本篇将深入探讨Spring Cloud的核心组件和原理,结合"springcloud学习源码-yuanma.zip"中的源码,为你带来一次全面的学习体验。 首先,我们来了解Spring Cloud的基础知识。Spring Cloud是基于Spring Boot的微服务...

    springCloud

    断路器示意图 SpringCloud Netflix实现了断路器库的名字叫Hystrix. 在微服务架构下,通常会有多个层次的服务调用. 下面是微服架构下, 浏览器端通过API访问后台微服务的一个示意图: hystrix 1 一个微服务的超时...

    2020最新版SpringCloud(H版&alibaba)框架开发教程全套完整版从入门到精通(41-80讲).rar

    《2020最新版SpringCloud(H版&alibaba)框架开发教程》是一套全面而深入的SpringCloud学习资源,涵盖了从基础到高级的各种技术点。这套教程旨在帮助开发者掌握SpringCloud的核心概念和实践技巧,尤其针对H版及阿里...

    <>源代码

    《SpringCloud微服务实战》这本书籍的源代码包含了大量的实践示例和配置,旨在帮助读者深入理解并掌握Spring Cloud在实际开发中的应用。Spring Cloud是一个基于Spring Boot实现的服务发现、配置管理和API网关等...

    springcloud的所有技能都在这里面了

    总的来说,这份"Spring Cloud.pdf"学习资料涵盖了Spring Cloud的基础知识、核心组件的使用、源码解析以及实战演示,对于想要深入学习和掌握Spring Cloud的开发者来说是一份宝贵的资源。通过系统学习并实践其中的示例...

    从零开始搭建SpringCloud第七节源码

    源码解析可以帮助我们理解Zuul如何处理请求,以及过滤器链的执行流程。 3. **断路器** - Hystrix是Netflix开源的断路器库,用于防止服务雪崩。当服务调用失败或响应时间过长时,断路器会打开,避免后续请求继续失败...

    spring cloud实战项目

    1. 配置中心:可能使用了Spring Cloud Config来集中管理所有服务的配置,这样可以方便地在不同环境中切换配置。 2. 服务注册与发现:可能使用Eureka或Consul等服务注册与发现组件,确保服务之间的通信。 3. 安全配置...

    疯狂springCloud实战架构

    《疯狂springCloud实战架构》是针对企业级分布式应用开发的一款强大框架——Spring Cloud的深度解析与实战指南。Spring Cloud作为微服务生态中的重要组件,它为开发者提供了在分布式系统(如配置管理、服务发现、...

Global site tag (gtag.js) - Google Analytics