ConfigurationClassParser解析@Configuration类源码详解

功能说明

ConfigurationClassParser是Spring解析Java配置(@Configuration)的核心类,主要方法是org.springframework.context.annotation.ConfigurationClassParser#parse(java.util.Set<org.springframework.beans.factory.config.BeanDefinitionHolder>)。首先需要弄懂几个类:
第一个就是ConfigurationClass,顾名思义,这个类就是所有包含注解@Configuration的抽象类。比如:

@Configuration
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {

}

ConfigurationClassParser的最主要的目的就是解析一个类为ConfigurationClass,解析完成之后存放到configurationClasses属性中,如下图所示:
在这里插入图片描述
如果要弄懂这个解析的过程,必须弄懂Spring的自动注入的几种方式以及ConfigurationClass这个类的含义。

首先抽略看一下ConfigurationClass类的定义
在这里插入图片描述
通常注入一个类有以下几种方式:

@Bean
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

通过@Bean的方式,此时AppConfig整个类作为ConfigurationClass,而myService作为BeanMethod对象保存到属性beanMethods中。

@ComponentScan注解

这就会扫描对应目录下的注解,比如@Component,当然也包括@Configuration,此时不是作为原来ConfigurationClass对象了,而是另一个ConfigurationClass对象,通过importedBy属性来记录原来的ConfigurationClass对象。

@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig  {
    ...
}
@Import注解
@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

比如上面这种情况,通过ConfigB注入了ConfigA,这个时候会将ConfigAConfigB都作为ConfigurationClass来处理,其中ConfigA对应的ConfigurationClass对象的属性importedBy就包含ConfigB所对应的ConfigurationClass对象。

当然@Import不止以上一种,还包括另外两种情形,用来注入ImportSelector类或者ImportBeanDefinitionRegistrar类。
比如Spring Boot中一个很重要的注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   // ... 省略主要部分
}

通过引入AutoConfigurationImportSelector,在接口方法selectImports来获取Spring Boot通过SPI机制自动注入的类名称列表。
通过ImportSelector接口的方式引入的类比较复杂,如果selectImports返回的类是包含@Configuration的,跟上面的相似,也会作为ConfigurationClass对象来处理。如果是ImportBeanDefinitionRegistrar,比如开启AOP的注解引入的AspectJAutoProxyRegistrar就是这样一个类型。

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
  // ....
}

这时候在ImportBeanDefinitionRegistrar引入的类会作为ConfigurationClass中的属性importBeanDefinitionRegistrars中的一员。

此外还有一些其他的方式 通过@ImportSource注解引入资源,此时则是添加到ConfigurationClass中的属性importedResources中。

而至于PropertySources,则是直接赋值到ConfigurationClassParser#environment属性中。

总之,解析的最终目标无非就是填充ConfigurationClassParser#configurationClasses属性,以及将不同形式注册的类放到ConfigurationClass中的对应属性中,比如importedBy记录被谁引入、beanMethods存放@Bean引入的、importedResources存放通过@ImportSource注解引入的资源,而importBeanDefinitionRegistrars则存放通过ImportBeanDefinitionRegistrar接口引入的。

如下图所示:
在这里插入图片描述

主要使用场景

这个类在ConfigurationClassPostProcessor中的使用如下:

// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
		this.metadataReaderFactory, this.problemReporter, this.environment,
		this.resourceLoader, this.componentScanBeanNameGenerator, registry);

// 所有需要进行解析的@Configuration类,当然还包括其他的比如@Component、@ComponentScan、@Import、@ImportResource
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
// 用于存放已经解析好的ConfigurationClass类
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
	// 进行解析
	parser.parse(candidates);
	// 进行眼这个你
	parser.validate();

	Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
	configClasses.removeAll(alreadyParsed);

	// Read the model and create bean definitions based on its content
	if (this.reader == null) {
		this.reader = new ConfigurationClassBeanDefinitionReader(
				registry, this.sourceExtractor, this.resourceLoader, this.environment,
				this.importBeanNameGenerator, parser.getImportRegistry());
	}
	// 加载bean定义
	this.reader.loadBeanDefinitions(configClasses);
	alreadyParsed.addAll(configClasses);

	candidates.clear();
	if (registry.getBeanDefinitionCount() > candidateNames.length) {
		String[] newCandidateNames = registry.getBeanDefinitionNames();
		Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
		Set<String> alreadyParsedClasses = new HashSet<>();
		for (ConfigurationClass configurationClass : alreadyParsed) {
			alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
		}
		for (String candidateName : newCandidateNames) {
			if (!oldCandidateNames.contains(candidateName)) {
				BeanDefinition bd = registry.getBeanDefinition(candidateName);
				if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
						!alreadyParsedClasses.contains(bd.getBeanClassName())) {
					candidates.add(new BeanDefinitionHolder(bd, candidateName));
				}
			}
		}
		candidateNames = newCandidateNames;
	}
}
while (!candidates.isEmpty());
ConfigurationClassParser构造
/**
 * Create a new {@link ConfigurationClassParser} instance that will be used
 * to populate the set of configuration classes.
 */
