Spring 工具 ConfigurationClassBeanDefinitionReader 注册配置类中的bean定义

Spring Bean定义注册流程
本文详细介绍了Spring框架中ConfigurationClassBeanDefinitionReader类的作用,即如何读取已经解析好的配置类,将其注册为bean定义到bean容器的过程。此过程涉及ConfigurationClassParser的使用,并通过SpringApplication.run方法触发。

概述

读取一组已经被完整解析的配置类ConfigurationClass,向给定bean容器BeanDefinitionRegistry注册其中所有的bean定义。

该内部工具由Spring BeanDefinitionRegistryPostProcessor ConfigurationClassParser使用。在容器启动过程中, ConfigurationClassParser会在BeanDefinitionRegistryPostProcessor 应用阶段被调用,用于发现应用中所有的配置类ConfigurationClass,然后交给ConfigurationClassBeanDefinitionReader将这些配置类中的bean定义注册到容器。

应用位置

SpringApplication.run()
=>refreshContext(ConfigurableApplicationContext context)
	=> EmbeddedWebApplicationContext.refresh()
		=> AbstractApplicationContext.refresh()
		=>invokeBeanFactoryPostProcessors()
			=> PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()
				=> ConfigurationClassPostProcessor.processConfigBeanDefinitions()
					=> ConfigurationClassBeanDefinitionReader.loadBeanDefinitions()

对外提供的服务方法

// 这是该类对外提供的唯一的服务方法入口
// configurationModel 通常来自 ConfigurationClassParser 
void loadBeanDefinitions(Set<ConfigurationClass> configurationModel)

源码分析

/**
 * Reads a given fully-populated set of ConfigurationClass instances, registering bean
 * definitions with the given BeanDefinitionRegistry based on its contents.
 * 
 * 读取一组带有完整解析数据的ConfigurationClass集合,基于他们所携带的信息向给定BeanDefinitionRegistry
 * 注册其中所有的bean定义。
 * 
 * This class was modeled after the BeanDefinitionReader hierarchy, but does
 * not implement/extend any of its artifacts as a set of configuration classes is not a
 * Resource.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @author Phillip Webb
 * @author Sam Brannen
 * @since 3.0
 * @see ConfigurationClassParser
 */
class ConfigurationClassBeanDefinitionReader {

	private static final Log logger = LogFactory.getLog(ConfigurationClassBeanDefinitionReader.class);

	private static final ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();

	private final BeanDefinitionRegistry registry;

	private final SourceExtractor sourceExtractor;

	private final ResourceLoader resourceLoader;

	private final Environment environment;

	private final BeanNameGenerator importBeanNameGenerator;

	private final ImportRegistry importRegistry;

	//对@Conditional注解求值的工具类
	private final ConditionEvaluator conditionEvaluator;


