2024年SpringMVC这篇文章吃透了,最少最少涨5000(1),2024年最新可能是全网最细的C C++-资源加载机制剖析

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

父容器可有可无,并不是必须的,为了更好的管理 bean,springmvc 建议我们用父子容器,controller 之外的 bean,比如 service,dao 等,建议放到父容器中,controller 层的和 springmvc 相关的一些 bean 放在 springmvc 容器中,咱们继续。

4.1、过程

AbstractDispatcherServletInitializer#onStartup方法中会调用父类的onStartup,即AbstractContextLoaderInitializer#onStartup,我们进到这个方法中,代码如下图,干了 2 个事情

  1. 图中编号 ①:创建父容器,只是实例化了,并未启动
  2. 图中编号 ②:创建了一个监听器 ContextLoaderListener,这是一个 ServletContextListener 类型的监听器,稍后会在这个监听器中启动父容器

6cd8e179cfead8df1cc9e86fb18933fb.png

下面来分别来细说下上面 2 段代码干的活。

4.2、①:负责创建父容器

AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext,只是创建了一个AnnotationConfigWebApplicationContext对象,并将父容器配置类 rootConfigClass 注册到容器中,并没有启动这个容器,若 rootConfigClass 为空,父容器不会被创建,所以父容器可有可无。

a4559bae2da16e6fd604f924643ff02d.png

4.2、②:创建 ContextLoaderListener 监听器

代码如下,创建的时候将父容器对象 rootAContext 传进去了。

ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
//getRootApplicationContextInitializers()返回置为ApplicationContextInitializer数组,是个函数式接口,在父容器初始化的过程中,会作为一个扩展点预留给开发者用
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);

ContextLoaderListener,这是一个 ServletContextListener 类型的监听器,所以在 web 容器启动和销毁的过程中会被调用,如下图,这个监听器干了 2 件事

  1. contextInitialized 方法:这个方法会在 web 容器启动时被调用,内部负责启动父容器
  2. 在 contextDestroyed 方法:这个方法会在 web 容器销毁时被调用,内部负责关闭父容器

004085310864ba72bb537358a82a8654.png

5、阶段 3&4:创建 springmvc 容器&注册 DispatcherServlet

在回到AbstractDispatcherServletInitializer#onStartup,看这个方法的第二行,如下图

b8b587130bc3c679757e68d88d73555f.png

registerDispatcherServlet源码如下

protected void registerDispatcherServlet(ServletContext servletContext) {
    //①:DispatcherServlet的servlet名称,默认为:dispatcher
    String servletName = getServletName();

    //②:创建springmvc容器
    WebApplicationContext servletAppContext = createServletApplicationContext();

    //③:创建DispatcherServlet,注意这里将springmvc容器对象做为参数传递给DispatcherServlet了
    FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
    //设置ApplicationContextInitializer列表,可以对springmvc容器在启动之前进行定制化
    dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

    //④:将 dispatcherServlet 注册到servlet上下文中
    ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
    registration.setLoadOnStartup(1);
    registration.addMapping(getServletMappings());
    registration.setAsyncSupported(isAsyncSupported());

    //⑤:注册Filter
    Filter[] filters = getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        for (Filter filter : filters) {
            registerServletFilter(servletContext, filter);
        }
    }
    //⑥:这个方法预留给咱们自己去实现,可以对dispatcherServlet做一些特殊的配置
    customizeRegistration(registration);
}

protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
    return new DispatcherServlet(servletAppContext);
}

6、阶段 5:启动父容器:ContextLoaderListener

6.1、过程

上面的onStartup方法执行完毕之后,会执行监听器ContextLoaderListener的初始化,会进入到他的contextInitialized方法中

3edcdc262245d9f0b9ea91ab6c5e5905.png

initWebApplicationContext源码如下,截取了主要的几行

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    //this.context就是父容器对象
    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
    //①:配置及启动父容器
    configureAndRefreshWebApplicationContext(cwac, servletContext);
    //将父容器丢到servletContext中进行共享,方便其他地方获取
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
}
6.2、代码 ①:配置父容器以及启动父容器
//①:配置及启动父容器
configureAndRefreshWebApplicationContext(cwac, servletContext);

configureAndRefreshWebApplicationContext方法关键代码如下

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    //①:定制上线文,这里主要是遍历ApplicationContextInitializer列表,调用每个ApplicationContextInitializer#initialize方法来对容器进行定制,相当于一个扩展点,可以有程序员自己控制
    customizeContext(sc, wac);
    //②:刷新容器,就相当于启动容器了,此时就会组装里面的bean了
    wac.refresh();
}

