Spring核心原理:基于注解的IOC初始化

  1. 基于注解(annotation)方式的配置

目前来说XML配置的方式相对成熟,便于统一管理。随着SpringBoot的兴起,基于注解开发可以大大简化XML配置,提高开发速度,虽然会造成后期维护的成本增加。但是我想没有人会很愿意的编写大量的XML文件。

  • 类注解:@Component,@Repository,@Controller,@Service

  • 类内部注解:@Autowiring,@Value,@Resource

  1. 分析源码

Spring当中AnnotationConfigApplicationContext于AnnotationConfigWebApplicationContext这两个类是专门用于处理注解方式配置的容器,直接依赖于将注解作为容器配置信息来源的IOC容器。两者几乎没有差别,只是在注解Bean定义的载入有所不同。

(1)AnnotationConfigApplicationContext

类定义了AnnontatedBeanDefinitionReader属性用来保存读取注解bean定义的读取器。

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
// 类定义了AnnontatedBeanDefinitionReader属性用来保存读取注解bean定义的读取器。
    private final AnnotatedBeanDefinitionReader reader;

// 保存一个扫描指定类路径中注解bean定义的扫描器,并将其设置到容器当中
    private final ClassPathBeanDefinitionScanner scanner;

/**
    该类存在四种类构造器
*/
    // 默认构造函数,初始化空容器,稍后调用register()进行注册bean
    public AnnotationConfigApplicationContext() {
        StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
        this.reader = new AnnotatedBeanDefinitionReader(this);
        createAnnotatedBeanDefReader.end();
        this.scanner = new ClassPathBeanDefinitionScanner(this);
    }


    public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) {
        super(beanFactory);
        this.reader = new AnnotatedBeanDefinitionReader(this);
        this.scanner = new ClassPathBeanDefinitionScanner(this);
    }

// 最常用的构造函数,通过将涉及的配置类传递给该构造函数,
// 实现将相应配置类中的bean自动注册到容器中
    public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
        this();
        register(componentClasses);
        refresh();
    }
// 该构造器函数会自动扫描给定的包以及子包下的所有类,并自动识别所有的SpringBean
// 将其注册到容器中
    public AnnotationConfigApplicationContext(String... basePackages) {
        this();
        scan(basePackages);
        refresh();
    }

//  重要的方法

    // 为容器的注解bean读取器和注解bean扫描器设置bean名称产生器
    public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
        this.reader.setBeanNameGenerator(beanNameGenerator);
        this.scanner.setBeanNameGenerator(beanNameGenerator);
        getBeanFactory().registerSingleton(
                AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
    }

    // 为容器的注解bean读取器和注解bean扫描器设置作用范围元信息解析器
    public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) {
        this.reader.setScopeMetadataResolver(scopeMetadataResolver);
        this.scanner.setScopeMetadataResolver(scopeMetadataResolver);
    }


    // 为容器注册一个要被处理的注解bean,新注册的bean必须手动调用容器的refresh()方法刷新容器来触发对新注册的bean处理。,
    @Override
    public void register(Class<?>... componentClasses) {
        Assert.notEmpty(componentClasses, "At least one component class must be specified");
        StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register")
                .tag("classes", () -> Arrays.toString(componentClasses));
        this.reader.register(componentClasses);
        registerComponentClass.end();
    }
    // 扫描指定路径及其子包下的注解类,为了使新添加的类被处理,必须手动调用refresh方法刷新容器
    @Override
    public void scan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan")
                .tag("packages", () -> Arrays.toString(basePackages));
        // 调用ClassPathDefinitionScanner.scan()方法来扫描给定的包及其子包下的所有类
        this.scanner.scan(basePackages);
        scanPackages.end();
    }

}

从该类源码看到,Spring对注解处理分为两种:

1,直接将Bean注册到容器中,可以初始化容器时注册也可以在容器创建后手动调用注册方法注册,然后手动调用刷新容器方法。

