Spring源码导读之ConfigurationClassPostProcessor

本文深入探讨Spring框架中配置类的解析与增强机制,详细分析ConfigurationClassPostProcessor如何识别和处理配置类,生成更多BeanDefinition,并对带有@Configuration注解的类进行代理增强,确保单例模式的正确应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring源码导读

目录

种子类

什么时候被定义

实现的两个方法

postProcessBeanDefinitionRegistry

生根发芽

全量配置类和轻量配置类

ConfigurationClassParser

postProcessBeanFactory

案例

Spring对全量配置类的增强


 

种子类

也称配置类

在上一章AnnotationConfigApplicationContext容器介绍中提到,该容器提供两个成员变量对beanDefinition进行注册。

1、scanner:   ClassPathBeanDefinitionScanner。扫描指定报下的class文件, 如果被component注解且是top level类,则注册。

2、reader:      AnnotatedBeanDefinitionReader。注册指定的class文件。

这个容器看似太简单了,但其实基于注解的bean定义的主要部分并在在此容器的加载阶段完成。AnnotationConfigApplicationContext只是注册的bean只能被认为是一个 “种子类”,后面会针对这个“种子类”上的各种注解进行生根发芽,生成其他注解方式生成的BeanDefinition。

这个 “生根发芽” 的工作就是ConfigurationClassPostProcessor,完成的。ConfigurationClassPostProcessor实现了beanFactoryPostProcessor。它会在refresh()的invokeBeanFactoryPostProcessors阶段,完成“生根发芽”。

 

什么时候被定义

ConfigurationClassPostProcessor的bean定义什么以后加入到容器的?

1、AnnotatedBeanDefinitionReader的构造函数种会调AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)

2、ClassPathBeanDefinitionScanner#scan种也会调用AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)

3、在xml模式下如果配置<context:annotation-config/>,会通过其NameSpaceHandler解析并注册

实现的两个方法

postProcessBeanDefinitionRegistry:负责"生根发芽"。生成更多的beanDefinition。

postProcessBeanFactory: 在某些情况它会对被Configuration注解的类进行代理增强。

源码介绍:

postProcessBeanDefinitionRegistry

生根发芽

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
	// 对加入进来的registry生成一个唯一Id, 说明它可以针对多个容器进行处理。
	int registryId = System.identityHashCode(registry);
	if (this.registriesPostProcessed.contains(registryId)) {
		throw new IllegalStateException(
				"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
	}
	// 如果已经处理过了, 则抛出异常
	if (this.factoriesPostProcessed.contains(registryId)) {
		throw new IllegalStateException(
				"postProcessBeanFactory already called on this post-processor against " + registry);
	}
	// 记录已经处理过的registry
	this.registriesPostProcessed.add(registryId);

	// 开始"生根发芽"
	processConfigBeanDefinitions(registry);
}

processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
	//第一步:寻找第一波种子
	
	// 拿到所有bean定义的beanName作为候选name
	String[] candidateNames = registry.getBeanDefinitionNames();

	// 循环处理
	for (String beanName : candidateNames) {
		BeanDefinition beanDef = registry.getBeanDefinition(beanName);
		// 判断这个BeanDefinition是不是全量配置, 或者是轻量配置
		// 第一次进来,不会有这个标记的。
		if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
				ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
			}
		}
		// 通过对类的AnnotationMetadata进行判断,是不是一个配置候选类, 
		// 并且这里判断之后, 会在BeanDefinition上打标记
		else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
			如果是则加入configCandidates
			configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
		}
	}

	// 如果没有配置类则返回
	if (configCandidates.isEmpty()) {
		return;
	}

	// 排序
	configCandidates.sort((bd1, bd2) -> {
		int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
		int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
		return Integer.compare(i1, i2);
	});

	// 获取一个bean名称的生成器
	SingletonBeanRegistry sbr = null;
	if (registry instanceof SingletonBeanRegistry) {
		sbr = (SingletonBeanRegistry) registry;
		if (!this.localBeanNameGeneratorSet) {
			BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
			if (generator != null) {
				this.componentScanBeanNameGenerator = generator;
				this.importBeanNameGenerator = generator;
			}
		}
	}

	if (this.environment == null) {
		this.environment = new StandardEnvironment();
	}

	// 生成一个配置类的解析器
	ConfigurationClassParser parser = new ConfigurationClassParser(
			this.metadataReaderFactory, this.problemReporter, this.environment,
			this.resourceLoader, this.componentScanBeanNameGenerator, registry);

	Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
	// 生成一个已解析的列表, 因为下面要进入一个循环体, 要记录那些已经被解析的ConfigurationClass
	Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
	
	//第二步:从第一波种子开始进行生根发芽
	do {
		// 调用parser解析 candidates 是作为本次解析的配置类集合,理解为本次的 "种子"
		parser.parse(candidates);
		parser.validate();

		// configClasses是经过本次生根发芽后的总的配置类集合 = "种子" + "种子生成的类"
		Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
		// 剔除 已经被解析过的ConfigurationClass
		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());
		}
		// 第三步: 将解析出来的configClasses进行注册。
		this.reader.loadBeanDefinitions(configClasses);
		// 同时将configClasses 加入到 alreadyParsed, 标记为已经处理过了
		alreadyParsed.addAll(configClasses);

		// 再将candidates进行清理
		candidates.clear();
		
		// 第四步 寻找下一波种子再次进行解析和注册
		// 这个方法体的主要作用是判断本次生成的BeanDefinition, 有没有新的配置类,
		// 如果有则加入candidates作为"种子", 进入下一次循环
		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());

	// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
	if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
		sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
	}

	if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
		// Clear cache in externally provided MetadataReaderFactory; this is a no-op
		// for a shared cache since it'll be cleared by the ApplicationContext.
		((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
	}
}

全量配置类和轻量配置类

ConfigurationClassUtils#checkConfigurationClassCandidate:通过对类的AnnotationMetadata进行判断,是不是一个配置候选类, 配置类分为:

全量配置类: 被Configuration注解的类

轻量配置类: 是否被Componet、ComponentScan、Import、ImportResource注解,或存在方法有Bean注解

public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
	String className = beanDef.getBeanClassName();
	if (className == null || beanDef.getFactoryMethodName() != null) {
		return false;
	}

	// 尝试获得一个AnnotationMetadata
	AnnotationMetadata metadata;
	// 如果beanDef 实现了AnnotatedBeanDefinition 那么可以直接getMetadata
	if (beanDef instanceof AnnotatedBeanDefinition &&
			className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) {
		// Can reuse the pre-parsed metadata from the given BeanDefinition...
		metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata();
	}
	// 如果beanDef实现了AbstractBeanDefinition, 则获取它的class, new 出一个StandardAnnotationMetadata
	else if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) {
		// Check already loaded Class if present...
		// since we possibly can't even load the class file for this Class.
		Class<?> beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass();
		metadata = new StandardAnnotationMetadata(beanClass, true);
	}
	else {
		try {
			// 最后尝试 通过SimpleMetadataReader 获取一个metadata,
			MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
			// 其实是AnnotationMetadataReadingVisitor
			metadata = metadataReader.getAnnotationMetadata();
		}
		catch (IOException ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Could not find class file for introspecting configuration annotations: " + className, ex);
			}
			return false;
		}
	}

	// 判断metadata封装的类是否是一个全量配置, 就是判断是否配Configuration注解: metadata.isAnnotated(Configuration.class.getName())
	if (isFullConfigurationCandidate(metadata)) {
		// 那么则在bean定义种打上"全量配置"标记
		beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
	}
	// 判断metadata封装的类是否是一个轻量配置, 是否被Componet、ComponentScan、Import、ImportResource注解
	// 或者存在方法上有Bean注解
	else if (isLiteConfigurationCandidate(metadata)) {
		// 那么则在bean定义上打上"轻量配置"标记
		beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
	}
	// 否则认为不是一个配置类
	else {
		return false;
	}

	// It's a full or lite configuration candidate... Let's determine the order value, if any.
	Integer order = getOrder(metadata);
	if (order != null) {
		beanDef.setAttribute(ORDER_ATTRIBUTE, order);
	}

	return true;
}

全量配置类:

public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
	return metadata.isAnnotated(Configuration.class.getName());
}

轻量配置类: 

ConfigurationClassParser

ConfigurationClassParser#parse(java.util.Set<BeanDefinitionHolder>)

