Spring源码阅读之非默认标签解析成BeanDefinition定义流程

上一篇文章我们讲解了 Spring 中<bean>标签是怎么被解析成 BeanDefinition 的。而如果不是<bean><import><beans><alias>这四类的其他标签又是怎么被解析成BeanDefinition的呢?比如本篇要讲的 context 相关的标签是怎么被解析成BeanDefinition的。比如常用的<context:component-scan>标签,再比如<context:property-placeholder>标签是怎么被处理的。本文以<context:component-scan>标签为例讲解,我提供了测试例子。感兴趣的同学把<context:property-placeholder>标签的解析流程看看,基本流程差不多。接下来我们就来观摩一下这些标签被解析的全流程吧。

  1. 在文章开始前,还是老样子,先提供一个测试的例子。方便同学们看完讲解之后自己去 Debug 源码。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 首先,我们先在applicationContext.xml文件里面加上context标签 -->
    <!-- 同时我们在base-package指定的路径下创建两个类,分别是接口UserService,和其对应的实现类UserServiceImpl -->
    <!-- 这里给大家留个小思考,就是我们的文件头里面的内容,为啥会有这么一大堆东西?我在下篇自定义标签中来揭晓答案 -->
    <context:component-scan base-package="com.xzhao.service"/>
</beans>

  1. 提供好测试用例之后,我们就来看看其源码是怎么做的。直接来到上篇文章的开头,即DefaultBeanDefinitionDocumentReader.parseBeanDefinitions()方法。因为<context:component-scan>标签不是DefaultNameSpace的,所以会走delegate.parseCustomElement()方法。那我们就来看看对于这种element它是怎么被解析的。

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
      NodeList nl = root.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++) {
        Node node = nl.item(i);
        if (node instanceof Element) {
          Element ele = (Element) node;
          if (delegate.isDefaultNamespace(ele)) {
            parseDefaultElement(ele, delegate);
          }
          else {
            // '<context:component-scan>'会走这里。
            // 当然 '<context:property-placeholder>'标签也是走这里。
            delegate.parseCustomElement(ele);
          }
        }
      }
    }
    else {
      delegate.parseCustomElement(root);
    }
}

  1. 进去之后,解析element的实现逻辑如下。

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // Step1:根据element获取对应的namespaceuri,
    // 根据这个我们才能获得其相应的handle,即具体的解析处理器
    // 其实在这里返回的值长这个样子:http://www.springframework.org/schema/context
    // 相信如果仔细的同学应该不会这玩意儿陌生
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
      return null;
    }
    // Step2:这里会首先拿到一个命名空间解析器,即NamespaceResolver对象
    // 然后利用NamespaceResolve对象去解析NamespaceUri,从而就得到了一个非常重要的对象
    // 命名空间处理器 NamespaceHandler 对象,其实正是这个对象去把element对象解析成BeanDefinition定义的。
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
      error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
      return null;
    }
    // Step3:利用的得到的NamespaceHandler去调用其parse方法,
    // 即可获得 '<context:component-scan>'标签对应的BeanDefinition对象了
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

  1. 接下来大家应该会想,那么多不同的标签,我们应该找那个解析器来解析这个标签呢?所以问题来了,怎么获得解析器?在获得解析器之前首先要先拿到一个叫命名空间解析器(namespaceHandlerResolve),这个概念好像不太好理解,我截了个图,大概就长这个样子。如下:

有了命名空间解析器之后,我们来看看是怎么获得命名空间处理器的。

