栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring源码(二)——配置类解析以及扫描Bean对象

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring源码(二)——配置类解析以及扫描Bean对象

Spring源码(二)——配置类解析以及扫描Bean对象

上一篇Spring源码(一)——Bean生命周期源码解读文章,讲了下Bean的生命周期以及ApplicationContext构造方法的大致逻辑,其中最核心的是refresh方法,但是里面有些方法没有细说。比如invokeBeanFactoryPostProcessors(beanFactory),registerBeanPostProcessors(beanFactory),
finishBeanFactoryInitialization(beanFactory)
这三个方法 是refresh 中想当重要的方法。今天我们就详细看看invokeBeanFactoryPostProcessors这个方法

invokeBeanFactoryPostProcessors

这个方法直译过来就是:执行BeanFactory的后置处理器,注意这个后置处理器是Spring自带的后置处理器,而不是程序员编写的PostProcessor后置处理器。我们上一篇文章中说了,在AnnotationConfigApplicationContext的构造方法中,Spring去创建了一个AnnotationBeanDefinition读取器(AnnotationBeanDefinitionReader),这个读取器的构造方法中会向BeanFactory中注册spring自带的BeanPostProcessor。

在执行这些BeanPostProcessor中,spring会去解析你传入的配置类以及扫码方法。

解析配置类

我们下钻看看invokeBeanFactoryPostProcessors这个方法。

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
		//主要就是这个方法。getBeanFactoryPostProcessors这个方法是获取容器中的已经存在的BeanFactoryPostProcessor对象。
		PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

		//这块忽略,不重要
		if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
			beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
			beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
		}
	}

继续想下钻 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,能看到这样一段代码。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		//.....省略的代码
		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);
		Set candidates = new linkedHashSet<>(configCandidates);
		Set alreadyParsed = new HashSet<>(configCandidates.size());
		do {
			parser.parse(candidates);
			//.....省略的代码
		}while (!candidates.isEmpty());
		//.....省略的代码
}

这里面能看到 parser.parse(candidates) 这样一个解析配置类的方法。
我们继续下钻看看parse 是什么逻辑。

public void parse(Set configCandidates) {
		this.deferredimportSelectors = new linkedList<>();

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

		processDeferredimportSelectors();
	}

这段代码能看出来,这个方法是遍历所有程序员在AnontationConfigApplicationContext启动时,传入的配置类。然后根据配置类的BeanDefinition的类型判断走那个parse方法。由于我们的SysConfig.class 是一个注解配置类,所以我们会进入 processConfigurationClass方法。

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
		//.....省略的代码
		// 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);
}

然后下钻doProcessConfigurationClass这个方法。

@Nullable
	protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
			throws IOException {

		// 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.warn("Ignoring @PropertySource annotation on [" + sourceClass.getmetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}

		// Process any @ComponentScan annotations
		Set 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 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 readerClass = importResource.getClass("reader");
			for (String resource : resources) {
				String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
				configClass.addimportedResource(resolvedResource, readerClass);
			}
		}

		// Process individual @Bean methods
		Set 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(configClass, sourceClass);

这个方法查看配置类的内部类是否是配置类 如果是再次调用processConfigurationClass。也就是遍历判断配置类。

下一段

// 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.warn("Ignoring @PropertySource annotation on [" + sourceClass.getmetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}

这个方法是获取配置类上的@PropertySources 注解 ,将propertis文件的键值对加载到容器的环境中。

在下一段 重点来了!!!!!:

// Process any @ComponentScan annotations
		Set 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 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());
					}
				}
			}
		}

注意:

Set componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getmetadata(), ComponentScans.class, ComponentScan.class);

ComponentScan.class 这个类是不是非常熟悉。是不是就是我们的扫描包路径的一个注解?@ComponentScan({“com.service”,“com.Aspect”}) 是不是就是这个?。
spring去遍历@Component这个注解配置的扫描路径,去执行下面这个扫描的方法(扫描的具体类容后面在说)。

Set scannedBeanDefinitions =
   					this.componentScanParser.parse(componentScan, sourceClass.getmetadata().getClassName());