2,通过扫描指定包及其子包下的所有类处理,初始化注解容器时自动扫描路径。如果创建后给定动态路径添加注解bean则需要手动调用容器扫描方法,然后手动刷新容器,使得容器对所有注册注解bean进行处理。

(2)详细分析两种处理方式:1,直接将bean注册到容器的过程

通过注解bean定义读取器注册注解bean:

// AnnotatedBeanDefinitionReader类中的register()方法注册指定的注解bean。
// 读取注解Bean的源码如下:
    // Bean定义读取器向容器注册注解Bean定义类
    private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
            @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
            @Nullable BeanDefinitionCustomizer[] customizers) {
        // 根据指定的注解Bean定义类,创建Spriing容器中对注解Bean的封装数据结构。
        AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
        if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
            return;
        }
        abd.setInstanceSupplier(supplier);
        // 解析注解Bean定义的作用域,如@Scope("property") 则为Bean为原型类型
        // 若@Scope("sinletonn")则为bean的单态类型
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
        // 为注解Bean设置作用域
        abd.setScope(scopeMetadata.getScopeName());
        // 为注解Bean定义生成Bean的名称
        String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
        // 处理注解Bean的通用注解,--
        AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
        // 若向容器注册注解bean定义时,使用了额外的限定符注解则解析限定符注解
        // 主要配置autowiring自动依赖注入装配的限定条件,即@Qulifier注解
        // spring自动依赖注入默认按照类型注入,如果使用@Qulifier注解则按照名称装配
        if (qualifiers != null) {
            for (Class<? extends Annotation> qualifier : qualifiers) {
                // priomary注解设置了该bean为autowiring自动依赖注入装配时的首选。
                if (Primary.class == qualifier) {
                    abd.setPrimary(true);
                }
                // 如果装配@Lazy注解,则设置该Bean非延迟初始化,没有配置则预实例化
                else if (Lazy.class == qualifier) {
                    abd.setLazyInit(true);
                }
                /**
                 * 如果使用了除@Primar @Lazy以外的其他注解,则为该注解bean添加一个autowiring自动依赖注入
                 * 装配限定符,该bean在进autowiring自动依赖注入装配时根据名称限定符进行装配
                 */
                else {
                    abd.addQualifier(new AutowireCandidateQualifier(qualifier));
                }
            }
        }
        if (customizers != null) {
            for (BeanDefinitionCustomizer customizer : customizers) {
                customizer.customize(abd);
            }
        }
        // 创建一个Bean名称的bean定义对象,封装注解bean 定义类数据
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
        // 工具类方法会根据@Scope注解的值为bean定义应用相应的代理模式,主要在Spring面向切面中使用
        definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        // 向IOC容器注册注解Bean类定义对象
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
    }

从源码来看,注册注解bean定义类的基本步骤:

  • 使用注解元数据解析器解析注解bean中关于作用于的配置。

  • 使用工具类方法processCommonDefintionAnnotaions()方法处理注解bean定义类中通用的注解。

  • 使用AnnontaionConfigUilts的applyScopedProxyMode()方法创建作用域的代理对象。

  • 通过BeanDefintionReaderUtils向容器注册Bean。

分析这四步的具体实现过程

    // AnnotationScopeMetadataResolver.resolveScopeMetadate()
    // 用该方法获取对象中指定类型的注解value值,并且判断注册的bean时原生类型property
    // 还是singleton类型。
    @Override
    public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
        ScopeMetadata metadata = new ScopeMetadata();
        if (definition instanceof AnnotatedBeanDefinition annDef) {
            // 从注解bean定义类的属性中查找属性为Scope的值,即@Scope注解的值
            // annDef.getMetadata()方法将Bean中所有的注解和注解的值存放在一个Map集合
            AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
                    annDef.getMetadata(), this.scopeAnnotationType);
            // 获取@Scope注解的值设置到返回的对象中
            if (attributes != null) {
                metadata.setScopeName(attributes.getString("value"));
                // 获取@Scope注解中的proxyMode属性值,在创建代理对象时会用到
                ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
                // 如果@Scope的proxyMode属性为Default或者no
                if (proxyMode == ScopedProxyMode.DEFAULT) {

                    proxyMode = this.defaultProxyMode;
                }
                metadata.setScopedProxyMode(proxyMode);
            }
        }
        // 返回解析的作用域元信息对象
        return metadata;
    }

    // AnnotationConfigUilts.processCommonDefitionAnnotations()
    // 该方法主要对注解bean定义的一些通用注解进行解析。
