自学笔记记录:Spring Boot 2.5.0源码学习【1、静态资源配置原理】

【“没有什么是一个断点不能解决的”系列】

一、Spring Boot静态资源配置原理

  • Spring Boot 启动默认加载 XxxxxAutoConfiguration 类(自动配置类)

  • SpringMVC 功能的自动配置类 主要是 WebMvcAutoConfiguration,检查 WebMvcAutoConfiguration 配置类是否生效

    // WebMvcAutoConfiguration.class
    
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
    @AutoConfigureOrder(-2147483638)
    @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
    public class WebMvcAutoConfiguration {}
    

    @Configuration 说明这是一个配置类;

    @ConditionalOnWebApplication(type = Type.SERVLET) 判断是不是一个 Servlet 应用,我们的 Spring Boot 应用就是一个典型的 Servlet 应用,因此生效;

    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) 判断应用里面是否有 Servlet、DispatcherServlet、WebMvcConfigurer 这三个类型场景,Servlet类就是原生的Web开发类,DispatcherServlet、WebMvcConfigurer 都是导了 spring-webmvc-5.3.7.jar 包自然会有的类,因此生效;

    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) 判断容器中没有 WebMvcConfigurationSupport 类型的组件时就会生效

  • WebMvcAutoConfiguration 配置类给容器中配了什么?

    主要来看 WebMvcAutoConfigurationAdapter 这个内部配置类

    // WebMvcAutoConfiguration.class
    
    @Configuration(proxyBeanMethods = false)
    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
    

    注意留意@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})

    配置文件的相关属性和xxx进行了绑定,分别是 WebMvcProperties (点进去发现与 spring.mvc 进行了绑定)、 ResourceProperties(点进去发现与 spring.resources 进行了绑定)、WebProperties(点进去发现与 spring.web 进行了绑定),@EnableConfigurationProperties 注解帮我们按制定规则绑定好配置文件并且将它们导入容器中。

    1、额外注意配置类中只有一个有参构造器的情况

    另外注意,WebMvcAutoConfigurationAdapter 这个配置类中只有一个有参构造器,那么这个有参构造器中所有参数的值默认都会从容器中去找,

    • ResourceProperties resourceProperties:这个就是上面绑定了配置文件并且@EnableConfigurationProperties开启注入容器中的组件,所以这是获取和 spring.resources 绑定的所有的值的对象;
    • WebProperties webProperties:获取和 spring.web绑定的所有的值的对象;
    • WebMvcProperties mvcProperties:获取和 spring.mvc绑定的所有的值的对象;
    • ListableBeanFactory beanFactory:Spring 的 beanFactory,相当于找 Spring 容器;
    • ObjectProvider messageConvertersProvider:HttpMessageConverters,找到系统中所有的 HttpMessageConverters;
    • ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider:ResourceHandlerRegistrationCustomizer,相当于找到资源处理器的自定义器;
    • ObjectProvider dispatcherServletPath:DispatcherServletPath,相当于找 DispatchServlet 处理的路径;
    • ObjectProvider<ServletRegistrationBean<?>> servletRegistrations:ServletRegistrationBean,给应用注册原生的 Servlet、Filter 时用到…
    // WebMvcAutoConfiguration.class
    // public static class WebMvcAutoConfigurationAdapter 配置类中的构造器
    
    public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
        this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources());
        this.mvcProperties = mvcProperties;
        this.beanFactory = beanFactory;
        this.messageConvertersProvider = messageConvertersProvider;
        this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
        this.dispatcherServletPath = dispatcherServletPath;
        this.servletRegistrations = servletRegistrations;
        this.mvcProperties.checkConfiguration();
    }
    

    2、资源处理的默认规则

    然后再来主要关注 addResourceHandlers(ResourceHandlerRegistry registry) 方法,添加资源处理器

    // WebMvcAutoConfiguration.class
    // public static class WebMvcAutoConfigurationAdapter
    
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!this.resourceProperties.isAddMappings()) {
            logger.debug("Default resource handling disabled");
        } else {
            // webjars 的规则
            this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
            // 
            this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                if (this.servletContext != null) {
                    ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                    registration.addResourceLocations(new Resource[]{resource});
                }
    
            });
        }
    }
    
    1. 首先对 resourceProperties.isAddMappings() 进行判断,也就是配置文件绑定的 spring.resources.add-mappings 属性进行判断,如果该属性为 false 的话,那就进入报异常就不会进入 else 部分执行了,配置也就不再生效。因此我们可以在 application.properties 中对 spring.resources.add-mappings 属性设置为 false,相当于禁用掉所有静态资源的配置规则,就无法访问到静态资源。

    2. 在 spring.resources.add-mappings 属性值为 true 的默认情况下,进入 else 部分;

    3. 首先是 “/webjars/**” 请求路径的配置处理规则,写死了默认到 “/META-INF/resources/webjars/” 路径查找;

      // WebMvcAutoConfiguration.class
      // public static class WebMvcAutoConfigurationAdapter
      // public void addResourceHandlers(ResourceHandlerRegistry registry)
      
      this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
      

      访问 /webjars/ 下所有的请求,都会进入到 /META-INF/resources/webjars/ 路径下去查找。而且这个资源还能设置 spring.resources.cache.period 缓存时间,并且会缓存设置的相应时间,即访问过该路径一次之后,会在浏览器中缓存生效。缓存策略封装在 addResourceHandler 了,进入可以查看。

    4. 再是 静态资源请求路径 的配置规则,访问 spring.mvc.static-path-pattern 属性值的请求路径,默认是 “/**”,到 spring.web.resources.static-locations 属性值所在的路径下查找,默认是"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”;

      // WebMvcAutoConfiguration.class
      // public static class WebMvcAutoConfigurationAdapter
      // public void addResourceHandlers(ResourceHandlerRegistry registry)
      
      this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
            registration.addResourceLocations(new Resource[]{resource});
        }
      });
      

      this.mvcProperties.getStaticPathPattern() 获取到静态资源路径,可以发现 staticPathPattern 是在 WebMvcProperties 中的,而 WebMvcProperties 又是与 配置文件 spring.mvc 绑定的,相当于如果我们配了 spring.mvc.static-path-pattern,那么我们就会获取到对应的值,如果没有配,就是默认值 “/**”,任何访问 / 下的路径请求,就会去到this.resourceProperties.getStaticLocations() 路径下去查找,跟踪进入到 WebProperties 类中的 Resources 类,可以发现底层是配有默认静态路径的,分别是"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”,因此我们访问 / 路径请求静态资源,就会去到上面四个默认静态路径位置下查找。并且也有缓存策略,都封装在了 addResourceHandler 中了。

      (相较于Spring Boot 2.3.4版本不同的地方是,去掉了 spring.resources 的 Application Properties 配置属性,替换成了 spring.web.resources)

      // WebProperties.class
      
      public static class Resources {
        private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
        private String[] staticLocations;
        private boolean addMappings;
        private boolean customized;
        private final WebProperties.Resources.Chain chain;
        private final WebProperties.Resources.Cache cache;
      
        public Resources() {
            this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
            this.addMappings = true;
            this.customized = false;
            this.chain = new WebProperties.Resources.Chain();
            this.cache = new WebProperties.Resources.Cache();
        }
      
        public String[] getStaticLocations() {
            return this.staticLocations;
        }
      }
      
    5. 以及 欢迎页 的配置规则,

      HandlerMapping:是 Spring MVC 中的一个核心组件,翻译为 处理器映射,保存了每一个 Handler 能处理哪些请求。找到各自的哪个请求由各自对应的哪个处理器处理,找到之后再利用反射机制调用能够处理的那个方法。

      此处 WelcomePageHandlerMapping 表示处理欢迎页请求的映射规则

      // WebMvcAutoConfiguration.class
      // public static class EnableWebMvcConfiguration
      
      @Bean
      public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
        WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
        welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
        welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
        return welcomePageHandlerMapping;
      }
      

      同样这里 @Bean 给容器中注入 welcomePageHandlerMapping,这个方法中的传参也一定都会从容器中去获取。

      1. 此处 this.mvcProperties.getStaticPathPattern() 拿的仍然是配置文件中 静态路径配置属性(spring.mvc.static-path-pattern)的值或者是其默认值"/**";

      2. 进入到 WelcomePageHandlerMapping 构造器,具体检查其构造过程,

        WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
            if (welcomePage != null && "/**".equals(staticPathPattern)) {
                // 要使用欢迎页功能,必须是"/**"路径请求
                logger.info("Adding welcome page: " + welcomePage);
                this.setRootViewName("forward:index.html");
            } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                // 否则调用 Controller,看谁能处理这个 /index 请求
                logger.info("Adding welcome page template: index");
                this.setRootViewName("index");
            }
        }
        

        从第一个 if 判断中我们可以看到 欢迎页需要存在 并且 静态路径配置属性(spring.mvc.static-path-pattern) 的值必须是 “/**”,我们才能使用欢迎页功能。

      3. 额外注意

        spring:
        mvc:
         static-path-pattern: /res/**
        #修改默认静态资源请求访问路径,会导致 欢迎页功能、favivon浏览器页面标签小图标功能 失效!原因是与上面第5步欢迎页配置同理,底层写死了,只有 "/**" 请求路径才能判断成功,才能访问到我们项目里来,但是改了路径请求之后就相当于无法访问进项目里来了,因此会失效。
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
rar包内含有spring2.5.6源码,解压即可使用 源代码分析,是一件既痛苦又快乐的事情,看别人写的代码是通过的,但当你能够看明白的时候,相信快乐也会随之而来,为了减少痛苦,更快的带来快乐,在这里希望通过这篇文章对觉得困难的朋友有一个帮助。 本文以spring框架的XmlBeanFactory为入手点进行分析,希望能够以尽量简洁明了的方式给予有需要的朋友一定的帮助。 首先来打开该类的代码,我们将看到如下代码: Java代码 public class XmlBeanFactory extends DefaultListableBeanFactory { private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); } public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); } } public class XmlBeanFactory extends DefaultListableBeanFactory { private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); } public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); } } 这个类的代码很简单,一个成员对象加两个构造函数,从这里我们可以看出,最重要的地方在于最后一个构造函数: Java代码 super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); 第一句就是将父亲工厂交给父类的构造函数,实际上最后也就是把父工厂保存到类的parentBeanFactory成员对象中,这个对象是在AbstractBeanFactory抽象类中定义的,而这个父工厂也会一直传递到该抽象类进行保存。第二句就是整个类中最重要的地方了,顾名思义,它的目的是通过XmlBeanDefinitionReader这个XML的Reader从资源resource中(也就是你的配置文件)读取bean的定义。接下来我们打开XmlBeanDefinitionReader的loadBeanDefinitions方法,我们可看到在这个方法里代码就一行,调用了一个同名不同参的方法,而参数是EncodedResource的一个实例,这个类实际上是Resource的一个包装类,用来保存资源的Encode的,那接下来我们再看被调用的loadBeanDefinitions方法,这个方法里最主要的部分就是: Java代码 InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); 这里的目的是将资源包装成一个InputSource,连同Resource作为参数传递到doLoadBeanDefinitions方法 Java代码 DocumentBuilderFactory factory = createDocumentBuilderFactory(); if (logger.isDebugEnabled()) { logger.debug("Using JAXP implementation [" + factory + "]"); } DocumentBuilder builder = createDocumentBuilder(factory); Document doc = builder.parse(inputSource); return registerBeanDefinitions(doc, resource); DocumentBuilderFactory factory = createDocumentBuilderFactory(); if (logger.isDebugEnabled()) { logger.debug("Using JAXP implementation [" + factory + "]"); } DocumentBuilder builder = createDocumentBuilder(factory); Document doc = builder.parse(inputSource); return registerBeanDefinitions(doc, resource); 这个方法的目的一目了然,就是为了将资源解释成为Document对象,然后调用registerBeanDefinitions方法,这里不做详细解释,不了解的话请去看看关于JAXP的介绍。接下来我们打开registerBeanDefinitions方法: Java代码 public int registerBeanDefinitions(Document doc, Resource resource) throws BeansException { XmlBeanDefinitionParser parser = (XmlBeanDefinitionParser) BeanUtils.instantiateClass(this.parserClass); return parser.registerBeanDefinitions(this, doc, resource); } public int registerBeanDefinitions(Document doc, Resource resource) throws BeansException { XmlBeanDefinitionParser parser = (XmlBeanDefinitionParser) BeanUtils.instantiateClass(this.parserClass); return parser.registerBeanDefinitions(this, doc, resource); } 这里创建了一个XmlBeanDefinitionParser接口的实现,这个接口的具体类是DefaultXmlBeanDefinitionParser,这个接口很简单,只有registerBeanDefinitions一个方法,这个方法的作用也很明了,就是用来注册Bean的定义的,所以说类和方法的名字一定要起得有意义,这样可以让人一看就大概了解其作用,减少了很多阅读代码的痛苦。废话不多说,我们打开DefaultXmlBeanDefinitionParser的registerBeanDefinitions方法,这个类就是解释XML配置文件的核心类了,打开registerBeanDefinitions方法后我们看到如下代码: Java代码 public int registerBeanDefinitions(BeanDefinitionReader reader, Document doc, Resource resource) throws BeanDefinitionStoreException { this.beanDefinitionReader = reader; this.resource = resource; logger.debug("Loading bean definitions"); Element root = doc.getDocumentElement(); //初始化根元素 initDefaults(root); if (logger.isDebugEnabled()) { logger.debug("Default lazy init '" + getDefaultLazyInit() + "'"); logger.debug("Default autowire '" + getDefaultAutowire() + "'"); logger.debug("Default dependency check '" + getDefaultDependencyCheck() + "'"); } preProcessXml(root);//一个空方法用于扩展 int beanDefinitionCount = parseBeanDefinitions(root);//解释配置的主要方法 if (logger.isDebugEnabled()) { logger.debug("Found " + beanDefinitionCount + " elements in " + resource); } postProcessXml(root); //一个空方法用于扩展 return beanDefinitionCount; } public int registerBeanDefinitions(BeanDefinitionReader reader, Document doc, Resource resource) throws BeanDefinitionStoreException { this.beanDefinitionReader = reader; this.resource = resource; logger.debug("Loading bean definitions"); Element root = doc.getDocumentElement(); //初始化根元素 initDefaults(root); if (logger.isDebugEnabled()) { logger.debug("Default lazy init '" + getDefaultLazyInit() + "'"); logger.debug("Default autowire '" + getDefaultAutowire() + "'"); logger.debug("Default dependency check '" + getDefaultDependencyCheck() + "'"); } preProcessXml(root);//一个空方法用于扩展 int beanDefinitionCount = parseBeanDefinitions(root);//解释配置的主要方法 if (logger.isDebugEnabled()) { logger.debug("Found " + beanDefinitionCount + " elements in " + resource); } postProcessXml(root); //一个空方法用于扩展 return beanDefinitionCount; } 在这个方法当中,主要用于解释定义的有两个方法,一个是initDefaults,一个是parseBeanDefinitions,第一个方法是用来解释根元素的属性的,例如lazy-init, autowire等,而parseBeanDefinitions就是用来解释具体的bean定义了,方法代码如下: Java代码 protected int parseBeanDefinitions(Element root) throws BeanDefinitionStoreException { NodeList nl = root.getChildNodes(); int beanDefinitionCount = 0; for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { Element ele = (Element) node; if (IMPORT_ELEMENT.equals(node.getNodeName())) { importBeanDefinitionResource(ele); } else if (ALIAS_ELEMENT.equals(node.getNodeName())) { String name = ele.getAttribute(NAME_ATTRIBUTE); String alias = ele.getAttribute(ALIAS_ATTRIBUTE); this.beanDefinitionReader.getBeanFactory().registerAlias(name, alias); } else if (BEAN_ELEMENT.equals(node.getNodeName())) { beanDefinitionCount++; BeanDefinitionHolder bdHolder = parseBeanDefinitionElement(ele, false); BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.beanDefinitionReader.getBeanFactory()); } } } return beanDefinitionCount; } protected int parseBeanDefinitions(Element root) throws BeanDefinitionStoreException { NodeList nl = root.getChildNodes(); int beanDefinitionCount = 0; for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { Element ele = (Element) node; if (IMPORT_ELEMENT.equals(node.getNodeName())) { importBeanDefinitionResource(ele); } else if (ALIAS_ELEMENT.equals(node.getNodeName())) { String name = ele.getAttribute(NAME_ATTRIBUTE); String alias = ele.getAttribute(ALIAS_ATTRIBUTE); this.beanDefinitionReader.getBeanFactory().registerAlias(name, alias); } else if (BEAN_ELEMENT.equals(node.getNodeName())) { beanDefinitionCount++; BeanDefinitionHolder bdHolder = parseBeanDefinitionElement(ele, false); BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.beanDefinitionReader.getBeanFactory()); } } } return beanDefinitionCount; } 其他标签具体如何被解释这里就不多说,相信大家也能看得懂,这里主要讲一下解释bean的的处理,我们注意以下代码: Java代码 else if (BEAN_ELEMENT.equals(node.getNodeName())) { beanDefinitionCount++; BeanDefinitionHolder bdHolder = parseBeanDefinitionElement(ele, false); BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.beanDefinitionReader.getBeanFactory()); } else if (BEAN_ELEMENT.equals(node.getNodeName())) { beanDefinitionCount++; BeanDefinitionHolder bdHolder = parseBeanDefinitionElement(ele, false); BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.beanDefinitionReader.getBeanFactory()); } 这里是当碰到一个bean标签的时候所进行的处理,也既是对bean的定义进行解释,可以看到parseBeanDefinitionElement方法的第一个参数就是bean则个元素,第二个参数表示该bean是否为内置的bean,从这里进行解释的bean都不可能是内置的,所以这里直接以false为参数,打开parseBeanDefinitionElement方法,就可以看到这个方法里就是对bean的内部的解释,也很简单,也不多讲了,呵呵(下班时间已经到了,所以就写这么多了,基本的流程也就这样,没什么特别难的地方。),对了,最后还有一点就是解释完后,bean的定义将会被保存到beanFactory中,这个beanFactory的实现就是XmlBeanFactory了,该beanFactory是在new的时候被传递到reader中的,就是该类中以下这行代码: Java代码 private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); 好了,就这么多了,本文只作为参考,只讲解了如何加载bean定义这块,只作为一个参考,希望对其他朋友能有所帮助吧,因为时间匆忙,有错漏的地方请指正。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值