public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
		ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
		BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {

	this.metadataReaderFactory = metadataReaderFactory;
	this.problemReporter = problemReporter;
	this.environment = environment;
	this.resourceLoader = resourceLoader;
	this.registry = registry;
	this.componentScanParser = new ComponentScanAnnotationParser(
			environment, resourceLoader, componentScanBeanNameGenerator, registry);
	this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}

在这里插入图片描述
其中各个核心属性作用如下:
MetadataReaderFactory:用于根据类名称获取MetadataReader,读取一个类的各种元数据,比如是否包含某个注解、所有注解的类型,这样就可以轻松的判断一个类是否是需要通过注解注入的类
ResourceLoader:资源读取类,可以用于读取各种文件或网络流数据,在此处应该主要是用于加载classpath下面的class文件
BeanNameGenerator:当将一个class类生成一个BeanDefinition的时候,需要一个唯一的名称,就是通过这个类来实现的
BeanDefinitionRegistry:用于注册Bean定义
以上这个都是通过外面传入的,另外在这个类里面,自己构造了一个类对象,这个类就是ConditionEvaluator,当一个类上面添加了的@Conditional类型的注解的时候,就需要判断这个类是否要注册到Spring容器中,那么此时ConditionEvaluator就派上用场了。
当收集完所有的ConfigurationClass后,就会通过ConfigurationClassParser针对其中每一个进行解析了。

ConfigurationClassParser核心方法parse
public void parse(Set<BeanDefinitionHolder> configCandidates) {
	for (BeanDefinitionHolder holder : configCandidates) {
		BeanDefinition bd = holder.getBeanDefinition();
		try {
			// 针对带有注解的
			if (bd instanceof AnnotatedBeanDefinition) {
				// bean定义包含元数据信息
				parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
			}
			else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
				// bean定义中包括类信息
				parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
			}
			else {
				// bean定义中包含类名称
				parse(bd.getBeanClassName(), holder.getBeanName());
			}
		}
		catch (BeanDefinitionStoreException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new BeanDefinitionStoreException(
					"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
		}
	}

	this.deferredImportSelectorHandler.process();
}

无论是带有注解的,还是bean定义包含类信息,或者类名称的,最终都是构造一个ConfigurationClass对象,然后再进行处理的。

protected final void parse(@Nullable String className, String beanName) throws IOException {
	Assert.notNull(className, "No bean class name for configuration class bean definition");
	MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
	processConfigurationClass(new ConfigurationClass(reader, beanName));
}

protected final void parse(Class<?> clazz, String beanName) throws IOException {
	processConfigurationClass(new ConfigurationClass(clazz, beanName));
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
	processConfigurationClass(new ConfigurationClass(metadata, beanName));
}
  1. 构造ConfigurationClass对象
/**
 * Create a new {@link ConfigurationClass} with the given name.
 * @param metadataReader reader used to parse the underlying {@link Class}
 * @param beanName must not be {@code null}
 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
 */
public ConfigurationClass(MetadataReader metadataReader, String beanName) {
	Assert.notNull(beanName, "Bean name must not be null");
	this.metadata = metadataReader.getAnnotationMetadata();
	this.resource = metadataReader.getResource();
	this.beanName = beanName;
}
/**
 * Create a new {@link ConfigurationClass} with the given name.
 * @param clazz the underlying {@link Class} to represent
 * @param beanName name of the {@code @Configuration} class bean
 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
 */
public ConfigurationClass(Class<?> clazz, String beanName) {
	Assert.notNull(beanName, "Bean name must not be null");
	this.metadata = new StandardAnnotationMetadata(clazz, true);
	this.resource = new DescriptiveResource(clazz.getName());
	this.beanName = beanName;
}
/**
 * Create a new {@link ConfigurationClass} with the given name.
 * @param metadata the metadata for the underlying class to represent
 * @param beanName name of the {@code @Configuration} class bean
 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
 */
public ConfigurationClass(AnnotationMetadata metadata, String beanName) {
	Assert.notNull(beanName, "Bean name must not be null");
	this.metadata = metadata;
	this.resource = new DescriptiveResource(metadata.getClassName());
	this.beanName = beanName;
}

ConfigurationClass中的属性如下:

// 当前ConfigurationClass类对象的注解元数据
private final AnnotationMetadata metadata;
// 当前ConfigurationClass类定义的资源文件
private final Resource resource;
// 对应的Spring中的beanName
@Nullable
private String beanName;
// 被引入的那个类
private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1);
// 定义的带有@Bean注解的方法
private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
// @ImportSource
private final Map<String, Class<? extends BeanDefinitionReader>> importedResources =
		new LinkedHashMap<>();
// 通过@Import额外引入的ImportBeanDefinitionRegistrar定义的bean
private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars =
		new LinkedHashMap<>();

final Set<String> skippedBeanMethods = new HashSet<>();

创建完毕之后,就会进行处理了,首先会判断一下当前类是否需要进行处理,也就是判断Conditional这些条件是否满足环境要求,如果经过判断需要进行过滤,那么就不会继续进行解析了,立即返回。
读取缓存configurationClasses,第一次这里通常是没有值的
然后将configClass转为一个SourceClass类对象,其实就是对类和元数据进行一下包装。因为该类提供了一堆有用的方法,比如加载类对loadClass,判断继承的类或实现的接口isAssignable,返回ConfigurationClass对象asConfigClass,获取成员类getMemberClasses等等。
2. 解析ConfigurationClass对象

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
	// 此时的ConfigurationPhase时机是PARSE_CONFIGURATION,对于ConditionalOnBean是不起作用的
	if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
		return;
	}

	ConfigurationClass existingClass = this.configurationClasses.get(configClass);
	if (existingClass != null) {
		if (configClass.isImported()) {
			if (existingClass.isImported()) {
				existingClass.mergeImportedBy(configClass);
			}
			// Otherwise ignore new imported config class; existing non-imported class overrides it.
			return;
		}
		else {
			// Explicit bean definition found, probably replacing an import.
			// Let's remove the old one and go with the new one.
			this.configurationClasses.remove(configClass);
			this.knownSuperclasses.values().removeIf(configClass::equals);
		}
	}
	
	// Recursively process the configuration class and its superclass hierarchy.
	SourceClass sourceClass = asSourceClass(configClass);
	do {
		// 循环处理 因为可能包含父类
		sourceClass = doProcessConfigurationClass(configClass, sourceClass);
	}
	while (sourceClass != null);
	// 添加缓存
	this.configurationClasses.put(configClass, configClass);
}

完成一些类的准备工作以后(BeanDefinitionHolder->ConfigurationClass->SourceClass),开始真正的解析工作了。主要处理如下的注解@Component@PropertySources@ComponentScan@ComponentScans@Import@ImportResource@Bean、以及默认接口方法、父类逻辑。

/**
 * Apply processing and build a complete {@link ConfigurationClass} by reading the
 * annotations, members and methods from the source class. This method can be called
 * multiple times as relevant sources are discovered.
 * @param configClass the configuration class being build
 * @param sourceClass a source class
 * @return the superclass, or {@code null} if none found or previously processed
 */
@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
		throws IOException {

	if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
		// Recursively process any member (nested) classes first
		processMemberClasses(configClass, sourceClass);
	}

	// Process any @PropertySource annotations
	for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), PropertySources.class,
			org.springframework.context.annotation.PropertySource.class)) {
		if (this.environment instanceof ConfigurableEnvironment) {
			processPropertySource(propertySource);
		}
		else {
			logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
					"]. Reason: Environment must implement ConfigurableEnvironment");
		}
	}

	// Process any @ComponentScan annotations
	Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
	if (!componentScans.isEmpty() &&
			!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
		for (AnnotationAttributes componentScan : componentScans) {
			// The config class is annotated with @ComponentScan -> perform the scan immediately
			Set<BeanDefinitionHolder> scannedBeanDefinitions =
					this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
			// Check the set of scanned definitions for any further config classes and parse recursively if needed
			for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
				BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
				if (bdCand == null) {
					bdCand = holder.getBeanDefinition();
				}
				if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
					parse(bdCand.getBeanClassName(), holder.getBeanName());
				}
			}
		}
	}

	// Process any @Import annotations
	processImports(configClass, sourceClass, getImports(sourceClass), true);

	// Process any @ImportResource annotations
	AnnotationAttributes importResource =
			AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
	if (importResource != null) {
		String[] resources = importResource.getStringArray("locations");
		Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
		for (String resource : resources) {
			String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
			configClass.addImportedResource(resolvedResource, readerClass);
		}
	}

	// Process individual @Bean methods
	Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
	for (MethodMetadata methodMetadata : beanMethods) {
		configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
	}

	// Process default methods on interfaces
	processInterfaces(configClass, sourceClass);

	// Process superclass, if any
	if (sourceClass.getMetadata().hasSuperClass()) {
		String superclass = sourceClass.getMetadata().getSuperClassName();
		if (superclass != null && !superclass.startsWith("java") &&
				!this.knownSuperclasses.containsKey(superclass)) {
			this.knownSuperclasses.put(superclass, configClass);
			// Superclass found, return its annotation metadata and recurse
			return sourceClass.getSuperClass();
		}
	}

	// No superclass -> processing is complete
	return null;
}
processMemberClasses 处理成员内部类

