SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(3)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(3)

image.png


本系列代码地址: https://github.com/JoJoTec/spring-cloud-parent

我们继续分析上一节提到的 WebHandler。加入 Spring Cloud Sleuth 以及 Prometheus 相关依赖之后, Spring Cloud Gateway 的处理流程如下所示:


image.png


Spring Cloud Gateway 入口 -> WebFlux 的 DefaultWebFilterChain


Spring Cloud Gateway 是基于 Spring WebFlux 开发的异步响应式网关,异步响应式代码比较难以理解和阅读,我这里给大家分享一种方法去理解,通过这个流程来理解 Spring Cloud Gateway 的工作流程以及底层原理。其实可以理解为,上图这个流程,就是拼出来一个完整的 Mono(或者 Flux)流,最后 subscribe 执行。

当收到一个请求的时候,会经过 org.springframework.web.server.handler.DefaultWebFilterChain,这是 WebFilter 的调用链,这个链路包括三个 WebFilter:

  • org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter:添加 Prometheus 相关依赖之后,会有这个 MetricsWebFilter,用于记录请求处理耗时,采集相关指标。
  • org.springframework.cloud.sleuth.instrument.web.TraceWebFilter:添加 Spring Cloud Sleuth 相关依赖之后,会有这个 TraceWebFilter。
  • org.springframework.cloud.gateway.handler.predicate.WeightCalculatorWebFilter:Spring Cloud Gateway 路由权重相关配置功能相关实现类,这个我们这里不关心。

在这个 DefaultWebFilterChain 会形成这样一个 Mono,我们依次将他们标记出来,首先是入口代码 org.springframework.web.server.handler.DefaultWebFilterChain#filter

public Mono<Void> filter(ServerWebExchange exchange) {
  return Mono.defer(() ->
          // this.currentFilter != null 代表 WebFilter 链还没有结束
          // this.chain != null 代表 WebFilter 链不为空
      this.currentFilter != null && this.chain != null ?
          //在 WebFilter 链没有结束的情况下,调用 WebFilter
          invokeFilter(this.currentFilter, this.chain, exchange) :
          //在 WebFilter 结束的情况下,调用 handler 
          this.handler.handle(exchange));
}

对于我们这里的 WebFilter 链的第一个 MetricsWebFilter,假设启用了对应的采集统计的话,这时候生成的 Mono 就是:

return Mono.defer(() ->
  chain.filter(exchange).transformDeferred((call) -> {
    long start = System.nanoTime();
    return call
        //成功时,记录响应时间
        .doOnSuccess((done) -> MetricsWebFilter.this.onSuccess(exchange, start))
        //失败时,记录响应时间和异常
        .doOnError((cause) -> MetricsWebFilter.this.onError(exchange, start, cause));
  });
);

这里为了方便,我们对代码做了简化,由于我们要将整个链路的所有 Mono 和 Flux 拼接在一起行程完整链路,所以原本是 MetricsWebFilter中的 onSuccess(exchange, start)方法,被改成了 MetricsWebFilter.this.onSuccess(exchange, start) 这种伪代码。

接着,根据DefaultWebFilterChain 的源码分析,chain.filter(exchange) 会继续 WebFilter 链路,到达下一个 WebFilter,即 TraceWebFilter。经过 TraceWebFilter,Mono 就会变成:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, chain.filter(exchange), TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

可以看出,在 TraceWebFilter 中,整个内部 Mono (chain.filter(exchange) 后续的结果)都被封装成了一个 MonoWebFilterTrace,这也是保持链路追踪信息的关键实现。

继续 WebFilter 链路,经过最后一个 WebFilter WeightCalculatorWebFilter; 这个 WebFilter 我们不关心,里面对路由权重做了一些计算操作,我们这里直接忽略即可。这样我们就走完了所有 WebFilter 链路,来到了最后的调用 DefaultWebFilterChain.this.handler,这个 handler 就是 org.springframework.web.reactive.DispatcherHandler。在 DispatcherHandler 中,我们会计算出路由并发送请求到符合条件的 GatewayFilter。经过 DispatcherHandler,Mono 会变成:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    Flux.fromIterable(DispatcherHandler.this.handlerMappings) //读取所有的 handlerMappings
      .concatMap(mapping -> mapping.getHandler(exchange)) //按顺序调用所有的 handlerMappings 的 getHandler 方法,如果有对应的 Handler 会返回,否则返回 Mono.empty();
      .next() //找到第一个返回不是 Mono.empty() 的 Handler
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //调用对应的 Handler
      .flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //处理结果
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