customizeContext方法,我们进去看一下,这里涉及到了一个新的类,所以有必要去看一下,混个脸熟,源码如下,这是给开发者留的一个扩展点,通过ApplicationContextInitializer这个来做扩展,这是一个函数式接口,下面代码会遍历ApplicationContextInitializer列表,然后调用其initialize方法,我们可以在这个方法中对 spring 上线文进行定制

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
  List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
    determineContextInitializerClasses(sc);

    for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
        Class<?> initializerContextClass =
            GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
        if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
            throw new ApplicationContextException(String.format(
                "Could not apply context initializer [%s] since its generic parameter [%s] " +
                "is not assignable from the type of application context used by this " +
                "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                wac.getClass().getName()));
        }
        this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
    }

    AnnotationAwareOrderComparator.sort(this.contextInitializers);
    for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
        initializer.initialize(wac);
    }
}
6.3、ApplicationContextInitializer 接口:容器启动前用来初始化容器

是个函数式接口,在容器启动之前用来对容器进行定制,作为一个扩展点预留给开发者用,父容器和 springmvc 容器都用到了。

@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

 /**
  * 初始化给定的spring容器
  * @param applicationContext the application to configure
  */
 void initialize(C applicationContext);

}

7、阶段 6:启动 springmvc 容器:DispatcherServlet#init()

到目前为止父容器已经启动完毕了,此时 DispatcherServlet 会被初始化,会进入到他的 init()方法中。

7.1、DispatcherServlet 类图

991cdc1240f1b20ed3754b09ec5d4982.png

7.2、HttpServletBean#init()

这个方法会调用initServletBean()这个方法,其他的先不看

ae38e03b19ca2a1a1fbb2ebca5c2cc83.png

7.3、FrameworkServlet#initServletBean

提取了关键的代码,就 2 行

@Override
protected final void initServletBean() throws ServletException {
    //初始化springmvc容器,就是启动springmvc容器
    this.webApplicationContext = initWebApplicationContext();
    //这个方法内部是空的,预留给子类去实现的,目前没啥用
    initFrameworkServlet();
}

下面咱们进到initWebApplicationContext方法中去。

7.4、FrameworkServlet#initWebApplicationContext

关键代码如下,干了 3 件事情:

  1. 从 servlet 上线文对象中找到父容器
  2. 为 springmvc 容器指定父容器
  3. 调用 configureAndRefreshWebApplicationContext 方法配置 springmvc 容器以及启动容器,这个是关键咯
protected WebApplicationContext initWebApplicationContext() {
    //①:从servlet上线文中获取父容器
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
 //②:this.webApplicationContext就是springmvc容器,此时这个对对象不为null,所以满足条件
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            //springmvc容器未启动
            if (!cwac.isActive()) {
                //springmvc容器未设置父容器,则给其设置父容器,此时rootContext可能为空,这个没什么关系
                if (cwac.getParent() == null) {
                    cwac.setParent(rootContext);
                }
                //③:配置springmvc容器以及启动springmvc容器
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    //这里省略了一部分代码,如果springmvc采用配置文件的方式会走这部分代码
    ......
 //返回容器
    return wac;
}
7.5、FrameworkServlet#configureAndRefreshWebApplicationContext

为了让大家看清楚,如下代码,这里只提取了关键代码,主要干了 3 件事情

  1. 代码 ①:向 springmvc 容器中添加了一个 ContextRefreshListener 监听器,这个监听器非常非常重要,springmvc 容器启动完毕之后会被调用,会出现在阶段 8 中
  2. 代码 ②:给开发者预留的一个扩展点,通过 ApplicationContextInitializer#initialize 方法对容器进行定制
  3. 代码 ③:启动容器
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
 //①:向springmvc容器中添加了一个监听器对象,这个监听器特别重要,稍后在
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
 //②:扩展点:循环遍历ApplicationContextInitializer列表,调用其initialize方法对容器进行定制
    applyInitializers(wac);
    //③:刷新容器,相当于启动容器
    wac.refresh();
}

8、阶段 7:springmvc 容器启动过程中处理@WebMVC

8.1、SpringMVC 配置类被处理

此时 springmvc 容器启动了,此时注意下MvcConfig这个类,由于其上有@Conguration 注解,所以会被当做一个配置类被处理,这个类有 2 个非常重要的特征。

  1. 标注了@EnableWebMvc 注解
  2. 实现了 WebMvcConfigurer 接口

d9d007d7cb888ef1d4edc56b7a05c930.png

下面来说说这 2 个特征的作用。

8.2、@EnableWebMvc:配置 springmvc 所需组件

看一下这个注解的源码,如下,重点在于它上面的@Import(DelegatingWebMvcConfiguration.class)注解,这个注解的功能不知道的,可以回头去看我的 spring 系列,从头看一遍。