static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
        AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
        // bean定义了Lazy注解则该bean预实例化属性值设置为Lazy注解的值
        if (lazy != null) {
            abd.setLazyInit(lazy.getBoolean("value"));
        }
        else if (abd.getMetadata() != metadata) {
            lazy = attributesFor(abd.getMetadata(), Lazy.class);
            if (lazy != null) {
                abd.setLazyInit(lazy.getBoolean("value"));
            }
        }
        // 如果Bean定义中有@Primary注解则该bean设置为autowiring自动依赖注入装配的首选对象
        if (metadata.isAnnotated(Primary.class.getName())) {
            abd.setPrimary(true);
        }
        // 如果bean定义中有@DependsOn注解则该bean设置所依赖的的bean名称
        // 容器将在实例化此实例前,首先实例化依赖的bean
        AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
        if (dependsOn != null) {
            abd.setDependsOn(dependsOn.getStringArray("value"));
        }
        AnnotationAttributes role = attributesFor(metadata, Role.class);
        if (role != null) {
            abd.setRole(role.getNumber("value").intValue());
        }
        AnnotationAttributes description = attributesFor(metadata, Description.class);
        if (description != null) {
            abd.setDescription(description.getString("value"));
        }
    }

//  AnnotationConfigUtils.applyScopedProxyMode()
// 根据定义bean中的配置作用域@Scope来为其使用相应的代理策列。
    static BeanDefinitionHolder applyScopedProxyMode(
            ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
        // 获取注解bean定义类中@Scope注解的porxyMode属性值
        ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
        // 如果配置的@Scope注解的proxyMode属性值为NO则不使用代理模式
        if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
            return definition;
        }
        // 获取配置@Scope注解的proxyMode属性值,如果为TARGET_CLASS
        // 则返回true如果为InterFaces则返回false
        boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
        // 为注册的bean创建显影的代理模式
        return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
    }

// BeanDefinitionReaderUtils.registerBeanDefinition()
// 校验BeanDefintion信息,将bean放入容器中一个管理BeanDefinition的HashMap当中
    /**
     * 将解析的BeanDefinitionHold注册到SPring ioc容器中
     */
    public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

        // 获取解析BeanDefinition的名称
        String beanName = definitionHolder.getBeanName();
        // 向SpringIOC容器注册BeanDefinition
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

        // 如果解析的BeanDefinition有别名向Springioc容器注册别名
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                registry.registerAlias(beanName, alias);
            }
        }
    }

2,形参为包路径的注册过程

当创建注解容器时,如果传入的参数时注解bean定义类的包,注解容器将会扫描给定的包及其子包,扫描到bean定义载入并进行注册。

