前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springmvc5.x-mvc实现原理及源码实现

springmvc5.x-mvc实现原理及源码实现

作者头像
逍遥壮士
发布2023-09-12 20:01:34
1260
发布2023-09-12 20:01:34
举报
文章被收录于专栏:技术趋势技术趋势

基础知识

请看原来写的文章:springmvc

源码学习

代码语言:javascript
复制
@RequestMapping("/{id}")
    public String showUserInfo(ModelMap modelMap, @PathVariable("id")Integer id){
        Student student = new Student();
        student.setId(id);
        student.setAge(100);
        student.setName("test");
        modelMap.addAttribute("name", student.getName());
        modelMap.addAttribute("age", student.getAge());
        modelMap.addAttribute("id", student.getId());
        return "result";
    }

springmvc的初始化

  1. DispatcherServlet 初始化:DispatcherServlet 是 Spring MVC 的前端控制器,在 web.xml 或 WebApplicationInitializer 中配置 Servlet 容器时会初始化 DispatcherServlet。DispatcherServlet 的初始化源码位置为 org.springframework.web.servlet.DispatcherServlet。
代码语言:javascript
复制
<servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

代码位置:org.springframework.web.servlet.DispatcherServlet#initStrategies

代码语言:javascript
复制
//用于初始化 Spring MVC 的各个策略组件。
protected void initStrategies(ApplicationContext context) {
      //初始化处理文件上传的解析器,用于解析请求中的 multipart 数据。
        this.initMultipartResolver(context);
      //初始化处理国际化的解析器,用于解析请求中的语言区域信息。
        this.initLocaleResolver(context);
      //初始化处理主题的解析器,用于解析请求中的主题信息。
        this.initThemeResolver(context);
      //初始化处理器映射器,用于将请求映射到对应的处理器(Controller)。
        this.initHandlerMappings(context);
      //初始化处理器适配器,用于调用处理器的方法并从中获取 ModelAndView 对象。
        this.initHandlerAdapters(context);
      //初始化处理器异常解析器,用于处理请求过程中发生的异常。
        this.initHandlerExceptionResolvers(context);
      //初始化请求到视图名称的转换器,用于将处理器返回的逻辑视图名称转换为实际的视图路径。
        this.initRequestToViewNameTranslator(context);
      //初始化视图解析器,用于将视图名称解析为具体的视图类型。
        this.initViewResolvers(context);
      //初始化 FlashMap 管理器,用于处理请求间的数据传递。
        this.initFlashMapManager(context);
    }
  1. 注册 ServletContextListener:在 Servlet 容器启动时,通常会注册一个监听器(ServletContextListener)来初始化 Spring MVC 的上下文。在监听器的 contextInitialized 方法中实现 Spring MVC 的初始化,例如加载配置文件、创建 ApplicationContext 等。

代码位置:org.springframework.context.event.SimpleApplicationEventMulticaster#doInvokeListener

代码语言:javascript
复制
//执行应用程序事件监听器
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
            //调用监听器的 并传递当前发生的应用程序事件作为参数。
            listener.onApplicationEvent(event);
        } catch (ClassCastException var6) {
            String msg = var6.getMessage();
            if (msg != null && !this.matchesClassCastMessage(msg, event.getClass().getName())) {
                throw var6;
            }

            Log logger = LogFactory.getLog(this.getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Non-matching event type for listener: " + listener, var6);
            }
        }

    }
  1. WebApplicationContext 初始化:Spring MVC 使用了自己的容器(WebApplicationContext),该容器会在 ServletContextListener 初始化时创建并配置。关于 WebApplicationContext 的初始化可以参考 org.springframework.web.context.support.XmlWebApplicationContext 或 org.springframework.web.context.support.AnnotationConfigWebApplicationContext 源码。
  1. HandlerMapping 和 HandlerAdapter 初始化:HandlerMapping 负责将请求映射到对应的处理器(Controller),而 HandlerAdapter 则负责调用相应的处理器方法。这些组件的初始化涉及配置文件、注解扫描等过程,相关源码位置为 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping、org.springframework.web.servlet.handler.SimpleUrlHandlerMapping,以及 org.springframework.web.servlet.handler.BeanNameUrlHandlerAdapter、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 等。