public void parse(Set<BeanDefinitionHolder> configCandidates) {
	this.deferredImportSelectors = new LinkedList<>();

	for (BeanDefinitionHolder holder : configCandidates) {
		BeanDefinition bd = holder.getBeanDefinition();
		try {
			// 传入Metadata进行解析
			if (bd instanceof AnnotatedBeanDefinition) {
				parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
			}
			// 传入Class进行解析
			else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
				parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
			}
			else {
				// 传入className进行解析
				parse(bd.getBeanClassName(), holder.getBeanName());
			}
		}
		catch (BeanDefinitionStoreException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new BeanDefinitionStoreException(
					"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
		}
	}

	processDeferredImportSelectors();
}

这三种parse其实都是针对ConfigurationClass 的 metatada进行解析

如果是传入className,则将className作为资源,使用MetadataReader获取它的AnnotationMetadata

如果传入class,则将其封装成StandardAnnotationMetadata

如果传入metadata,则直接使用这个metadata

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));
}

public ConfigurationClass(MetadataReader metadataReader, String beanName) {
	// 则将className作为资源,使用MetadataReader获取它的AnnotationMetadata
	Assert.notNull(beanName, "Bean name must not be null");
	this.metadata = metadataReader.getAnnotationMetadata();
	this.resource = metadataReader.getResource();
	this.beanName = beanName;
}

public ConfigurationClass(Class<?> clazz, String beanName) {
	Assert.notNull(beanName, "Bean name must not be null");
	// 如果传入class,则将其封装成StandardAnnotationMetadata
	this.metadata = new StandardAnnotationMetadata(clazz, true);
	this.resource = new DescriptiveResource(clazz.getName());
	this.beanName = beanName;
}

public ConfigurationClass(AnnotationMetadata metadata, String beanName) {
	Assert.notNull(beanName, "Bean name must not be null");
	// 如果传入metadata,则直接使用这个metadata
	this.metadata = metadata;
	this.resource = new DescriptiveResource(metadata.getClassName());
	this.beanName = beanName;
}

processConfigurationClass:

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
	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);
		}
	}

	// 将configClass转换为SourceClass, SourceClass是在ConfigurationClass上的包装, 它多了一些方法,比如可以拿到其memberClasses
	SourceClass sourceClass = asSourceClass(configClass);
	do {
		// 最后将configClass和sourceClass都传入进行处理, 并返回sourceClass的一个超类
		// 知道其超类都没有了, 才停止处理
		// 这里有很多递归过程, 有可能这个类可能会有超类, 有嵌套类和内部类等
		sourceClass = doProcessConfigurationClass(configClass, sourceClass);
	}
	while (sourceClass != null);

	// 将configClass加入缓存
	this.configurationClasses.put(configClass, configClass);
}

然后调用 doProcessConfigurationClass,解析内部类和嵌套类,被Spring的各种注解注解的类,以及它的超类。

具体查看《ConfigurationClassPostProcessor使用案例

postProcessBeanFactory

如果我们的种子类配置了Configuration注解,就会被认为是全量的配置类,Spring会对这个类进行一次增强。其增强动作就是在postProcessBeanFactory中完成的。

案例

/**
 * 如果 “种子”注解了{@link Configuration} 则会调用
 * {@link ConfigurationClassPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)}
 * ==》{@link ConfigurationClassPostProcessor#enhanceConfigurationClasses(ConfigurableListableBeanFactory)}
 * 对进行增强保证 单例
 * 案例中的EnhanceRegisterBeanMethodTest 并没有实现{@link org.springframework.beans.factory.FactoryBean} 接口,不是简单工厂模式,而是工厂方法模式
 * @author : 江鹏亮
 * @date : 2020-06-17 21:57
 **/
//@Configuration
public class EnhanceRegisterBeanMethodTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(EnhanceRegisterBeanMethodTest.class);
        Person person1 = context.getBean(Person.class);
        // 获取factoryBean
        EnhanceRegisterBeanMethodTest enhanceRegisterBeanMethodTest = context.getBean(EnhanceRegisterBeanMethodTest.class);
        System.out.println("enhanceRegisterBeanMethodTest = " + enhanceRegisterBeanMethodTest.getClass().getSimpleName());
        // 调用工厂方法再次调用person方法。
        Person person2 = enhanceRegisterBeanMethodTest.person();
        // 看看person1 和 person2 是否相等
        if (person1 == person2) {
            System.out.println("person1 == person2");
        } else {
            System.out.println("person1 != person2");
        }
    }
    @Bean
    public Person person() {
        System.out.println("Person 的 构造函数被调用");
        return new Person();
    }
}