// ClassPathBeanDefintionScanner扫描器扫描给定包及其子包
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
    // 创建一个类路径bean的扫描器
    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        this(registry, true);
    }
    // 为容器创建一个类路径bean定义扫描器,并指定是否使用默认的扫描过滤规则
    // 即Spring默认扫描配置@Commponent @Repository @Service @Controller
    // 注解bean,同时支持java EE 6的@ManagedBean和@Named注解。
    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
        this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
    }

    // 核心的方法
   protected Set<BeanDefinitionHolder> doScan(String... basePackages) {

        Assert.notEmpty(basePackages, "At least one base package must be specified");
//        创建一个集合,存放扫描到Bean定义的封装类
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        // 便利扫描所有给定的包
        for (String basePackage : basePackages) {
            // 父类的findCandidateComponentrs,扫描给定类路径,获取符合条件的bean定义
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            // 遍历扫描到的所有类
            for (BeanDefinition candidate : candidates) {
                // 获取Bean定义类中@Scope注解的值即获取Bean的作用域!
                ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
                //为bean设置注解配置的作用域
                candidate.setScope(scopeMetadata.getScopeName());
                // 为bean生成名称
                String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
                // 如果扫描的bean不是Spring的注解bean则为bean设置默认值
                // 设置bean的自动依赖注入装配属性
                if (candidate instanceof AbstractBeanDefinition abstractBeanDefinition) {
                    postProcessBeanDefinition(abstractBeanDefinition, beanName);
                }
                // 如果扫描到的bean是spring的注解bean,则处理其通用的注解
                if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
                    AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);
                }
                // 根据bean名称检查指定bean是否需要在容器中注册,或者是否在容器中存在冲突
                if (checkCandidate(beanName, candidate)) {
                    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    // 根据注解中配置的作用域,为bean应用相应的代理模式
                    definitionHolder =
                            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                    beanDefinitions.add(definitionHolder);
                    // 向容器注册扫描到的bean
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
    }

}

该doScan()方法调用父类的ClassPathSacnningCandidateComponentProvider来扫描获取给定包及其子包的类,主要使用FFinCandidateCommonents()方法具体实现扫描给定类路径包的功能。

// 扫描给定类路径的包
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
            return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
        }
        else {
            return scanCandidateComponents(basePackage);
        }
    }

private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
        // 创建存储 扫描到的类集合
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            Set<String> types = new HashSet<>();
            for (TypeFilter filter : this.includeFilters) {
                String stereotype = extractStereotype(filter);
                if (stereotype == null) {
                    throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
                }
                types.addAll(index.getCandidateTypes(basePackage, stereotype));
            }
            boolean traceEnabled = logger.isTraceEnabled();
            boolean debugEnabled = logger.isDebugEnabled();
            for (String type : types) {
                // 为指定资源获取资源读取器,元注解读取器通过汇编ASM读取资源的元信息
                MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type);
                // 如果扫描到的类符合容器配置的过滤规则
                if (isCandidateComponent(metadataReader)) {
                    // 通过汇编读取资源字节码文件的bean定义的元信息
                    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                    sbd.setSource(metadataReader.getResource());
                    //  扫描到类符合容器配置的过滤规则
                    if (isCandidateComponent(sbd)) {
                        if (debugEnabled) {
                            logger.debug("Using candidate component class from index: " + type);
                        }
                        candidates.add(sbd);
                    }
                    else {
                        if (debugEnabled) {
                            logger.debug("Ignored because not a concrete top-level class: " + type);
                        }
                    }
                }
                else {
                    if (traceEnabled) {
                        logger.trace("Ignored because matching an exclude filter: " + type);
                    }
                }
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }
        return candidates;
    }

    /**
     判断元信息读取器读取的类是否符合容器定义的注解过滤规则
     */
    protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        // 如果读取的类的注解在排除注解过滤规则中,返回false
        for (TypeFilter tf : this.excludeFilters) {
            if (tf.match(metadataReader, getMetadataReaderFactory())) {
                return false;
            }
        }
        // 如果读取的类注解不在排除注解过滤规则中,则返回true
        for (TypeFilter tf : this.includeFilters) {
            if (tf.match(metadataReader, getMetadataReaderFactory())) {
                return isConditionMatch(metadataReader);
            }
        }
        // 即不再排除规则也不在包含规则中返回false
        return false;
    }
(2)AnnotationConfigWebApplicationContext

该类是AnnotationCofnigApplicationContext的WEB版,对于注解bean的扫描基本相同只是对注解Bean定义的载入稍有不同。

