SpringBoot启动流程(二)——自动装配原理

本文详细探讨了SpringBoot的自动装配机制,从什么是自动装配开始,解释了自动装配如何通过DataSourceAutoConfiguration类工作,然后揭示了自动装配类在autoConfig.jar的spring.factories文件中加载的过程。文章深入到AutoConfigurationImportSelector和ConfigurationClassPostProcessor等关键类,逐步解析了自动装配何时被触发以及其在SpringBoot启动流程中的位置。

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

目录

什么是自动装配?

自动装配是如何工作的?

何时触发自动装配?


什么是自动装配?

我们先来看一下如果要配置一个DataSource在spring中是怎么配置的,需要在工程的xml中创建一个Bean节点,配置好后spring在启动时会将其初始化到IOC容器中供需要时使用,大致代码如下:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  
    destroy-method="close">  
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />  
    <property name="url"  
        value="jdbc:sqlserver://xxx" />  
    <property name="username" value="sa" />  
    <property name="password" value="********" />  
</bean>  

那么在SpringBoot中要想配置一个DataSource是怎么配置的呢,很简单只需要在yml文件配置对应属性的值无需额外的xml来配置Bean,此时Bean的创建过程是有跟数据源相关的自动配置类DataSourceAutoConfiguration完成的,这个过程我们称之为自动装配。

spring:
    datasource:
        url: jdbc:mysql://xxx
        username: @prd.db.username@
        password: @prd.db.password@
        driver-class-name: com.mysql.cj.jdbc.Driver

下面是 DataSourceAutoConfiguration的代码,上面的属性会映射到@EnableConfigurationProperties(DataSourceProperties.class)DataSourceProperties类中。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@Conditional(EmbeddedDatabaseCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import(EmbeddedDataSourceConfiguration.class)
	protected static class EmbeddedDatabaseConfiguration {

	}
//省略其他方法
}


--------------------- 下面是DataSourceProperties的部分代码--------------
--------------------- 可以看到与yml配置对应的参数          ---------------



@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

	/**
	 * Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
	 */
	private String driverClassName;

	/**
	 * JDBC URL of the database.
	 */
	private String url;

	/**
	 * Login username of the database.
	 */
	private String username;
//省略其他方法
}

自动装配是如何工作的?

SpringBoot能支持的自动装配的类都在autoConfig.jar 这个jar文件下的spring.factories文件中了,下面仅仅贴出其中部分作为示例说明, 该文件是如何加载的请参考SpringBoot启动流程(一)这里不赘述了。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\

加载这些类是通过AutoConfigurationImportSelector类完成的,该类是通过@EnableAutoConfiguration注解导入的,而@EnableAutoConfiguration注解又是标注在SpringBootApplication注解上的,下方贴出示意代码。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
。
。
。
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

AutoConfigurationPackages.Registrar是用来加载我们应用程序里标注了支持注解的类,比如@RestController、@Configuration等。这部分代码比较简单,大家可以自行查看,我这里重点写一下AutoConfigurationImportSelector是如何加载自动装配类的。

AutoConfigurationImportSelector 有一个方法selectImports(),该方法中又调用了getAutoConfigurationEntry方法,如下:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
        // 该方法就是从spring.factories中加载EnableAutoConfiguration所有的类名
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
        // 根据项目pom文件导入的starter过滤用不到的
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

getCandidateConfigurations方法比较简单就是从spring.factories文件中加载对应类的名称此处并没有实例化。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

filter代码如下:这个代码相对复杂,我暂时也没搞清楚是怎么与当前用到的比较的,有哪位老铁知道的请留言指教一下,哈!

List<String> filter(List<String> configurations) {
			long startTime = System.nanoTime();
			String[] candidates = StringUtils.toStringArray(configurations);
			boolean skipped = false;
			for (AutoConfigurationImportFilter filter : this.filters) {
				boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
				for (int i = 0; i < match.length; i++) {
					if (!match[i]) {
						candidates[i] = null;
						skipped = true;
					}
				}
			}
			if (!skipped) {
				return configurations;
			}
			List<String> result = new ArrayList<>(candidates.length);
			for (String candidate : candidates) {
				if (candidate != null) {
					result.add(candidate);
				}
			}
			if (logger.isTraceEnabled()) {
				int numberFiltered = configurations.size() - result.size();
				logger.trace("Filtered " + numberFiltered + " auto configuration class in "
						+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
			}
			return result;
		}

	}

到这我们需要用到的自动装配的类就被加载进来了,但是我们的分析并没有结束,还有一个问题,就是这个selectImports 方法是在什么时候触发的?

何时触发自动装配?

我们找到AbstractApplicationContext中的invokeBeanFactoryPostProcessors方法,如果不知道为什么要找这个方法,可以先看下我写的SpringBoot启动流程(一)这里就不说了。

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
		PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

		// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
		// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
		if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
			beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
			beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
		}
	}

接着我们找PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,阅读该方法,我们找到该类下的invokeBeanDefinitionRegistryPostProcessors方法如下

private static void invokeBeanDefinitionRegistryPostProcessors(
			Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {

		for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
			StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
					.tag("postProcessor", postProcessor::toString);
			postProcessor.postProcessBeanDefinitionRegistry(registry);
			postProcessBeanDefRegistry.end();
		}
	}