这个方法里面就会去扫描配置的路径,扫描所有添加了@Component注解的类,将类信息封装成BeanDefinition中。这里是不会创建Bean对象的,只是存储类信息,到后面将所有类信息一起实例化。

下一段:

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

这个方法就是遍历扫描出来的所有@Component注释的类的BeanDefinition,将这个BeanDefinition当作配置类,用迭代的方式继续执行parse方法。继续解析。

继续看下一段

// Process any @import annotations
processimports(configClass, sourceClass, getimports(sourceClass), true);

这个方法是处理所有@import注解。进入方法

private void processimports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection importCandidates, boolean checkForCircularimports) {
//.....省略的代码
				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 = BeanUtils.instantiateClass(candidateClass, importSelector.class);
						ParserStrategyUtils.invokeAwareMethods(
								selector, this.environment, this.resourceLoader, this.registry);
						if (this.deferredimportSelectors != null && selector instanceof DeferredimportSelector) {
							this.deferredimportSelectors.add(
									new DeferredimportSelectorHolder(configClass, (DeferredimportSelector) selector));
						}
						else {
							String[] importClassNames = selector.selectimports(currentSourceClass.getmetadata());
							Collection importSourceClasses = asSourceClasses(importClassNames);
							processimports(configClass, currentSourceClass, importSourceClasses, 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 =
								BeanUtils.instantiateClass(candidateClass, importBeanDefinitionRegistrar.class);
						ParserStrategyUtils.invokeAwareMethods(
								registrar, 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));
					}
				}
//.....省略的代码
}

这个方法也会去判断导入的类是不是配置类

  1. 如果导入的类实现了importSelector类型
    a. 如果是DeferredimportSelector类型,表示推迟的importSelector,它会在当前配置类所属的批次中所有配置类都解析完了之后执行
    b. 如果是普通的importSelector类型,迭代的方法执行processimports方法。
  2. 如果导入的类是importBeanDefinitionRegistrar类型,回将importBeanDefinitionRegistrar配置到configClass中。
  3. 如果都不是 就是一个普通的配置类,调用processConfigurationClass 继续解析这个新配置类。

继续下一段代码:

AnnotationAttributes importResource =
				AnnotationConfigUtils.attributesFor(sourceClass.getmetadata(), importResource.class);
		if (importResource != null) {
			String[] resources = importResource.getStringArray("locations");
			Class readerClass = importResource.getClass("reader");
			for (String resource : resources) {
				String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
				configClass.addimportedResource(resolvedResource, readerClass);
			}
		}

这一点判断importResource.class注解。将导入的源文件路径添加到配置类的importedResource属性上。 注意这个类不会将导入的当作配置类。
再下一段

Set beanMethods = retrieveBeanMethodmetadata(sourceClass);
		for (Methodmetadata methodmetadata : beanMethods) {
			configClass.addBeanMethod(new BeanMethod(methodmetadata, configClass));
		}

retrieveBeanMethodmetadata方法的类容如下:

private Set retrieveBeanMethodmetadata(SourceClass sourceClass) {
		Annotationmetadata original = sourceClass.getmetadata();
		Set 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 asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
				if (asmMethods.size() >= beanMethods.size()) {
					Set 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;
	}

这个方法就是将配置类以及它的父类的所有使用了@Bean注解的方法。然后将这些方法添加到配置的的一个Method中

然后在下一个方法:

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

这个方法与上一段代码是类似的,上一段是将父类中被@Bean注解的方法封装假如到配置类的BeanMethod中,这一段就是将配置类实现的接口中,被@Bean注解的方法封装假如到配置类的BeanMethod中。
最后一段代码:

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

这段代码很简单,判断当前配置类是否有父类,如果有将这个父类返回,然后继续执行这个doProcessConfigurationClass方法解析这个父类。

解析配置类总结

  1. 判断配置类的内部类是不是配置类,是就解析内部类。
  2. 判断是否有@ComponentScan,有的话扫描所有@Component,@Name,@ManagedBean。如果是@Component注解的类会将这个类当作一个配置类进行解析。
  3. 配置类上是否有@imports,获取导入的类。
    a. 如果导入的类是importSelector.class类型,并且是DeferredimportSelector.class加入延迟解析数据。如果只是一个importSelector.继续解析这个类
    b.如果当前类是importBeanDefinitionRegistrar类型,将importBeanDefinitionRegistrar假如到配置类中。
    c. 就是一个普通类型,当成新的配置类解析。
  4. 判断配置类上是否有@importResource注解,将注解执行的文件加载到配置文件中。
  5. 获取配置类中所有@Bean注解的方法,添加到配置类的beanMethod中。
  6. 判断配置类的接口类,将其中的@Bean注解的方法添加到配置类中。
  7. 判断配置类是否有父类,如果有将父类当成一个新的配置类,迭代方式解析。
思维导图:

上面就是我们的spring解析配置类的思路。
下面我们继续详细的看看@ComponentScan的扫描方法。

扫描Bean对象(doScan)
protected Set doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set beanDefinitions = new linkedHashSet<>();
		for (String basePackage : basePackages) {
			Set candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				Scopemetadata scopemetadata = this.scopemetadataResolver.resolveScopemetadata(candidate);
				candidate.setScope(scopemetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition) {
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopemetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}

在从findCandidateComponents继续下钻,能看到scanCandidateComponents这样一个方法。

private Set scanCandidateComponents(String basePackage) {
		Set candidates = new linkedHashSet<>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolvebasePackage(basePackage) + '/' + this.resourcePattern;
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				if (resource.isReadable()) {
					try {
						metadataReader metadataReader = getmetadataReaderFactory().getmetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setSource(resource);
							if (isCandidateComponent(sbd)) {
								if (debugEnabled) {
									logger.debug("Identified candidate component class: " + resource);
								}
								candidates.add(sbd);
							}
							else {
								if (debugEnabled) {
									logger.debug("Ignored because not a concrete top-level class: " + resource);
								}
							}
						}
						else {
							if (traceEnabled) {
								logger.trace("Ignored because not matching any filter: " + resource);
							}
						}
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
				else {
					if (traceEnabled) {
						logger.trace("Ignored because not readable: " + resource);
					}
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}

这个方法传入了一个basePackages的参数 ,这个就是扫描的路径,spring首先会将要扫描的路径进行拼接,classpath*:com/service*.class 这种类型,如下代码。

String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolvebasePackage(basePackage) + '/' + this.resourcePattern;

获取这个路径下所有所有的class文件。

Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

遍历这些文件判断是否能需要封装成BeanDefinition中,加入到容器中。

metadataReader metadataReader = getmetadataReaderFactory().getmetadataReader(resource);
		if (isCandidateComponent(metadataReader)) {
			ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
			sbd.setSource(resource);
			if (isCandidateComponent(sbd)) {
				if (debugEnabled) {
					logger.debug("Identified candidate component class: " + resource);
				}
				candidates.add(sbd);
			}
		}

这个方法会判断是否使用这个class:isCandidateComponent(metadataReader metadataReader),这个允许@Component能注册,另外还有@ManagedBean (javax.annotation.ManagedBean),@Named(javax.inject.Named)这些注解的类生成BeanDefinition.
然后在通过isCandidateComponent(AnnotatedBeanDefinition beanDefinition),判断是否假如配置类中。
我们在来看看默认生成BeanName的方法,

public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                        Character.isUpperCase(name.charAt(0))){
            return name;
        }
        char chars[] = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }

这个方法中 能看出 如果class的类名前两个数字是大写,则直接返回class名称,否则返回首字母小写的类名称做BeanName,当然如果设置了名称肯定用设置的名称。

然后调用AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);方法给BeanDefinition设置初始值。

AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);

最后将BeanDefinition注册到容器中去

registerBeanDefinition(definitionHolder, this.registry);

扫描的流程图

好了以上就是这个文章的所有内容了。




Thankfulness is the quickest path to joy.
懂得感恩的人,更容易收获快乐。
                                               ;————摘自有道翻译

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/603168.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号