public NamespaceHandler resolve(String namespaceUri) {
    // Step1:获取所有的handlerMappings,即上图红框里面的内容
    // 其中key为一个http链接,比如:http://www.springframework.org/context
    // 这个key也就是namespaceUri了,而对应的value其实是一个handler,仔细看看其实就是一个class的全路径名字
    Map<String, Object> handlerMappings = getHandlerMappings();
    // Step2:获取我们指定的handler名字,如果取到不到则直接返回null
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
      return null;
    }
    // Step3:由于'<context:component-scan>'标签对应的namespaceUri是 http://www.springframework.org/context
    // 所以获取到的一定是className,即是一个字符串对象。
    // 如果在一次解析的时候可能已经存在的是其对应的实例对象了,因为每解析完一个标签,则会将当前key的value替换成对象
    // 所以就会在此处直接返回
    else if (handlerOrClassName instanceof NamespaceHandler) {
      return (NamespaceHandler) handlerOrClassName;
    }
    else {
      // Step4:所以将className直接强转为String
      String className = (String) handlerOrClassName;
      try {
        // Step4.1:利用反射机制获取到className对应的Class对象
        Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
        // Step4.2:检查handlerClass是否是实现了NameSpaceHandler接口,从接口的继承树可以看出
        // 所以的handler都是直接或间接的实现了NamespaceHandler接口,从而实现了 init() 、parse() 、decorate() 几个非常重要的方法
        if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
          throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
              "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
        }
        // Step4.3:根据Class实例化出对应的namespaceHandler对象,即获取到了我们想要的命名空间解析器了
        NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
        // Step4.4:执行初始化操作,这一步非常非常重要,
        // 点到 namespaceHandler 对应的Handler类里面,我们会发现一些非常眼熟的东西
        // 比如此处对应的handler类是ContextNamespaceHandler类。
        // 一个小常识,一般标签以什么开头,其对应的handler就是标签开头命名的
        namespaceHandler.init();
        // Step4.5:将初始化完成的handler对象放到handlerMapppings里面
        // 这样做是为了当在有context标签需要解析的时候,则直接从该map里面获取即可,而不需要在重复创建了,相当于充当了一个本地缓存的角色,加速解析工作
        handlerMappings.put(namespaceUri, namespaceHandler);
        // Step4.6:将创建好的namespaceHandler对象返回
        return namespaceHandler;
      }
      catch (ClassNotFoundException ex) {
        throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
            "] for namespace [" + namespaceUri + "]", ex);
      }
      catch (LinkageError err) {
        throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
            className + "] for namespace [" + namespaceUri + "]", err);
      }
    }
}

// 这里简单看下 ContextNamespaceHandler类的init方法干了啥
// 其实非常简单,就是将标签以 <context:xxx> 开头的各种标签进行一个注册操作
// 这里面大家应该比较熟悉的标签 'property-placeholder','component-scan'
public class ContextNamespaceHandler extends NamespaceHandlerSupport {

  @Override
  public void init() {
    registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
    registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
    registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
    registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
    registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
    registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
    registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
  }

}

// 注册操作也比较简单,就是将标签对应的真正解析器放到其父类(NamespaceHandlerSupport类)的parse map中,方便后面开始具体的解析工作
// 从代码也可以看出,'<context:component-scan>'标签对应的真正解析器其实是 ComponentScanBeanDefinitionParser
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
}

  1. 到此,我们拿到了命名空间解析器了,接下来看看具体的解析工作是怎么做的。

// 上面我们讲到,将相关的解析器全部注册到了NamespaceHandlerSupport类的parse map里面
// 所以解析工作也肯定是先调用NamespaceHandlerSupport的parse方法获取到对应的解析器
// 然后在调用具体解析器的parse方法执行解析工作获得BeanDefinition定义
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // Step1:获取解析器对象,具体怎么找的,就不展开了,很简单,
    // 想想就是无非先拿到标签的名字,然后从parse map里面get得到对应的解析器
    // 我们重点关注下具体的解析工作,因为所有实现都是在具体的解析类的方法里面实现的
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    // Step2:根据解析器对象调用其对应的parse方法,执行具体的解析工作,
    // 从而得到BeanDefinition定义对象
    return (parser != null ? parser.parse(element, parserContext) : null);
}

  1. 我们来看看<context:component-scan>标签对应的解析器,即ComponentScanBeanDefinitionParse类的parse方法的实现。这里有个小技巧,一般标签名叫什么,相应的 parse 类的名字也是与其对应的,大家阅读别的标签的源码的时候可以注意下,方便找到对应的解析类。

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // Step1:获取 'base-package' 属性对应的值
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    // Step2:将获取到的basePackage做了一些字符串的处理转换工作,比如常见的那种占位符
    // 说白了就是字符串的一些替换操作,得到一个spring认为的标准的字符串对象
    // 感兴趣的朋友,自行了解相应的内容,这里不做深入探讨
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    // Step3:字符串切割,这里我做了个测试,按照规则写了一个字符串,可以切割出多个子串
    // 我猜想他的意思应该是base-package可以同时指定多个包路径,
    // 之前没有做过同时指定多个包路径的操作,大佬们见笑了
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
        ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

    // Actually scan for bean definitions and register them.
    // Step4:注释写的也比较明朗,就是首先获得一个scanner,即扫描器,
    // 然后拿着扫描器去挨个扫描指定包下的类,从而得到多个BeanDefinition对象
    ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
    // Step5:得到了多个BeanDefinition定义,又来到了比较熟悉的步骤,注册这些BeanDefinition定义到BeanFactory里面
    // 到此,我们关于 '<context:component-scan>'标签解析成对应的BeanDefinition对象也就讲完了
    registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

    return null;
}

  1. 接着来看看扫描器是怎么被创建出来的,其实这个里面就是对配置文件的相关属性进行解析赋值操作。

protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
    // Step1:判断当前scanner是否使用默认的过滤器
    boolean useDefaultFilters = true;
    if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
      useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
    }

    // Delegate bean definition registration to scanner class.
    // Step2:将BeanDefinition的构建委托给扫描器去创建
    // 创建一个scanner类,使用的是 ClassPathBeanDefinitionScanner 作为扫描器
    ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
    // Step3:设置BeanDefinition的一些默认属性
    scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults());
    scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns());
    // Step4:配置w恩建如果指定了 'resource-pattern',则设置
    if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
      scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
    }

    try {
      // Step5:解析 'name-generator' 属性
      parseBeanNameGenerator(element, scanner);
    }
    catch (Exception ex) {
      parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }

    try {
      // Step6:解析 'scope-resolver' 属性
      parseScope(element, scanner);
    }
    catch (Exception ex) {
      parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }
    // Step7:解析 'include-filter' 属性
    parseTypeFilters(element, scanner, parserContext);
    // Step8:返回初始化好的scanner对象,其实这个方法里面是对标签相关属性的解析和设置操作
    return scanner;
}

  1. 得到ClassPathBeanDefinitionScanner扫描器之后,开始使用扫描器去执行真正的扫描工作了,我们来看下具体实现。

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // Step1:用于存放扫描到的类对应的BeanDefinition定义
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    // Step2:遍历所有指定的扫描包
    for (String basePackage : basePackages) {
      // Step2.1:查询当前扫描包下的所有候选BeanDefinition定义
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      // Step2.2:遍历每个候选的BeanDefinition
      for (BeanDefinition candidate : candidates) {
        // Step2.2.1:获取scope的元数据信息,我们通常用的有 'signleton' 和 'property'。
        // 而 'signleton'也是默认值。
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
        candidate.setScope(scopeMetadata.getScopeName());
        // Step2.2.2:获取候选的BeanDefinition对应的beanName
        String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
        // Step2.2.3:如果当前的候选BeanDefinition实现了AbstractBeanDefinition,
        // 则执行BeanDefinition的后置处理操作
        if (candidate instanceof AbstractBeanDefinition) {
          postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
        }
        // Step2.2.4:如果候选的BeanDefinition实现了AnnotatedBeanDefinitio,
        // 则执行注解的通用逻辑,通常对BeanDefinition设置一些常用参数,
        // 比如是否是懒加载,是否有 depends-on(依赖),role(角色),description(描述)这四个属性
        if (candidate instanceof AnnotatedBeanDefinition) {
          AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
        }
        // Step2.2.5:检查候选的BeanDefinition
        if (checkCandidate(beanName, candidate)) {
          // Step2.2.5.1:将BeanDefinition封装成BeanDefinitionHolder对象
          BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
          // Step2.2.5.2:获取代理的BeanDefinitionHolder对象
          definitionHolder =
              AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
          beanDefinitions.add(definitionHolder);
          // Step2.2.5.3:将最终的BeanDefinition注册到BeanFactory中,完成BeanDefinition定义的解析工作
          registerBeanDefinition(definitionHolder, this.registry);
        }
      }
    }
    return beanDefinitions;
}

  1. 上面这个方法里面几个比较重要的方法我们来重点看下具体实现。

  • indCandidateComponents: 获取指定包下所有的候选BeanDefinition对象

  • postProcessBeanDefinition: BeanDefinition对象的后置操作

  • checkCandidate: 检查候选的BeanDefinition是否可以被注册到BeanFactory

// 首先来看第一个方法的,如何获取指定包路径下的候选BeanDefinition定义
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    // Step1:componentsIndex不为null  && 索引如果有includeFilters,
    // 则根据type去获取BeanDefinition定义,没有遇到过这种使用方式。
    // 所以这个地方请求大神赐教
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
      return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
      // Step2:我们通常的使用方式是这种
      return scanCandidateComponents(basePackage);
    }
}