注意监听器:org.springframework.context.event.SimpleApplicationEventMulticaster#doInvokeListener

  1. 视图解析器初始化:视图解析器(ViewResolver)负责将处理器方法的返回值解析为具体的视图。Spring MVC 支持多种类型的视图解析器,如 InternalResourceViewResolver、FreeMarkerViewResolver 等。初始化过程中会配置视图解析器的相关属性和位置,例如视图前缀、后缀等。相关源码位置为 org.springframework.web.servlet.view.InternalResourceViewResolver。

由于我们配的视图是:

代码语言:javascript
复制
<!-- 配置视图解析器 -->
  <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
  </bean>

所以解析出来是:

代码位置:org.springframework.beans.factory.BeanFactoryUtils#beansOfTypeIncludingAncestors(org.springframework.beans.factory.ListableBeanFactory, java.lang.Class<T>, boolean, boolean)

最终我们都会调onRefresh()完成初始化。代码位置:org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext

代码语言:javascript
复制
if (!this.refreshEventReceived) {
      // Either the context is not a ConfigurableApplicationContext with refresh
      // support or the context injected at construction time had already been
      // refreshed -> trigger initial onRefresh manually here.
      onRefresh(wac);
    }

springmvc分发实现

那么spring加载完成后,就是调用的问题,这里注意,会根据不同的调用方式来进行分发,比如http tcp 等的分发方式都不太一样。那最终都会调到doDispatch.

代码位置:org.springframework.web.servlet.DispatcherServlet#doDispatch

代码语言:javascript
复制
//分发方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //初始化请求信息
    HttpServletRequest processedRequest = request;
        //用于存储根据请求对象匹配到的处理器对象
    HandlerExecutionChain mappedHandler = null;
        //标识是否已解析多部分请求
    boolean multipartRequestParsed = false;
      //获取当前WebAsyncManager对象(异步处理)
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
            //初始化视图模型
      ModelAndView mv = null;
      Exception dispatchException = null;

      try {
                //检查请求是否为多部分请求(文件或),通过检查请求头中的 "Content-Type" 是否以 "multipart/" 开头来判断。
        processedRequest = checkMultipart(request);
                //判断是否一致,如果是则为true
        multipartRequestParsed = (processedRequest != request);

        // 获取处理对象
        mappedHandler = getHandler(processedRequest);
                //为空就是没找着路劲 返回404
        if (mappedHandler == null) {
          noHandlerFound(processedRequest, response);
          return;
        }

        // 获取处理对
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

        // 获取请求方法
        String method = request.getMethod();
                //判断是否为get请求
        boolean isGet = "GET".equals(method);
                //如果是 或头为HEAD
        if (isGet || "HEAD".equals(method)) {
                    //获取最后时间
          long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
          if (logger.isDebugEnabled()) {
            logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
          }
                    //新请一个响应并检查,如果不通过直接中止
          if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
            return;
          }
        }
              //调用处理器的预处理方法 ,如果不通过中止
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
          return;
        }

        // 调用适配器
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
              //检查异步处理是否已经开始
        if (asyncManager.isConcurrentHandlingStarted()) {
          return;
        }
              //设置默认的视图名到ModelAndView中
        applyDefaultViewName(processedRequest, mv);
                //调用后置处理器方法
        mappedHandler.applyPostHandle(processedRequest, response, mv);
      }
      catch (Exception ex) {
        dispatchException = ex;
      }
      catch (Throwable err) {
        // As of 4.3, we're processing Errors thrown from handler methods as well,
        // making them available for @ExceptionHandler methods and other scenarios.
                //如果出象则创建一个错误的异常
        dispatchException = new NestedServletException("Handler dispatch failed", err);
      }
            //返回客户端
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
      triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
      triggerAfterCompletion(processedRequest, response, mappedHandler,
          new NestedServletException("Handler processing failed", err));
    }
    finally {
            // 方法检查异步处理是否已经开始
      if (asyncManager.isConcurrentHandlingStarted()) {
        // Instead of postHandle and afterCompletion
        if (mappedHandler != null) {
                    //用于在异步处理开始后执行相关的清理操作或其他逻辑处理
          mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
        }
      }
      else {
                //用于清理多部分请求中使用的资源
        // Clean up any resources used by a multipart request.
        if (multipartRequestParsed) {
          cleanupMultipart(processedRequest);
        }
      }
    }
  }