接着找到ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry方法如下

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
		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);
		}
		this.registriesPostProcessed.add(registryId);

		processConfigBeanDefinitions(registry);
	}

接着我们找到该类下的processConfigBeanDefinitions方法这个比较长,我们这里找到解析@Configuration注解的代码部分如下:

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

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
	StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
    //这个方法是重点
	parser.parse(candidates);
	parser.validate();

接着找parser.parse()方法如下,从这个方法中我们可以看到先由parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());完成解析,deferredImportSelectorHandler.process()方法中又以组的方式处理了一遍,最终的处理方法与parse方法一样(这地方没弄明白为啥)。

public void parse(Set<BeanDefinitionHolder> configCandidates) {
		for (BeanDefinitionHolder holder : configCandidates) {
			BeanDefinition bd = holder.getBeanDefinition();
			try {
				if (bd instanceof AnnotatedBeanDefinition) {
					parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
				}
				else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
					parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
				}
				else {
					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();
	}

跟着parse()方法,我们找到如下重点代码:

// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
	sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);

阅读doProcessConfigurationClass方法可以看到,这个方法中解析了很多注解,比如@CompoScan,@PropertySource,还有我们最关心的@Import注解。

protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {

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

		// 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), filter, 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;
	}

在processImports这个方法中,可以看到String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());调用了selectImports方法,到这个时候终于找到出发该方法的地方了,是不是相当不容易,哈,大家在阅读这2个方法时可能比较费劲,这里面很多递归查找。

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
			boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);
						Predicate<String> selectorFilter = selector.getExclusionFilter();
						if (selectorFilter != null) {
							exclusionFilter = exclusionFilter.or(selectorFilter);
						}
						if (selector instanceof DeferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
						}
						else {
                            // 这个地方调用了Selecter中的selectImports方法
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
						}
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
					}
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
						configClass.getMetadata().getClassName() + "]", ex);
			}
			finally {
				this.importStack.pop();
			}
		}
	}

 

 

 

 

### Spring Boot 自动装配原理解析 Spring Boot自动装配功能是其核心特性之一,它极大地简化了开发者的配置工作。以下是关于 Spring Boot 自动装配原理的详细解析: #### 1. **Starter 启动器** Spring Boot 提供了一系列场景化的 Starter 启动器,这些启动器封装了特定技术栈所需的依赖项和默认配置。例如 `spring-boot-starter-web` 就包含了构建 Web 应用所需的核心库[^1]。 当开发者引入某个 Starter 启动器时,Spring Boot 会基于项目的依赖关系以及运行环境中的条件,决定加载哪些组件和服务。这种设计使得开发者无需手动管理复杂的依赖树。 --- #### 2. **SpringFactoriesLoader 加载机制** Spring Boot 使用 `SpringFactoriesLoader` 来加载扩展点。具体来说,`META-INF/spring.factories` 文件中定义了一些 key-value 对应的关系,其中 value 是一组类名列表。Spring Boot 会在应用启动阶段读取该文件,并将指定的类实例化后注册到 Spring 容器中[^2]。 这一过程的关键在于通过 SPI(Service Provider Interface)机制动态发现并加载扩展点,从而实现了高度可插拔的功能模块支持。 --- #### 3. **AutoConfigurationImportSelector 类的作用** `AutoConfigurationImportSelector` 是 Spring Boot 实现自动装配的重要入口。它的主要职责是扫描项目中可用的自动配置类,并将其加入到 Spring 容器中。这一步骤由 `selectImports()` 方法完成,返回一个需要导入的候选类集合[^3]。 具体的筛选逻辑如下: - 利用 `@EnableAutoConfiguration` 注解触发整个流程。 - 结合 `spring-boot-autoconfigure` 模块中的元数据文件 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 找到所有可能适用的自动配置类。 - 根据上下文中实际存在的 Bean 和其他条件判断是否启用某一项配置。 --- #### 4. **Condition 接口与条件匹配** 为了确保只加载必要的配置而不会造成冲突或冗余,Spring Boot 设计了一套灵活的条件评估体系——即 Condition 接口及其子接口家族。常见的实现有: - `@ConditionalOnClass`: 当某些类存在于 classpath 上时生效。 - `@ConditionalOnMissingBean`: 如果容器中不存在某种类型的 Bean,则创建一个新的。 - `@ConditionalOnProperty`: 根据配置属性值控制行为。 这些条件注解被广泛应用于各个自动配置类之上,只有满足对应约束才会执行相应操作。 --- #### 5. **总结** 综上所述,Spring Boot自动装配是一个复杂但优雅的过程,涉及多个关键技术环节: - Starter 启动器提供基础框架; - SpringFactoriesLoader 动态加载扩展点; - AutoConfigurationImportSelector 负责挑选合适的配置类; - Conditions 系统保障精准适配。 掌握以上知识点不仅有助于深入理解 Spring Boot 工作方式,在面对面试官提问时也能展现扎实的技术功底。 ```java @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` 上述代码片段展示了如何快速搭建一个具备自动装配能力的应用程序。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

有机叶生菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值