	/**
	 * Create a new ConfigurationClassBeanDefinitionReader instance that will be used
	 * to populate the given BeanDefinitionRegistry.
	 *  
	 * 创建一个新的往指定的BeanDefinitionRegistry填充bean定义的
	 * ConfigurationClassBeanDefinitionReader 实例
	 */
	ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor,
			ResourceLoader resourceLoader, Environment environment, BeanNameGenerator importBeanNameGenerator,
			ImportRegistry importRegistry) {

		this.registry = registry;
		this.sourceExtractor = sourceExtractor;
		this.resourceLoader = resourceLoader;
		this.environment = environment;
		this.importBeanNameGenerator = importBeanNameGenerator;
		this.importRegistry = importRegistry;
		this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
	}


	/**
	 * Read configurationModel, registering bean definitions
	 * with the registry based on its contents.
	 * configurationModel 是一组ConfigurationClass,表示一组配置类,该方法从中读取bean定义
	 * 并注册到bean容器
	 */
	public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
		TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
		for (ConfigurationClass configClass : configurationModel) {
			//遍历处理参数configurationModel中的每个配置类
			loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
		}
	}

	/**
	 * Read a particular ConfigurationClass, registering bean definitions
	 * for the class itself and all of its Bean methods.
	 * 从指定的一个配置类ConfigurationClass中提取bean定义信息并注册bean定义到bean容器 :
	 * 1. 配置类本身要注册为bean定义
	 * 2. 配置类中的@Bean注解方法要注册为配置类
	 */
	private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
			TrackedConditionEvaluator trackedConditionEvaluator) {

		if (trackedConditionEvaluator.shouldSkip(configClass)) {
			String beanName = configClass.getBeanName();
			if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
				this.registry.removeBeanDefinition(beanName);
			}
			this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
			return;
		}

		if (configClass.isImported()) {
			// 如果这是一个通过import机制被导入进来的配置类,将它本身作为一个bean定义注册到容器
			registerBeanDefinitionForImportedConfigurationClass(configClass);
		}
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {
			// 现在把配置类里面@Bean注解的方法作为bean定义注册到容器
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}
		// 从配置类导入的bean定义资源中获取bean定义信息并注册到容器
		// 比如导入的xml或者groovy bean定义文件
		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
		// 从配置类导入的ImportBeanDefinitionRegistrar中获取bean定义信息并注册到容器
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
	}

	/**
	 * Register the Configuration class itself as a bean definition.
	 * 配置类本身作为bean定义注册到容器
	 */
	private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) {
		AnnotationMetadata metadata = configClass.getMetadata();
		AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata);

		ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
		configBeanDef.setScope(scopeMetadata.getScopeName());
		String configBeanName = this.importBeanNameGenerator.generateBeanName(
															configBeanDef, this.registry);
		AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);

		BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
		definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, 
											definitionHolder, this.registry);
		this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
		configClass.setBeanName(configBeanName);

		if (logger.isDebugEnabled()) {
			logger.debug("Registered bean definition for imported class '" + configBeanName + "'");
		}
	}

	/**
	 * Read the given BeanMethod, registering bean definitions
	 * with the BeanDefinitionRegistry based on its contents.
	 * @Bean注解的配置类方法作为bean定义注册到bean容器
	 */
	private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
		ConfigurationClass configClass = beanMethod.getConfigurationClass();
		MethodMetadata metadata = beanMethod.getMetadata();
		String methodName = metadata.getMethodName();

		// Do we need to mark the bean as skipped by its condition?
		if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
			configClass.skippedBeanMethods.add(methodName);
			return;
		}
		if (configClass.skippedBeanMethods.contains(methodName)) {
			return;
		}

		// Consider name and any aliases
		AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
		List<String> names = new ArrayList<String>(Arrays.asList(bean.getStringArray("name")));
		String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

		// Register aliases even when overridden
		for (String alias : names) {
			this.registry.registerAlias(beanName, alias);
		}

		// Has this effectively been overridden before (e.g. via XML)?
		if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
			if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {			
				throw new BeanDefinitionStoreException(
				beanMethod.getConfigurationClass().getResource().getDescription(),
					beanName, "Bean name derived from @Bean method '" + 		
					beanMethod.getMetadata().getMethodName() +
		"' clashes with bean name for containing configuration class; please make those names unique!");
			}
			return;
		}

		ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
		beanDef.setResource(configClass.getResource());
		beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

		if (metadata.isStatic()) {
			// static @Bean method 静态@Bean方法
			beanDef.setBeanClassName(configClass.getMetadata().getClassName());
			beanDef.setFactoryMethodName(methodName);
		}
		else {
			// instance @Bean method 实例成员@Bean方法
			beanDef.setFactoryBeanName(configClass.getBeanName());
			beanDef.setUniqueFactoryMethodName(methodName);
		}
		beanDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR);
		beanDef.setAttribute(RequiredAnnotationBeanPostProcessor.SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

		AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

		Autowire autowire = bean.getEnum("autowire");
		if (autowire.isAutowire()) {
			beanDef.setAutowireMode(autowire.value());
		}

		String initMethodName = bean.getString("initMethod");
		if (StringUtils.hasText(initMethodName)) {
			beanDef.setInitMethodName(initMethodName);
		}

		String destroyMethodName = bean.getString("destroyMethod");
		if (destroyMethodName != null) {
			beanDef.setDestroyMethodName(destroyMethodName);
		}

		// Consider scoping
		ScopedProxyMode proxyMode = ScopedProxyMode.NO;
		AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
		if (attributes != null) {
			beanDef.setScope(attributes.getString("value"));
			proxyMode = attributes.getEnum("proxyMode");
			if (proxyMode == ScopedProxyMode.DEFAULT) {
				proxyMode = ScopedProxyMode.NO;
			}
		}

		// Replace the original bean definition with the target one, if necessary
		BeanDefinition beanDefToRegister = beanDef;
		if (proxyMode != ScopedProxyMode.NO) {
			BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
					new BeanDefinitionHolder(beanDef, beanName), this.registry,
					proxyMode == ScopedProxyMode.TARGET_CLASS);
			beanDefToRegister = new ConfigurationClassBeanDefinition(
					(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
		}

		if (logger.isDebugEnabled()) {
			logger.debug(String.format("Registering bean definition for @Bean method %s.%s()",
					configClass.getMetadata().getClassName(), beanName));
		}

		this.registry.registerBeanDefinition(beanName, beanDefToRegister);
	}

	protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) {
		if (!this.registry.containsBeanDefinition(beanName)) {
			return false;
		}
		BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName);

		// Is the existing bean definition one that was created from a configuration class?
		// -> allow the current bean method to override, since both are at second-pass level.
		// However, if the bean method is an overloaded case on the same configuration class,
		// preserve the existing bean definition.
		if (existingBeanDef instanceof ConfigurationClassBeanDefinition) {
			ConfigurationClassBeanDefinition ccbd = (ConfigurationClassBeanDefinition) existingBeanDef;
			return ccbd.getMetadata().getClassName().equals(
					beanMethod.getConfigurationClass().getMetadata().getClassName());
		}

		// A bean definition resulting from a component scan can be silently overridden
		// by an @Bean method, as of 4.2...
		if (existingBeanDef instanceof ScannedGenericBeanDefinition) {
			return false;
		}

		// Has the existing bean definition bean marked as a framework-generated bean?
		// -> allow the current bean method to override it, since it is application-level
		if (existingBeanDef.getRole() > BeanDefinition.ROLE_APPLICATION) {
			return false;
		}

		// At this point, it's a top-level override (probably XML), just having been parsed
		// before configuration class processing kicks in...
		if (this.registry instanceof DefaultListableBeanFactory &&
				!((DefaultListableBeanFactory) this.registry).isAllowBeanDefinitionOverriding()) {
			throw new BeanDefinitionStoreException(
			beanMethod.getConfigurationClass().getResource().getDescription(),
					beanName, 
					"@Bean definition illegally overridden by existing bean definition: " 
					+ existingBeanDef);
		}
		if (logger.isInfoEnabled()) {
			logger.info(String.format("Skipping bean definition for %s: a definition for bean '%s' " +
					"already exists. This top-level bean definition is considered as an override.",
					beanMethod, beanName));
		}
		return true;
	}

	private void loadBeanDefinitionsFromImportedResources(
			Map<String, Class<? extends BeanDefinitionReader>> importedResources) {

		Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<Class<?>, BeanDefinitionReader>();

		for (Map.Entry<String, Class<? extends BeanDefinitionReader>> entry : importedResources.entrySet()) {
			String resource = entry.getKey();
			Class<? extends BeanDefinitionReader> readerClass = entry.getValue();

			// Default reader selection necessary?
			if (BeanDefinitionReader.class == readerClass) {
				if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) {
					// When clearly asking for Groovy, that's what they'll get...
					// 如果是groovy bean定义文件采用如下读取类
					readerClass = GroovyBeanDefinitionReader.class;
				}
				else {
					// Primarily ".xml" files but for any other extension as well
					// 如果是xml bean定义文件采用如下读取类
					readerClass = XmlBeanDefinitionReader.class;
				}
			}

			BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
			if (reader == null) {
				try {
					// Instantiate the specified BeanDefinitionReader
					reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
					// Delegate the current ResourceLoader to it if possible
					if (reader instanceof AbstractBeanDefinitionReader) {
						AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader);
						abdr.setResourceLoader(this.resourceLoader);
						abdr.setEnvironment(this.environment);
					}
					readerInstanceCache.put(readerClass, reader);
				}
				catch (Throwable ex) {
					throw new IllegalStateException(
							"Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
				}
			}

			// TODO SPR-6310: qualify relative path locations as done in AbstractContextLoader.modifyLocations
			// 从导入的文件中读取bean定义并注册到bean容器
			reader.loadBeanDefinitions(resource);
		}
	}

	// 对参数中所有ImportBeanDefinitionRegistrar实例,逐一调用其方法registerBeanDefinitions()
	private void loadBeanDefinitionsFromRegistrars(
			Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
		for (Map.Entry<ImportBeanDefinitionRegistrar, AnnotationMetadata> entry : 
			registrars.entrySet()) {
			entry.getKey().registerBeanDefinitions(entry.getValue(), this.registry);
		}
	}


	/**
	 * RootBeanDefinition marker subclass used to signify that a bean definition
	 * was created from a configuration class as opposed to any other configuration source.
	 * Used in bean overriding cases where it's necessary to determine whether the bean
	 * definition was created externally.
	 */
	@SuppressWarnings("serial")
	private static class ConfigurationClassBeanDefinition extends RootBeanDefinition 
		implements AnnotatedBeanDefinition {

		private final AnnotationMetadata annotationMetadata;

		private final MethodMetadata factoryMethodMetadata;

		public ConfigurationClassBeanDefinition(ConfigurationClass configClass, 
		MethodMetadata beanMethodMetadata) {
			this.annotationMetadata = configClass.getMetadata();
			this.factoryMethodMetadata = beanMethodMetadata;
			setLenientConstructorResolution(false);
		}

		public ConfigurationClassBeanDefinition(
				RootBeanDefinition original, ConfigurationClass configClass, 
				MethodMetadata beanMethodMetadata) {
			super(original);
			this.annotationMetadata = configClass.getMetadata();
			this.factoryMethodMetadata = beanMethodMetadata;
		}

		private ConfigurationClassBeanDefinition(ConfigurationClassBeanDefinition original) {
			super(original);
			this.annotationMetadata = original.annotationMetadata;
			this.factoryMethodMetadata = original.factoryMethodMetadata;
		}

		@Override
		public AnnotationMetadata getMetadata() {
			return this.annotationMetadata;
		}

		@Override
		public MethodMetadata getFactoryMethodMetadata() {
			return this.factoryMethodMetadata;
		}

		@Override
		public boolean isFactoryMethod(Method candidate) {
			return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate));
		}

		@Override
		public ConfigurationClassBeanDefinition cloneBeanDefinition() {
			return new ConfigurationClassBeanDefinition(this);
		}
	}


	/**
	 * 用于跟踪某个配置类是否需要被忽略
	 * Evaluate @Conditional annotations, tracking results and taking into
	 * account 'imported by'.
	 */
	private class TrackedConditionEvaluator {

		private final Map<ConfigurationClass, Boolean> skipped = new HashMap<ConfigurationClass, Boolean>();

		public boolean shouldSkip(ConfigurationClass configClass) {
			Boolean skip = this.skipped.get(configClass);
			if (skip == null) {
				if (configClass.isImported()) {
					boolean allSkipped = true;
					for (ConfigurationClass importedBy : configClass.getImportedBy()) {
						if (!shouldSkip(importedBy)) {
							allSkipped = false;
							break;
						}
					}
					if (allSkipped) {
						// The config classes that imported this one were all skipped, 
						// therefore we are skipped...
						skip = true;
					}
				}
				if (skip == null) {
					skip = conditionEvaluator.shouldSkip(
							configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN
							);
				}
				this.skipped.put(configClass, skip);
			}
			return skip;
		}
	}

}