e6caffc1d224aef39a768f4624a88167.png

8.3、进入 DelegatingWebMvcConfiguration 类

代码如下,先注意下面 3 个特征

  1. 代码编号 ①:标注有@Configuration 注解,说明是一个配置类
  2. 代码编号 ②:继承了 WebMvcConfigurationSupport 类,这个类中有很多@Bean 标注的方法,用来定义了 springmvc 需要的所有组件
  3. 代码编号 ③:注入了WebMvcConfigurer列表,注意下,我们的 WebConfig 类就实现了 WebMvcConfigurer 这个接口,内部提供了很多方法可以用来对 springmvc 的组件进行自定义配置

b703184841f78d5e3047edd783e155c5.png

先来看看 WebMvcConfigurationSupport 这个类。

8.4、WebMvcConfigurationSupport:配置 springmvc 所需所有组件

这个类中会定义 springmvc 需要的所有组件,比如:RequestMapping、HandlerAdapter、HandlerInterceptor、HttpMessageConverter、HandlerMethodArgumentResolver、HandlerMethodReturnValueHandler 等等,所以如果你感觉@WebMVC 注解满足不了你的需求时,你可以直接继承这个类进行扩展。

这个类的源码我就不贴了,截几个图给大家看看

d326312623c5736feea27dea3e7debfd.png

8.5、WebMvcConfigurer 接口

这个接口就是我们用来对 springmvc 容器中的组件进行定制的,WebMvcConfigurationSupport 中创建 springmvc 组件的时候,会自动调用 WebMvcConfigurer 中对应的一些方法,来对组件进行定制,比如可以在 WebMvcConfigurer 中添加拦截器、配置默认 servlet 处理器、静态资源处理器等等,这个接口的源码如下

public interface WebMvcConfigurer {

 /**
  * 配置PathMatchConfigurer
  */
 default void configurePathMatch(PathMatchConfigurer configurer) {
 }

 /**
  * 配置ContentNegotiationConfigurer
  */
 default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
 }

 /**
  * 异步处理配置
  */
 default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
 }

 /**
  * 配置默认servlet处理器
  */
 default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
 }

 /**
  * 配置Formatter
  */
 default void addFormatters(FormatterRegistry registry) {
 }

 /**
  * 添加拦截器
  */
 default void addInterceptors(InterceptorRegistry registry) {
 }

 /**
  * 静态资源配置
  */
 default void addResourceHandlers(ResourceHandlerRegistry registry) {
 }

 /**
  * 跨越的配置
  */
 default void addCorsMappings(CorsRegistry registry) {
 }

 /**
  * 配置ViewController
  */
 default void addViewControllers(ViewControllerRegistry registry) {
 }

 /**
  * 注册视图解析器(ViewResolverRegistry)
  */
 default void configureViewResolvers(ViewResolverRegistry registry) {
 }

 /**
  * 注册处理器方法参数解析器(HandlerMethodArgumentResolver)
  */
 default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 }

 /**
  * 注册处理器方法返回值处理器(HandlerMethodReturnValueHandler)
  */
 default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
 }

 /**
  * 注册http报文转换器(HttpMessageConverter)
  */
 default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
 }

 /**
  * 扩展报文转换器
  */
 default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
 }

 /**
  * 配置异常解析器(HandlerExceptionResolver)
  */
 default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
 }

 /**
  * 扩展异常解析器(HandlerExceptionResolver)
  */
 default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
 }

 /**
  * 获得验证器
  */
 @Nullable
 default Validator getValidator() {
  return null;
 }

 /**
  * 获得MessageCodesResolver
  */
 @Nullable
 default MessageCodesResolver getMessageCodesResolver() {
  return null;
 }

}

9、阶段 8:组装 DispatcherServlet 中各种 SpringMVC 需要的组件

9.1、触发 ContextRefreshListener 监听器

大家回头看一下 8.5 中,有这样一段代码,注册了一个监听器ContextRefreshListener

5ffafa65fb90ecb4f594a2f55b799f6d.png

再来看看这个监听器的源码,如下图,包含 2 点信息

  1. 会监听 ContextRefreshedEvent 事件
  2. 监听到事件之后将执行FrameworkServlet.this.onApplicationEvent(event);,稍后会具体说这个代码

d79bc133b784abaec505faaf49f29b06.png

如下代码,springmvc 容器启动完毕之后,会发布一个ContextRefreshedEvent事件,会触发上面这个监听器的执行

f101e2c4ce4e891b8628037c66071ca1.png

9.2、进入 FrameworkServlet.this.onApplicationEvent(event);
public void onApplicationEvent(ContextRefreshedEvent event) {
    //标记已收到刷新事件
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        onRefresh(event.getApplicationContext());
    }
}