在这里插入图片描述
针对所有的内部类进行遍历,查看是否属于包含有效注解(@ConfigurationComponentComponentScanImportImportResource

/**
 * Register member (nested) classes that happen to be configuration classes themselves.
 */
private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
	Collection<SourceClass> memberClasses = sourceClass.getMemberClasses();
	if (!memberClasses.isEmpty()) {
		List<SourceClass> candidates = new ArrayList<>(memberClasses.size());
		for (SourceClass memberClass : memberClasses) {
			if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) &&
					!memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
				candidates.add(memberClass);
			}
		}

针对满足要求的类进行排序,并作为ConfigurationClass进行处理

		OrderComparator.sort(candidates);
		for (SourceClass candidate : candidates) {
			if (this.importStack.contains(configClass)) {
				this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
			}
			else {
				this.importStack.push(configClass);
				try {
					processConfigurationClass(candidate.asConfigClass(configClass));
				}
				finally {
					this.importStack.pop();
				}
			}
		}
	}
}
处理单独@Bean方法
// Process individual @Bean methods
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
	configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

首先获取一个类中的@Bean方法,作为MethodMetadata对象,然后包装为BeanMethod对象并添加到org.springframework.context.annotation.ConfigurationClass#beanMethods属性当中。

/**
 * Retrieve the metadata for all <code>@Bean</code> methods.
 */
private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) {
	AnnotationMetadata original = sourceClass.getMetadata();
	Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());
	if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
		// Try reading the class file via ASM for deterministic declaration order...
		// Unfortunately, the JVM's standard reflection returns methods in arbitrary
		// order, even between different runs of the same application on the same JVM.
		try {
			AnnotationMetadata asm =
					this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
			Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
			if (asmMethods.size() >= beanMethods.size()) {
				Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
				for (MethodMetadata asmMethod : asmMethods) {
					for (MethodMetadata beanMethod : beanMethods) {
						if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
							selectedMethods.add(beanMethod);
							break;
						}
					}
				}
				if (selectedMethods.size() == beanMethods.size()) {
					// All reflection-detected methods found in ASM method set -> proceed
					beanMethods = selectedMethods;
				}
			}
		}
		catch (IOException ex) {
			logger.debug("Failed to read class file via ASM for determining @Bean method order", ex);
			// No worries, let's continue with the reflection metadata we started with...
		}
	}
	return beanMethods;
}

比如解析如下Lite类型的ConfigurationClass

@Component
public class LiteModeBean {

    @Scope("prototype")
    @Bean
    public LiteOrderDao liteOrderDao() {
        return new LiteOrderDao();
    }

    @Bean
    public LiteOrderService liteOrderService() {
        return new LiteOrderService(liteOrderDao());
    }
}

在这里插入图片描述
在这里插入图片描述

处理单独@Import逻辑

参考博客:注册Bean方式之@Import源码分析

处理单独@ComponentScan 逻辑
  1. 首先解析注解参数
  2. 判断当前SourceClass是否可以用于注册(conditionEvaluator判断类上面的@Conditional条件),如果说SourceClass都不满足条件,由它引起的各种注册都是无效的,因此也就不需要进行扫描了
  3. 遍历解析ComponentScan,使用ComponentScanAnnotationParser扫描出所有符合条件的类并进行注册,注意这里已经注册了符合条件的类(包含@Configuration、@Component等的类)
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
		sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
		!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
	for (AnnotationAttributes componentScan : componentScans) {
		// The config class is annotated with @ComponentScan -> perform the scan immediately
		Set<BeanDefinitionHolder> scannedBeanDefinitions =
				this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

在这里插入图片描述
4. 检查是否符合ConfigurationClass条件(full or lite)并继续进行解析(按照ConfigurationClass),递归调用

		// Check the set of scanned definitions for any further config classes and parse recursively if needed
		for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
			BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
			if (bdCand == null) {
				bdCand = holder.getBeanDefinition();
			}
			if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
				parse(bdCand.getBeanClassName(), holder.getBeanName());
			}
		}
	}
}

参考博客:@ComponentScan的用法和源码分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值