接着细节深入

processedRequest = checkMultipart(request);

代码位置:org.springframework.web.servlet.DispatcherServlet#checkMultipart

代码语言:javascript
复制
//将请求转换为分段请求,并使分段解析程序可用。如果未设置多部分解析程序,则只需使用现有请求。
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        //不为空 且 请求是否是multipart类型
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
            //将请求转换为 multipart类型的请求对象,
      if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
        logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
            "this typically results from an additional MultipartFilter in web.xml");
      }
                //检查当前请求是否已经发生过多部分请求解析失败的异常
      else if (hasMultipartException(request) ) {
        logger.debug("Multipart resolution failed for current request before - " +
            "skipping re-resolution for undisturbed error rendering");
      }
      else {
        try {
                    //转换为HttpServletRequest 并返回
          return this.multipartResolver.resolveMultipart(request);
        }
        catch (MultipartException ex) {
          if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
            logger.debug("Multipart resolution failed for error dispatch", ex);
            // Keep processing error dispatch with regular request handle below
          }
          else {
            throw ex;
          }
        }
      }
    }
    // If not returned before: return original request.
    return request;
  }

上面这个方法解析multipart类型的请求。

接着:mappedHandler = getHandler(processedRequest); 这个是用于确定当前请求的处理程

代码语言:javascript
复制
Nullable
  protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
      //不为空
    if (this.handlerMappings != null) {
            //循环调用每个 HandlerMapping 的 getHandler(request) 方法,传入当前的 HttpServletRequest 对象作为参数,来获取对应的处理器。
      for (HandlerMapping hm : this.handlerMappings) {
        if (logger.isTraceEnabled()) {
          logger.trace(
              "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
        }
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
          return handler;
        }
      }
    }
    return null;
  }

当面这个用于获取HandlerExecutionChain,其实就是请求处理器(Handler)的方法。

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

根据刚刚获取的处理器进行获取HandlerAdapter,其实就是决定用来调modelandview或其他视图,有很多种,比如:

  1. RequestMappingHandlerAdapter:适配处理器函数、带有注解的控制器等类型的处理器。
  2. SimpleControllerHandlerAdapter:适配基于实现 Controller 接口的控制器。
  3. HttpRequestHandlerAdapter:适配实现 HttpRequestHandler 接口的处理器,用于处理原始的 HttpServletRequest 与 HttpServletResponse。
  4. HandlerAdapter 的默认实现 DefaultHandlerAdapter:用于处理没有明确适配器的处理器类型,默认使用 ServletInvocableHandlerMethod 来执行处理器方法。
  5. 针对异步请求的适配器:例如 AsyncHandlerInterceptor 和 Callable 所对应的适配器。
代码语言:javascript
复制
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
            //循环判断是哪种类型匹配,匹配就返回
      for (HandlerAdapter ha : this.handlerAdapters) {
        if (logger.isTraceEnabled()) {
          logger.trace("Testing handler adapter [" + ha + "]");
        }
                //获取最终的HandlerAdapter
        if (ha.supports(handler)) {
          return ha;
        }
      }
    }
    throw new ServletException("No adapter for handler [" + handler +
        "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
  }

mappedHandler.applyPreHandle(processedRequest, response),这个方法用于记录拦截器的执行位置。

代码语言:javascript
复制
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HandlerInterceptor[] interceptors = getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
      for (int i = 0; i < interceptors.length; i++) {
        HandlerInterceptor interceptor = interceptors[i];
                //调用前置拦截器方法 如果返回值为 false,表示拦截器不允许继续执行后续的处理逻辑
        if (!interceptor.preHandle(request, response, this.handler)) {
                    //方法进行拦截器链的后置处理,并直接返回 false
          triggerAfterCompletion(request, response, null);
          return false;
        }
                //记录位置
        this.interceptorIndex = i;
      }
    }
    return true;
  }