相关文章

Spring 内部工具 : 配置类、Bean定义的发现和注册
Spring 基于注解的 bean 定义/注册方式总结
Spring 配置类的分类
Spring 工具类 ConfigurationClassParser 分析得到配置类
Spring 工具类 ClassPathBeanDefinitionScanner bean定义扫描
Spring BeanDefinitionRegistryPostProcessor : ConfigurationClassPostProcessor

基于 NSFW Model 色情图片识别鉴黄 后面更新视频检测 项目背景: 随着互联网的快速发展,网络上的信息量呈现出爆炸式的增长。然而,互联网上的内容良莠不齐,其中不乏一些不良信息,如色情、暴力等。这些信息对青少年的健康成长和社会风气产生了不良影响。为了净化网络环境,保护青少年免受不良信息的侵害,我国政府加大了对网络内容的监管力度。在此背景下,本项目应运而生,旨在实现对网络图片和视频的自动识别与过滤,助力构建清朗的网络空间。 项目简介: 本项目基于 NSFW(Not Safe For Work)Model,利用深度学习技术对色情图片进行识别与鉴黄。NSFW Model 是一种基于卷积神经网络(CNN)的图像识别模型,通过学习大量的色情图片和非色情图片,能够准确地判断一张图片是否含有色情内容。本项目在 NSFW Model 的基础上,进一步优化了模型结构,提高了识别的准确率和效率。 项目功能: 色情图片识别:用户上传图片后,系统会自动调用 NSFW Model 对图片进行识别,判断图片是否含有色情内容。如果含有色情内容,系统会给出相应的提示,并阻止图片的传播。 视频检测:针对网络视频,本项目采用帧提取技术,将视频分解为一帧帧图片,然后使用 NSFW Model 对这些图片进行识别。如果检测到含有色情内容的图片,系统会给出相应的提示,并阻止视频的传播。 实时监控:本项目可应用于网络直播、短视频平台等场景,实时监控画面内容,一旦检测到含有色情内容的画面,立即进行屏蔽处理,确保网络环境的纯洁。
### 如何在本地部署 NSFW 模型或服务 要在本地环境中成功部署 NSFW(不适宜工作场合内容)检测模型或服务,以下是详细的说明: #### 准备环境 为了确保能够顺利运行模型和服务,需要安装必要的依赖项。这些工具和库包括但不限于以下几类: - **Python 环境**: 推荐使用 Python 3.7 或更高版本。 - **Transformers 库**: 提供加载预训练模型的功能[^1]。 - **PyTorch/TensorFlow**: 支持深度学习框架的计算需求。 - **Pillow (PIL)**: 处理图像文件并将其转换为适合输入模型的形式。 具体命令如下所示: ```bash pip install transformers torch Pillow ``` #### 加载模型与测试 通过 Hugging Face 的 `transformers` 工具包可以直接访问已有的 NSFW 图片分类模型。例如,可以采用名为 `"Falconsai/nsfw_image_detection"` 的公开模型来完成此任务[^1]。 下面是一个简单的代码片段展示如何加载该模型并对单张图片执行预测操作: ```python from PIL import Image from transformers import pipeline def classify_nsfw(image_path): # 打开指定路径下的图片文件 img = Image.open(image_path) # 初始化 image-classification 流水线对象,并指明使用的特定模型名称 classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection") # 对传入的图片调用流水线方法得到其类别标签及其置信度分数列表形式的结果 result = classifier(img) return result if __name__ == "__main__": test_img_path = "<your_test_image>" output_results = classify_nsfw(test_img_path) print(output_results) ``` 注意替换 `<your_test_image>` 成实际存在的图片绝对或者相对地址字符串值之前再尝试运行以上脚本。 #### 构建 RESTful API 服务 如果希望进一步扩展功能至 Web 应用程序层面,则可考虑利用 Flask/Django 这样的轻量级 web 开发框架构建起支持 HTTP 请求交互的服务端接口。这里给出基于 FastAPI 实现的一个简单例子作为示范用途: ```python import uvicorn from fastapi import FastAPI, File, UploadFile from PIL import Image from io import BytesIO from typing import List from pydantic import BaseModel app = FastAPI() class Prediction(BaseModel): label: str score: float @app.post("/predict/", response_model=List[Prediction]) async def predict(file: UploadFile = File(...)): try: contents = await file.read() pil_image = Image.open(BytesIO(contents)) clf_pipeline = pipeline('image-classification', model='Falconsai/nsfw_image_detection') predictions = clf_pipeline(pil_image) formatted_preds = [{"label": pred['label'], "score": round(pred['score'], 4)} for pred in predictions] return formatted_preds except Exception as e: raise ValueError(f"Error processing uploaded file {e}") if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=8000) ``` 启动服务器之后即可向 `/predict/` 路径发送 POST 请求附带上传待分析的目标图片获取返回结果了。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值