private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
      // Step1:获取要扫描的包下的那些类,比如根据我们的配置可以得到:
      // classpath*:com/xzhao/service/**/*.class
      // 指定的service下的任何子包和其下面的class
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
          resolveBasePackage(basePackage) + '/' + this.resourcePattern;
      // Step2:根据指定的classpath,获取对应下面的所有resource
      Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
      boolean traceEnabled = logger.isTraceEnabled();
      boolean debugEnabled = logger.isDebugEnabled();
      // Step3:遍历resource,根据resource创建BeanDefinition定义,即候选的BeanDefinition
      for (Resource resource : resources) {
        if (traceEnabled) {
          logger.trace("Scanning " + resource);
        }
        if (resource.isReadable()) {
          try {
            // Step3.1:获取resource对应的元数据读取器
            // matadataReader包含了三部分内容,分别是resource,简单理解就是class的位置
            // 第二部分是class的元数据,主要包含了classLoader,className(com.xzhao.service.UserService)等等信息
            // 第三部分是注解的元数据,和class的元数据差不多
            MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
            // step3.2:检查当前元数据是否可以创建BeanDefinition定义,
            // 主要是过滤掉不包含(即exclude)的class,返回false
            // 保留include的class,返回true
            if (isCandidateComponent(metadataReader)) {
              // Step3.2.1:创建BeanDefinition定义,ScannedGenericBeanDefinition 间接实现了BeanDefinition接口,
              // 这里创建是一个扫描的BeanDefinition定义
              ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
              sbd.setResource(resource);
              sbd.setSource(resource);
              // Step3.2.2:检测候选bean是否合法,
              // 即候选bean(是否是合法的 && 一个具体的bean(感觉可以历程就是是否可以实例化出bean实例)) || (是一个抽象的 && 同时存在注解方法))
              if (isCandidateComponent(sbd)) {
                if (debugEnabled) {
                  logger.debug("Identified candidate component class: " + resource);
                }
                // Step3.2.2.1:检测合法,就作为一个候选的BeanDefinition定义。
                candidates.add(sbd);
              }
              else {
                if (debugEnabled) {
                  logger.debug("Ignored because not a concrete top-level class: " + resource);
                }
              }
            }
            else {
              if (traceEnabled) {
                logger.trace("Ignored because not matching any filter: " + resource);
              }
            }
          }
          catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                "Failed to read candidate component class: " + resource, ex);
          }
        }
        else {
          if (traceEnabled) {
            logger.trace("Ignored because not readable: " + resource);
          }
        }
      }
    }
    catch (IOException ex) {
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
}
// 在接着看第二方法,这个方法就比较简单了,就这么简单,大家可以稍微松口气
// 它的作用就是给设置一些通用的属性
protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
    beanDefinition.applyDefaults(this.beanDefinitionDefaults);
if (this.autowireCandidatePatterns != null) {
      beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName));
    }
}
// 在看最后一个方法,该方法用于最后一步检测当前的BeanDefinition是否合法可以被注册到BeanFactory里面
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
    // Step1:当前BeanDefinition是否已经在BeanFactory里面注册过,如果没有则可以正常注册
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
    }
    // Step2:如果已经在BeanFactory里面注册了,
    // 则获取已经注册了的BeanDefinition定义,然后尝试获取存在的BeanDefinition的父BeanDefinition
    BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
    BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) {
      existingDef = originatingDef;
    }
    // Step3:拿已经存在的BeanDefinition和将要注册的BeanDefinition比较
    // 已存在的实现了ScannedGenericBeanDefinition则可以覆盖
    // 或者已存在的BeanDefinition的source != 新的BeanDefinition的source,则也可以创建
    // 或者已存在的BeanDefinition != 新的BeanDefinition,则也可以创建
    // 以上三种情况下,可以将新创建的BeanDefinition注册到BeanFactory里面
    if (isCompatible(beanDefinition, existingDef)) {
return false;
    }
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}

总结

至此,我们就完成了<context:component-scan>标签扫描类得到BeanDefinitions定义,并将得到的BeanDefinitions依次注册到BeanFactory里面。我们只需要在被注册的类上设置@Component @Service @Controller @Repository这种注解就可以被<component-scan>标签扫描到。

总结一下就是先拿到指定的包路径,然后读取该路径下的所有被声明了注解的类,然后拿到这些类的元数据,再然后根据这些元数据创建出ScannedGenericBeanDefinition对象,最后将符合条件的ScannedGenericBeanDefinition注册到BeanFactory里面。就完成了改标签的工作。最后,本篇文章篇幅较长,需要慢慢理解其中的逻辑,不明白的地方就多看几遍在理解。到此我们知道了 spring 提供的自定义的标签是如何解析成BeanDefinition定义的。

下一篇我们来介绍如何自己定义一个自定义的标签,希望大家和我一起坚持下去。


欢迎关注我,共同学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值