代码语言:javascript
复制
//用于触发拦截器链的后置处理(afterCompletion
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
      throws Exception {

    HandlerInterceptor[] interceptors = getInterceptors();
      //不为空
    if (!ObjectUtils.isEmpty(interceptors)) {
            //循环执行拦截器的后置处理逻辑,通常用于资源清理、日志记录等操作。
      for (int i = this.interceptorIndex; i >= 0; i--) {
        HandlerInterceptor interceptor = interceptors[i];
        try {
          interceptor.afterCompletion(request, response, this.handler, ex);
        }
        catch (Throwable ex2) {
          logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
        }
      }
    }
  }

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

接受三个参数:processedRequest 是经过前置处理器链处理后的请求对象,response 是响应对象,mappedHandler.getHandler() 是映射到的请求处理器对象。

在执行 handle() 方法时,会根据请求处理器的类型调用相应的处理逻辑。不同的请求处理器可能是不同类型的对象,例如 Controller、HttpRequestHandler 或 HttpMessageConverter 等。

注意:一般HTTP 请求中可以包含多种类型的参数,常见的有以下几种类型:

  1. 查询参数(Query Parameters):位于 URL 中,以 ? 开头,键值对使用 key=value 的形式表示,多个参数之间使用 & 分隔。例如: http://hong.com/api?key1=value1&key2=value2。可以通过解析 URL 来获取查询参数。
  2. 路径参数(Path Parameters):位于 URL 路径中,用于表示特定资源的标识符或属性。路径参数通常用于 RESTful 风格的路由中,使用占位符来代表参数值。例如: http://hong.com/api/users/{userId},其中 {userId} 就是路径参数。
  3. 请求体参数(Request Body Parameters):位于请求体中,通常使用表单数据或 JSON 格式来传递。可以通过 HTTP 请求的 Content-Type 头部字段来确定参数的类型。常见的参数类型有:
    • 表单参数(Form Parameters):使用表单数据格式传递,即 key=value 的形式。
    • JSON 参数(JSON Parameters):使用 JSON 格式传递,请求体中的数据是一个合法的 JSON 对象。
    • 文件参数(File Parameters):用于上传文件,请求体中包含文件的二进制数据。

判断请求参数的方式取决于你使用的服务器端框架或编程语言。大多数框架提供了相应的工具或库来解析和获取请求参数。一般而言,可以通过从请求对象中获取相应的参数来获取请求参数。例如,在 Java 的 Spring 框架中,可以使用 @RequestParam 注解、HttpServletRequest 对象等来获取请求参数。

这个位置非常复杂。有兴趣可以深入。

那么有些同学会疑问,springmvc可以支持哪些参数?

  1. 查询参数(Query Parameters):将查询参数作为方法参数进行接收。可以使用 @RequestParam 注解将参数与请求中的查询参数绑定,还可以指定默认值、是否必需等属性。
  2. 路径参数(Path Parameters):通过在请求路径中使用占位符来接收参数。使用 @PathVariable 注解将路径参数与方法参数进行绑定。
  3. 请求体参数(Request Body Parameters):通常用于接收 POST 或 PUT 请求中的数据。可以使用 @RequestBody 注解将请求体中的数据绑定到方法参数上。支持的数据格式包括 JSON、XML 等。
  4. 头部信息(Request Header):可以使用 @RequestHeader 注解将特定的请求头信息与方法参数绑定。
  5. Cookie 参数(Cookie Parameters):使用 @CookieValue 注解将特定的 Cookie 值与方法参数进行绑定。
  6. 表单参数(Form Parameters):适用于接收表单提交的参数。可以使用 @RequestParam 注解或 @ModelAttribute 注解将表单字段与方法参数进行绑定。
  7. 文件上传(File Upload):接收文件上传请求时,可以使用 MultipartFile 类型的方法参数来接收上传的文件数据。

当然上面是我所看源码了解到的,目前有没有其它暂时没看到。可以HandlerMethodArgumentResolver

mappedHandler.applyPostHandle(processedRequest, response, mv);

上面这段是拦截器的一些实现,我们有些请求或不开放的接口权限等可以结合这个来进行拦截。