handlerMappings 包括:

  • org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndPointHandlerMapping:由于我们项目中添加了 Actuator 相关依赖,所以这里有这个 HandlerMapping。Actuator 相关路径映射,不是我们这里关心的。但是可以看出,Actuator 相关路径优先于 Spring Cloud Gateway 配置路由
  • org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping:由于我们项目中添加了 Actuator 相关依赖,所以这里有这个 HandlerMapping。使用 @ControllerEndpoint 或者 @RestControllerEndpoint 注解标注的 Actuator 相关路径映射,不是我们这里关心的。
  • org.springframework.web.reactive.function.server.support.RouterFunctionMapping:在 Spring-WebFlux 中,你可以定义很多不同的 RouterFunction 来控制路径路由,但这也不是我们这里关心的。但是可以看出,自定义的 RouterFunction 会优先于 Spring Cloud Gateway 配置路由
  • org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping:针对 @RequestMapping 注解的路径的 HandlerMapping,不是我们这里关心的。但是可以看出,如果你在 Spring Cloud Gateway 中指定 RequestMapping 路径,会优先于 Spring Cloud Gateway 配置路由
  • org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping:这个是 Spring Cloud Gateway 的 HandlerMapping,会读取 Spring Cloud Gateway 配置并生成路由。这个是我们这里要详细分析的。

其实这些 handlerMappings,我们这里肯定走的是 RoutePredicateHandlerMapping 的相关逻辑,所以我们的 Mono 又可以简化成:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.getHandler(exchange)
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //调用对应的 Handler
      .flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //处理结果
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

我们来看 RoutePredicateHandlerMapping,首先这些 handlerMapping 都是继承了抽象类 org.springframework.web.reactive.handler.AbstractHandlerMapping, 前面我们拼接的 Mono 里面的 getHandler 的实现其实就在这个抽象类中:

public Mono<Object> getHandler(ServerWebExchange exchange) {
  //调用抽象方法 getHandlerInternal 获取真正的 Handler
  return getHandlerInternal(exchange).map(handler -> {
    //这里针对 handler 做一些日志记录
    if (logger.isDebugEnabled()) {
      logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
    }
    // 跨域处理
    ServerHttpRequest request = exchange.getRequest();
    if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
      CorsConfiguration config = (this.corsConfigurationSource != null ?
          this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
      CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
      config = (config != null ? config.combine(handlerConfig) : handlerConfig);
      if (config != null) {
        config.validateAllowCredentials();
      }
      if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
        return NO_OP_HANDLER;
      }
    }
    return handler;
  });
}

可以看出,其实核心就是每个实现类的 getHandlerInternal(exchange)方法,所以在我们拼接的 Mono 中,我们会忽略抽象类中的针对 handler 之后的 map 处理。

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.getHandlerInternal(exchange)
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //调用对应的 Handler
      .flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //处理结果
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

接下来经过 RoutePredicateHandlerMappinggetHandlerInternal(exchange) 方法,我们的 Mono 变成了:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
        .flatMap((Function<Route, Mono<?>>) r -> {
          exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
          return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
        }).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
          Mono.empty() //返回 Mono.empty()
          .then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
          }
        })))
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //调用对应的 Handler
      .flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //处理结果
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

RoutePredicateHandlerMapping.this.lookupRoute(exchange) 根据请求寻找路由,这个我们就不详细展开了,其实就是根据你的 Spring Cloud Gateway 配置,找到合适的路由。接下来我们来看调用对应的 Handler,即 FilteringWebHandler。DispatcherHandler.this.invokeHandler(exchange, handler) 我们这里也不详细展开,我们知道其实就是调用 Handler 的 handle 方法,即 FilteringWebHandler 的 handle 方法,所以 我们的 Mono 变成了:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
        .flatMap((Function<Route, Mono<?>>) r -> {
          exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
          return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
        }).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
          Mono.empty() 
          .then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
          }
        })))
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .then(FilteringWebHandler.this.handle(exchange).then(Mono.empty())) //调用对应的 Handler
      .flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //处理结果
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

由于调用对应的 Handler,最后返回的是 Mono.empty(),所以后面的 flatMap 其实不会执行了。所以我们可以将最后的处理结果这一步去掉。所以我们的 Mono 就变成了:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
        .flatMap((Function<Route, Mono<?>>) r -> {
          exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
          return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
        }).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
          Mono.empty() 
          .then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
          }
        })))
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .then(FilteringWebHandler.this.handle(exchange).then(Mono.empty()))), //调用对应的 Handler
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

FilteringWebHandler.this.handle(exchange) 其实就是从 Attributes 中取出路由,从路由中取出对应的 GatewayFilters,与全局 GatewayFilters 放到同一个 List 中,并按照这些 GatewayFilter 的顺序排序(可以通过实现 org.springframework.core.Ordered 接口来制定顺序),然后生成 DefaultGatewayFilterChain 即 GatewayFilter 链路。对应的源码是:

public Mono<Void> handle(ServerWebExchange exchange) {
  //从 Attributes 中取出路由,从路由中取出对应的 GatewayFilters
  Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
  List<GatewayFilter> gatewayFilters = route.getFilters();
  //与全局 GatewayFilters 放到同一个 List 中
  List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
  combined.addAll(gatewayFilters);
  //按照这些 GatewayFilter 的顺序排序(可以通过实现 `org.springframework.core.Ordered` 接口来制定顺序)
  AnnotationAwareOrderComparator.sort(combined);
  if (logger.isDebugEnabled()) {
    logger.debug("Sorted gatewayFilterFactories: " + combined);
  }
  //生成调用链
  return new DefaultGatewayFilterChain(combined).filter(exchange);
}

这个 GatewayFilter 调用链和 WebFilter 调用链类似,参考 DefaultGatewayFilterChain 的源码:

public Mono<Void> filter(ServerWebExchange exchange) {
  return Mono.defer(() -> {
    //如果链路没有结束,则继续链路
    if (this.index < filters.size()) {
      GatewayFilter filter = filters.get(this.index);
      //这里将 index + 1,也就是调用链路中的下一个 GatewayFilter
      DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
      //每个 filter 中如果想要继续链路,则会调用 chain.filter(exchange),这也是我们开发 GatewayFilter 的时候的使用方式
      return filter.filter(exchange, chain);
    }
    else {
      //到达末尾,链路结束
      return Mono.empty(); // complete
    }
  });
}

所以,经过 DefaultGatewayFilterChain 后,我们的 Mono 就会变成:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
        .flatMap((Function<Route, Mono<?>>) r -> {
          exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
          return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
        }).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
          Mono.empty() 
          .then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
          }
        })))
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .then(new DefaultGatewayFilterChain(combined).filter(exchange).then(Mono.empty()))), //调用对应的 Handler
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

再继续展开 DefaultGatewayFilterChain 的链路调用,可以得到:

return Mono.defer(() ->
  new MonoWebFilterTrace(source, 
    RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
        .flatMap((Function<Route, Mono<?>>) r -> {
          exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
          return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
        }).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
          Mono.empty() 
          .then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
          }
        })))
      .switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
      .then(
        Mono.defer(() -> {
          //如果链路没有结束,则继续链路
          if (DefaultGatewayFilterChain.this.index < DefaultGatewayFilterChain.this.filters.size()) {
            GatewayFilter filter = DefaultGatewayFilterChain.this.filters.get(DefaultGatewayFilterChain.this.index);
            //这里将 index + 1,也就是调用链路中的下一个 GatewayFilter
            DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(DefaultGatewayFilterChain.this, DefaultGatewayFilterChain.this.index + 1);
            //每个 filter 中如果想要继续链路,则会调用 chain.filter(exchange),这也是我们开发 GatewayFilter 的时候的使用方式
            return filter.filter(exchange, chain);
          }
          else {
            return Mono.empty(); //链路完成
          }
        })
        .then(Mono.empty()))
      ), //调用对应的 Handler
  TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
    //MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
  });
);

这样,就形成了 Spring Cloud Gateway 针对路由请求的完整 Mono 调用链。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
打赏
0
0
0
0
33
分享
相关文章
Spring Cloud Alibaba与Spring Cloud区别和联系?
Spring Cloud Alibaba与Spring Cloud区别和联系?
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
160 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
AI 时代:从 Spring Cloud Alibaba 到 Spring AI Alibaba
本次分享由阿里云智能集团云原生微服务技术负责人李艳林主讲,主题为“AI时代:从Spring Cloud Alibaba到Spring AI Alibaba”。内容涵盖应用架构演进、AI agent框架发展趋势及Spring AI Alibaba的重磅发布。分享介绍了AI原生架构与传统架构的融合,强调了API优先、事件驱动和AI运维的重要性。同时,详细解析了Spring AI Alibaba的三层抽象设计,包括模型支持、工作流智能体编排及生产可用性构建能力,确保安全合规、高效部署与可观测性。最后,结合实际案例展示了如何利用私域数据优化AI应用,提升业务价值。
289 4
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
266 5
在SpringCloud2023中快速集成SpringCloudGateway网关
本文主要简单介绍SpringCloud2023实战中SpringCoudGateway的搭建。后续的文章将会介绍在微服务中使用熔断Sentinel、鉴权OAuth2、SSO等技术。
207 2
在SpringCloud2023中快速集成SpringCloudGateway网关
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
109 0
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
185 1
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
【Azure Spring Cloud】Azure Spring Cloud connect to SQL using MSI
【Azure Spring Cloud】Azure Spring Cloud connect to SQL using MSI
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等