上面的onRefresh(event.getApplicationContext());会进到DispatcherServlet#onRefresh方法中。

9.3、进入 DispatcherServlet#onRefresh
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

里面会调用initStrategies方法。

9.4、DispatcherServlet#initStrategies:初始化 DispatcherServlet 中的组件

代码如下,这里面会初始化 DispatcherServlet 中的各种组件,这里的所有方法初始化的过程基本上差不多,就是先从 springmvc 容器中找这个组件,如果找不到一般会有一个兜底的方案

protected void initStrategies(ApplicationContext context) {
    //初始化MultipartResolver
    initMultipartResolver(context);
    //初始化LocaleResolver
    initLocaleResolver(context);
    //初始化ThemeResolver
    initThemeResolver(context);
    //初始化HandlerMappings
    initHandlerMappings(context);
    //初始化HandlerAdapters
    initHandlerAdapters(context);
    //初始化HandlerExceptionResolvers
    initHandlerExceptionResolvers(context);
    //初始化RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    //初始化视图解析器ViewResolvers
    initViewResolvers(context);
    //初始化FlashMapManager
    initFlashMapManager(context);
}

下面我们以initHandlerMappings(context);为例来看一下是如何初始化这些组件的。

9.5、initHandlerMappings(context);

源码如下,就是先从容器中找,找不到走兜底的方案。

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    //是否需要查找所有的HandlerMapping,默认为true
    if (this.detectAllHandlerMappings) {
        //从容器中查找所有的HandlerMapping
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        //对HandlerMapping列表进行排序
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            //查找名称为handlerMapping的HandlerMapping
            HandlerMapping hm = context.getBean("handlerMapping", HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
        }
    }

    // 如果没有找到HandlerMapping,则走兜底的方案
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    }
}

下面我们来看一下兜底的代码如何走的,进入 getDefaultStrategies 方法

9.6、DispatcherServlet#getDefaultStrategies:兜底的方案查找组件

这个方法的源码就不贴出来了,这里只说一下兜底的处理过程,springmvc 有个配置文件:spring-webmvc-5.3.6.jar!\org\springframework\web\servlet\DispatcherServlet.properties,properties 格式的文件,key 为组件的完整类名,value 为多个实现类的列表,在这个配置文件中指定了每种类型的组件兜底的情况下对应的实现类,比如没有找到 RequestMapping 的情况下,如下图红框的部分,有 3 个兜底的实现类,然后 springmvc 会实例化这 3 个类作为 RequestMapping。

0724ebbfa0121342c7693db5b3bde02d.png

10、阶段 9:销毁容器

10.1、销毁 springmvc 容器:DispatcherServlet#destroy

DispatcherServlet 销毁的时候会关闭 springmvc 容器

public void destroy() {
    if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
        ((ConfigurableApplicationContext) this.webApplicationContext).close();
    }
}
10.2、销毁父容器:ContextLoaderListener#contextDestroyed

父容器是在监听器中启动的,所以销毁的也是监听器负责的

public void contextDestroyed(ServletContextEvent event) {
    closeWebApplicationContext(event.getServletContext());
    ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

springmvc 容器的生命周期到此就结束了,想掌握好这个过程,建议大家 debug 走几遍,就熟悉了,下面带大家 debug 一下代码。

11、带大家 debug 代码

11.1、拉取源码
https://gitee.com/javacode2018/springmvc-series
11.2、将下面这个模块发布到 tomcat

cc77bb136722a4c1b1081fc2f074481d.png

11.2、按照下面配置设置断点,启动,调试代码

依次在下面这些方法中设置断点,然后启动 tomcat,一步步调试,我相信你们肯定可以吃透的。

1、org.springframework.web.SpringServletContainerInitializer#onStartup:入口
2、org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup
3、org.springframework.web.context.AbstractContextLoaderInitializer#onStartup
4、org.springframework.web.context.AbstractContextLoaderInitializer#registerContextLoaderListener:创建父容器
5、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext
6、org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#registerDispatcherServlet
7、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext:创建springmvc容器 & 注册DispatcherServlet


![img](https://img-blog.csdnimg.cn/img_convert/241855d2b5e68e7c774ff622c44c43ce.png)
![img](https://img-blog.csdnimg.cn/img_convert/2304f537046f19ff628a7a8f80c9abdc.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

tializer#registerContextLoaderListener:创建父容器
5、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext
6、org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#registerDispatcherServlet
7、org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext:创建springmvc容器 & 注册DispatcherServlet


[外链图片转存中...(img-AVE2Ki2V-1715586287945)]
[外链图片转存中...(img-Q3GCpngv-1715586287945)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值