public class AnnotationConfigWebApplicationContext extends AbstractRefreshableWebApplicationContext
        implements AnnotationConfigRegistry {
      // 核心方法
    @Override
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) {
        // 为容器设置注解bean定义读取器
        AnnotatedBeanDefinitionReader reader = getAnnotatedBeanDefinitionReader(beanFactory);
        // 为容器设置类路径bean定义扫描器
        ClassPathBeanDefinitionScanner scanner = getClassPathBeanDefinitionScanner(beanFactory);
        // 获取容器的bean名称生成器
        BeanNameGenerator beanNameGenerator = getBeanNameGenerator();
        // 为注解bean定义读取器和类路径扫描器设置bean名称生成器
        if (beanNameGenerator != null) {
            reader.setBeanNameGenerator(beanNameGenerator);
            scanner.setBeanNameGenerator(beanNameGenerator);
            beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
        }
        // 获取容器的作用域元信息解析器
        ScopeMetadataResolver scopeMetadataResolver = getScopeMetadataResolver();
        if (scopeMetadataResolver != null) {
            // 为注解bean定义读取器和类路径扫描器设置作用域元信息解析器。
            reader.setScopeMetadataResolver(scopeMetadataResolver);
            scanner.setScopeMetadataResolver(scopeMetadataResolver);
        }

        if (!this.componentClasses.isEmpty()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Registering component classes: [" +
                        StringUtils.collectionToCommaDelimitedString(this.componentClasses) + "]");
            }
            reader.register(ClassUtils.toClassArray(this.componentClasses));
        }

        if (!this.basePackages.isEmpty()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Scanning base packages: [" +
                        StringUtils.collectionToCommaDelimitedString(this.basePackages) + "]");
            }
            scanner.scan(StringUtils.toStringArray(this.basePackages));
        }
        // 获取容器定义的bean资源路径
        String[] configLocations = getConfigLocations();
        // 如果定位的bean资源路径不为空
        if (configLocations != null) {
            for (String configLocation : configLocations) {
                try {
                    // 使用当前容器的类加载器加载定位路径的字节码文件
                    Class<?> clazz = ClassUtils.forName(configLocation, getClassLoader());
                    if (logger.isTraceEnabled()) {
                        logger.trace("Registering [" + configLocation + "]");
                    }
                    reader.register(clazz);
                }
                catch (ClassNotFoundException ex) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Could not load class for config location [" + configLocation +
                                "] - trying package scan. " + ex);
                    }
                    // 如果加载定义路径失败则启用容器类路径扫描器扫描给定路径包及其子包中的类
                    int count = scanner.scan(configLocation);
                    if (count == 0 && logger.isDebugEnabled()) {
                        logger.debug("No component classes found for specified class/package [" + configLocation + "]");
                    }
                }
            }
        }
    }



}

IOC容器初始化小结:

1,初始化入口由容器实现中的refresh()方法调用来完成。

2,对Bean定义载入IOC容器使用方法时loadBeanDefinition(); 通过ResourceLoader来完成资源文件的定位,DefaultResourceLoader是默认实现,同时上下文本身就给出了ResourceLoader的实现,可以通过类路径,文件系统,url方式来定位资源。如果XmlFactory作为IOC容器,那么需要为他指定Bean定义的资源,而Bean的文件抽形成Resource的来被容器通过BeanDefintionReader来完成定义信息的解析和bean的信息注册。通常使用XmlBeanDefaintionReader来进行解析bean的XML定义文件,而XmlBeanDefationReader通常委托给BeanDefinitionParserDeleagete来完成。从而得到Bean定义的信息,而Spring通常是使用BeanDefinition来表示。容器会使用loadBeanDefinition(),registerBeanDefinition()相关方法为其加载注册等操作。容器解析得到BeanDefinition以后使用BeanDefinitionRegistry接口来进行注册。而注册就是将其注册在IOC容器其内部的HashMap。而这个HashMap是持有Bean的信息场所,下来的操作都是围绕HashMap进行操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值