结果:

当不被@Configuration注解时,调用工厂再次获取bean时,会触发bean的构造,这符合我们的预期,但不符合我们对单例的要求。

如何解决?加上@Configuration注解

/**
 * 如果 “种子”注解了{@link Configuration} 则会调用
 * {@link ConfigurationClassPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)}
 * ==》{@link ConfigurationClassPostProcessor#enhanceConfigurationClasses(ConfigurableListableBeanFactory)}
 * 对进行增强保证 单例
 * 案例中的EnhanceRegisterBeanMethodTest 并没有实现{@link org.springframework.beans.factory.FactoryBean} 接口,不是简单工厂模式,而是工厂方法模式
 * @author : 江鹏亮
 * @date : 2020-06-17 21:57
 **/
@Configuration
public class EnhanceRegisterBeanMethodTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(EnhanceRegisterBeanMethodTest.class);
        Person person1 = context.getBean(Person.class);
        // 获取factoryBean
        EnhanceRegisterBeanMethodTest enhanceRegisterBeanMethodTest = context.getBean(EnhanceRegisterBeanMethodTest.class);
        System.out.println("enhanceRegisterBeanMethodTest = " + enhanceRegisterBeanMethodTest.getClass().getSimpleName());
        // 调用工厂方法再次调用person方法。
        Person person2 = enhanceRegisterBeanMethodTest.person();
        // 看看person1 和 person2 是否相等
        if (person1 == person2) {
            System.out.println("person1 == person2");
        } else {
            System.out.println("person1 != person2");
        }
    }
    @Bean
    public Person person() {
        System.out.println("Person 的 构造函数被调用");
        return new Person();
    }
}

结果:

加上@Configuration后,Spring会对原来的工厂进行一次代理增强,再次调用工厂方法获取bean时,获取的是容器中cache中的bean。

Spring对全量配置类的增强

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	int factoryId = System.identityHashCode(beanFactory);
	if (this.factoriesPostProcessed.contains(factoryId)) {
		throw new IllegalStateException(
				"postProcessBeanFactory already called on this post-processor against " + beanFactory);
	}
	this.factoriesPostProcessed.add(factoryId);
	if (!this.registriesPostProcessed.contains(factoryId)) {
		// BeanDefinitionRegistryPostProcessor hook apparently not supported...
		// Simply call processConfigurationClasses lazily at this point then.
		processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
	}

	// 对ConfigurationClasses增强
	enhanceConfigurationClasses(beanFactory);
	beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

过滤全量配置类并使用ConfigurationClassEnhancer 对其进行增强。ConfigurationClassEnhancer代理用的是CGLib。在《ConfigurationClassEnhancer》中介绍CGLib并对ConfigurationClassEnhancer为案例进行解释。

public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
	Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
	
	// 过滤全量配置类加入到configBeanDefs中
	for (String beanName : beanFactory.getBeanDefinitionNames()) {
		BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
		if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
			if (!(beanDef instanceof AbstractBeanDefinition)) {
				throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
						beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
			}
			else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) {
				logger.warn("Cannot enhance @Configuration bean definition '" + beanName +
						"' since its singleton instance has been created too early. The typical cause " +
						"is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
						"return type: Consider declaring such methods as 'static'.");
			}
			configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
		}
	}
	if (configBeanDefs.isEmpty()) {
		// nothing to enhance -> return immediately
		return;
	}

	// new 一个ConfigurationClassEnhancer用于增强
	ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
	
	// 遍历每个configBeanDefs,对其增强
	for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
		AbstractBeanDefinition beanDef = entry.getValue();
		// If a @Configuration class gets proxied, always proxy the target class
		beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
		try {
			// Set enhanced subclass of the user-specified bean class
			Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
			if (configClass != null) {
				Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
				if (configClass != enhancedClass) {
					if (logger.isDebugEnabled()) {
						logger.debug(String.format("Replacing bean definition '%s' existing class '%s' with " +
								"enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
					}
					// 设置BeanClass为增强后的类
					beanDef.setBeanClass(enhancedClass);
				}
			}
		}
		catch (Throwable ex) {
			throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
		}
	}
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值