代码位置:org.springframework.web.servlet.HandlerExecutionChain#applyPostHandle

代码语言:javascript
复制
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
      throws Exception {

    HandlerInterceptor[] interceptors = getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
      for (int i = interceptors.length - 1; i >= 0; i--) {
        HandlerInterceptor interceptor = interceptors[i];
                //调用拦截器
        interceptor.postHandle(request, response, this.handler, mv);
      }
    }
  }

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

这个是最后的视图的实现了,但是视图有好多种 如下:

  1. JSP 视图(InternalResourceView):使用 JSP(JavaServer Pages)作为视图技术。通过 InternalResourceViewResolver 视图解析器,将逻辑视图名映射到 JSP 文件,并将模型数据传递给 JSP 进行渲染。
  2. Thymeleaf 视图(ThymeleafView):使用 Thymeleaf 模板引擎进行视图渲染。Thymeleaf 是一个现代化的 Java 模板引擎,可以与 HTML、XML、JavaScript 等文件进行集成。
  3. Freemarker 视图(FreeMarkerView):使用 FreeMarker 模板引擎进行视图渲染。FreeMarker 是一个模板引擎,通过模板文件和数据模型生成最终的输出。
  4. Velocity 视图(VelocityView):使用 Apache Velocity 模板引擎进行视图渲染。Velocity 是一个基于 Java 的模板引擎,可用于生成文本、HTML、XML 等格式的输出。
  5. JSON 视图(MappingJackson2JsonView):将模型数据以 JSON 格式返回给客户端。通过 Jackson 库将模型数据序列化为 JSON 字符串,并通过 HttpServletResponse 返回给客户端。
  6. XML 视图(MarshallingView):将模型数据以 XML 格式返回给客户端。通过 JAXB(Java Architecture for XML Binding)将模型数据转换为 XML,并通过 HttpServletResponse 返回给客户端。
  7. 等等:除了上述视图以外,Spring 还支持自定义视图解析器和自定义视图类型,可以根据业务需求使用其他视图技术来进行视图渲染。

代码位置:org.springframework.web.servlet.DispatcherServlet#processDispatchResult

代码语言:javascript
复制
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
      @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
      @Nullable Exception exception) throws Exception {

    boolean errorView = false;
      //不为空
    if (exception != null) {
            //匹配类型为ModelAndViewDefiningException
      if (exception instanceof ModelAndViewDefiningException) {
        logger.debug("ModelAndViewDefiningException encountered", exception);
                //转换成ModelAndViewDefiningException
        mv = ((ModelAndViewDefiningException) exception).getModelAndView();
      }
      else {
                //获取自定义异常
        Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                //转换
        mv = processHandlerException(request, response, handler, exception);
        errorView = (mv != null);
      }
    }

    // 不为空且 非被清除
    if (mv != null && !mv.wasCleared()) {
            //进行视图创建
      render(mv, request, response);
      if (errorView) {
        WebUtils.clearErrorRequestAttributes(request);
      }
    }
    else { //证明被解析过了
            //打印日志
      if (logger.isDebugEnabled()) {
        logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
            "': assuming HandlerAdapter completed request handling");
      }
    }
      //若请求是异步处理的(Concurrent handling started during a forward),则直接返回,不做后续处理。
    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      // Concurrent handling started during a forward
      return;
    }
      //不为空打印日志
    if (mappedHandler != null) {
      mappedHandler.triggerAfterCompletion(request, response, null);
    }
  }

后面就是一些异常和finally的处理,都是清空缓存的一些处理。不细看,有兴趣可以了解一下。

最后

springmvc非常重要,特别源码这块,涉及视图解析以及如何拦截等逻辑,这些核心特别是想在spring方面有所提升的同学,建议可以再细详深入debug一行一行把核心逻辑过一下,真的后面想走得深入或做架构方面及整合一些框架这些流程先后顺序必须懂,否则很容易吃一大亏。当然以上仅是本人看法。

参考文章:

https://www.yii666.com/blog/452442.html

https://blog.csdn.net/weixin_56644618/article/details/127594065

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-03 01:01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 技术趋势 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础知识
  • 源码学习
    • springmvc的初始化
      • springmvc分发实现
      • 最后
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档