docs/Spring全家桶/Spring源码分析/Spring启动流程/Spring启动流程(三):包的扫描流程.md
applicationContext ĴУǷ applicationContext Ĵ̣ڱУǽ spring νаɨġ
AnnotationConfigApplicationContext Ĺ췽
public AnnotationConfigApplicationContext(String... basePackages) {
this();
//Դİɨ裬ɨɺõһ BeanDefinition ļ
scan(basePackages);
refresh();
}
ǽĿ scan(basePackages); ϣ÷
AnnotationConfigApplicationContext#scan
public void scan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
// scannerthis()д
this.scanner.scan(basePackages);
}
ؼ this.scanner.scan(basePackages); scanner this() дĶ
public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
// scanner ﴴ
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
٣ǶԲҪķصעɨḶ́
AnnotationConfigApplicationContext#AnnotationConfigApplicationContext(String...)
|-AnnotationConfigApplicationContext#scan
|-ClassPathBeanDefinitionScanner#scan
|-ClassPathBeanDefinitionScanner#doScan
ClassPathBeanDefinitionScanner#doScan £
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
//Ҫɨİ·
for (String basePackage : basePackages) {
//ȡзBeanDefinition
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
//BeanDefinitionScope
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
//鿴ǷǷָbeanƣûָʹĸСд
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
//ifǴlazyAutowireDependencyOninitMethodenforceInitMethoddestroyMethod
// enforceDestroyMethodPrimaryRoleDescriptionЩ
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations(
(AnnotatedBeanDefinition) candidate);
}
//beanǷ
if (checkCandidate(beanName, candidate)) {
//ְװһ
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
//scopeǷδд
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(
scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
//ע beanDefinition
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
δɵĹܺˣ¼£
BeanDefinition Ҳ spring Ҫ֮һ BeanDefinition ķɲο spring ֮ BeanDefinition
ҪIJ
һҪ Set<BeanDefinition> candidates = findCandidateComponents(basePackage);ǸȥִУɶԲҪ÷ĵ£
AnnotationConfigApplicationContext#AnnotationConfigApplicationContext(String...)
|-AnnotationConfigApplicationContext#scan
|-ClassPathBeanDefinitionScanner#scan
|-ClassPathBeanDefinitionScanner#doScan
|-ClassPathScanningCandidateComponentProvider#findCandidateComponents
|-ClassPathScanningCandidateComponentProvider#scanCandidateComponents
յõ ClassPathScanningCandidateComponentProvider#scanCandidateComponents (ɾ)
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
//װɨ·װɺָʽclasspath*:org/springframework/learn/demo01/**/*.class
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
//·ȡԴɨ·µĵclassļõ Resource
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
for (Resource resource : resources) {
if (resource.isReadable()) {
//ԴȡԴMetadataReader
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// £
// 1\. ǷҪʼΪspring beanǷ @Component@Serviceע
// 2\. 鿴Ƿ@Conditionalһϵеע⣬ȻǷעBean
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
candidates.add(sbd);
}
}
}
}
return candidates;
}
Կϴ£
Ǿϴз
һûɶ÷һַƴ滻 org.springframework.learn.demo01 תΪ classpath*:org/springframework/learn/demo01/**/*.classشһУ
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
õɨ·ǽɨˡspring ɨʱɨ·µ class ļɨȻװ Resource
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
룬ͬأǶԲҪķֻã
AnnotationConfigApplicationContext#AnnotationConfigApplicationContext(String...)
|-AnnotationConfigApplicationContext#scan
|-ClassPathBeanDefinitionScanner#scan
|-ClassPathBeanDefinitionScanner#doScan
|-ClassPathScanningCandidateComponentProvider#findCandidateComponents
|-ClassPathScanningCandidateComponentProvider#scanCandidateComponents
|- GenericApplicationContext#getResources
|-AbstractApplicationContext#getResources
|-PathMatchingResourcePatternResolver#getResources
|-PathMatchingResourcePatternResolver#findPathMatchingResources
ǽۼ PathMatchingResourcePatternResolver#findPathMatchingResources:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// locationPattern classpath*:org/springframework/learn/demo01/**/*.class
// rootDirPath classpath*:org/springframework/learn/demo01/
String rootDirPath = determineRootDir(locationPattern);
// subPattern **/*.class
String subPattern = locationPattern.substring(rootDirPath.length());
// ﷵص Resource rootDirPath ľ·(urlʾ)
// URL [file:/xxx/spring-learn/build/classes/java/main/org/springframework/learn/demo01/]
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
// vfs Դ
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate
.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
// jarļ
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
// ļ·µļ
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
return result.toArray(new Resource[0]);
}
ָͨĴ£
spring ν pattrn תΪ url ·ģǸ룺
|-PathMatchingResourcePatternResolver#getResources
|-PathMatchingResourcePatternResolver#findAllClassPathResources
|-PathMatchingResourcePatternResolver#doFindAllClassPathResources
մ뵽 PathMatchingResourcePatternResolver#doFindAllClassPathResources:
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
// pathӦurl
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) :
ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
// urlתΪResourceӵ
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
// urlתΪResource
protected Resource convertClassLoaderURL(URL url) {
return new UrlResource(url);
}
ʱ path Ϊ org/springframework/learn/demo01/Ӵ֪յ java ClassLoader ȡ path Ӧ urlȻ url תΪ Resource ӵвء
õľ·֮¾Ƕ·бõ class ļˡٻص PathMatchingResourcePatternResolver#findPathMatchingResourcesspring ɨʱݴ url ͣɨ 3 ط
vfs ע˵ "URL protocol for a general JBoss VFS resource"ͨ JBoss VFS Դ URL Э飬ﲻĿ jar Ҫɨ jar е·ͻʹ jar ɨ跽ʽ class ļңڵʱdemo01 ʹļʽɨģصļɨ跽ʽ jar ɨģȤСо¡
Ǹ findPathMatchingResources
|-PathMatchingResourcePatternResolver#findPathMatchingResources
|-PathMatchingResourcePatternResolver#doFindPathMatchingFileResources
|-PathMatchingResourcePatternResolver#doFindMatchingFileSystemResources
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir,
String subPattern) throws IOException {
// ļ
Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
for (File file : matchingFiles) {
result.add(new FileSystemResource(file));
}
return result;
}
PathMatchingResourcePatternResolver#doFindMatchingFileSystemResources Уspring ɨ赽 File תΪ FileSystemResource 棬ĵڶ Resource (ǰΪ UrlResourceΪ FileSystemResource).
صע Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern); spring ļҵģ
|-PathMatchingResourcePatternResolver#findPathMatchingResources
|-PathMatchingResourcePatternResolver#doFindPathMatchingFileResources
|-PathMatchingResourcePatternResolver#doFindMatchingFileSystemResources
|-PathMatchingResourcePatternResolver#retrieveMatchingFiles
|-PathMatchingResourcePatternResolver#doRetrieveMatchingFiles
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result)
throws IOException {
for (File content : listDirectory(dir)) {
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
}
else {
// ļУݹ
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
// ļļ·
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
ϴȽϼƽļķʽһġ
ֵһǣgetPathMatcher().match(fullPattern, currPath) յõ AntPathMatcher#doMatchһ ant ·ƥ֤·д *紫 pattern /xxx/spring-framework/spring-learn/build/classes/java/main/org/springframework/learn/demo01/**/*.classʾƥ /xxx/spring-framework/spring-learn/build/classes/java/main/org/springframework/learn/demo01/ ļ.class ļβļǰ path /xxx/spring-framework/spring-learn/build/classes/java/main/org/springframework/learn/demo01/BeanObj2.classȻƥ䡣 AntPathMatcher#doMatch νƥģͲչˡ
ϲ裬ڵõ class ļӦ Resource .
Resource תΪ BeanDefinition
ClassPathScanningCandidateComponentProvider#scanCandidateComponents
// resource õ MetadataReader
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// £
// 1\. ǷҪʼΪspring beanǷ @Component@Serviceע
// 2\. 鿴Ƿ@Conditionalһϵеע⣬ȻǷעBean
if (isCandidateComponent(metadataReader)) {
// metadataReader תΪ ScannedGenericBeanDefinitionҲBeanDefinitionеһԱ
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
...
}
MetadataReader Ļȡ
|-ClassPathScanningCandidateComponentProvider#scanCandidateComponents
|-CachingMetadataReaderFactory#getMetadataReader
|-SimpleMetadataReaderFactory#getMetadataReader(Resource)
|-SimpleMetadataReader#SimpleMetadataReader
е SimpleMetadataReader Ĺ췽:
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
SimpleAnnotationMetadataReadingVisitor visitor
= new SimpleAnnotationMetadataReadingVisitor(classLoader);
// classļĶȡ
getClassReader(resource).accept(visitor, PARSING_OPTIONS);
this.resource = resource;
this.annotationMetadata = visitor.getMetadata();
}
ٽһ٣ class ļĶȡ ClassReader ࣺ
ʹ asm ȡ class ļȽϸӣͲˡ
һֱҶΪ spring ͨȡϢģ֪ԭ spring ͨ asm ֱӶȡ class ļȡϢ
µõ MetadataReader Ľ
صע annotations ԣһ annotations mappings``annotations Ϊ @Service``mappings һ飬Ϊ
0-@Service
1-@Component
2-@Index
annotations ˲² BeanObj1 ϵע⣺
mappings ɶҲò²⣬ҲԴעзһЩߣ
@Service @Component ע⣬@Component @Indexed߶ mappings У⿴רע֮ϵעģˣҾ͵ܰɣע⣺mappings ݺҪ
isCandidateComponent(MetadataReader)жǷҪʵΪ spring beanһУǵõ basePackage **** MetadataReader ļע****ЩDzǶҪת spring beanйܵ spring أ isCandidateComponent(MetadataReader) Ĺˡϻ˵ϴ룺
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
// ʡԲִ
for (TypeFilter tf : this.includeFilters) {
// жǷҪйܵspring
if (tf.match(metadataReader, getMetadataReaderFactory())) {
// жǷ@Conditionalһϵеע
return isConditionMatch(metadataReader);
}
}
return false;
}
Ҫжϣ
@Conditional һϵеעһжϡ
spring У spring bean עкܶ࣬ @Component``@Repository``@Service``@Controller``@ConfigurationԼдעֻ࣬ҪЩע⣬
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Component @Service @Repository ֮һ
@Component
public @interface MySpringBean {
...
}
ܱ spring ʶ spring ṩע⣨@Component``@Repository ȣжDz spring bean ʱֻҪ
if(annotation == Component.class || annotation == Repository.class) {
...
}
жϾˡԶע @MySpringBeanspring ô֪ spring bean أǶ @MySpringBean ʱһҪ @Component @Service @Repository ֮һܱ spring ʶʲôأǸ AbstractTypeHierarchyTraversingFilter#match(MetadataReader, MetadataReaderFactory)ǶԲҪĴֻ
|-ClassPathScanningCandidateComponentProvider#isCandidateComponent(MetadataReader)
|-AbstractTypeHierarchyTraversingFilter#match(MetadataReader, MetadataReaderFactory)
|-AnnotationTypeFilter#matchSelf
յ AnnotationTypeFilter#matchSelf:
@Override
protected boolean matchSelf(MetadataReader metadataReader) {
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
// annotationType @Component
return metadata.hasAnnotation(this.annotationType.getName()) ||
(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}
ؼˣ
metadata.hasAnnotation(this.annotationType.getName())
this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())
ȿ metadata.hasAnnotation(this.annotationType.getName()) ıȽϣ
// AnnotationMetadata#hasAnnotation
default boolean hasAnnotation(String annotationName) {
return getAnnotations().isDirectlyPresent(annotationName);
}
getAnnotations() õĽ
mappings
0-@Service
1-@Component
2-@Index
ʵǰõ MetadataReader ݣ
ȥ isDirectlyPresent ж annotations mappings ûг @Component:
private boolean isPresent(Object requiredType, boolean directOnly) {
// ж annotations ûг @Component
for (MergedAnnotation<?> annotation : this.annotations) {
Class<? extends Annotation> type = annotation.getType();
if (type == requiredType || type.getName().equals(requiredType)) {
return true;
}
}
if (!directOnly) {
// ж mappings ûг @Component
for (AnnotationTypeMappings mappings : this.mappings) {
for (int i = 1; i < mappings.size(); i++) {
AnnotationTypeMapping mapping = mappings.get(i);
if (isMappingForType(mapping, requiredType)) {
return true;
}
}
}
}
return false;
}
this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())鿴ã
|-AnnotationTypeFilter#matchSelf
|-AnnotationMetadata#hasMetaAnnotation
|-MergedAnnotationsCollection#get(String, Predicate)
|-MergedAnnotationsCollection#get(String, Predicate, MergedAnnotationSelector)
|-MergedAnnotationsCollection#find
յIJҷ MergedAnnotationsCollection#find:
private <A extends Annotation> MergedAnnotation<A> find(Object requiredType,
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
@Nullable MergedAnnotationSelector<A> selector) {
MergedAnnotation<A> result = null;
for (int i = 0; i < this.annotations.length; i++) {
MergedAnnotation<?> root = this.annotations[i];
AnnotationTypeMappings mappings = this.mappings[i];
// mappings mappings
for (int mappingIndex = 0; mappingIndex < mappings.size(); mappingIndex++) {
AnnotationTypeMapping mapping = mappings.get(mappingIndex);
if (!isMappingForType(mapping, requiredType)) {
continue;
}
// ҵ @Component ע
MergedAnnotation<A> candidate = (mappingIndex == 0
? (MergedAnnotation<A>) root
: TypeMappedAnnotation.createIfPossible(mapping, root, IntrospectionFailureLogger.INFO));
if (candidate != null && (predicate == null || predicate.test(candidate))) {
if (selector.isBestCandidate(candidate)) {
return candidate;
}
result = (result != null ? selector.select(result, candidate) : candidate);
}
}
}
return result;
}
Կҷʽ metadata.hasAnnotation(this.annotationType.getName()) ߶ơ
Ͼ spring жǷ @Service``@Component עˡ
java УעDzܼ̳еģ
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Base {
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Child extends Base {
}
java вģspring Dzעעķʽʵڼ̳еĹܡ
ClassPathScanningCandidateComponentProvider#isConditionMatch ʵϣжǷ @Conditional עģʶΪ spring beanյõ ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata, ConfigurationCondition.ConfigurationPhase):
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// ʡһЩ
// õ condition
List<Condition> conditions = new ArrayList<>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
}
AnnotationAwareOrderComparator.sort(conditions);
// ж condition Ƿ
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if ((requiredPhase == null || requiredPhase == phase)
// ж condition Ƿһͷtrue
&& !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
// ͨȡ Condition
private Condition getCondition(String conditionClassName, @Nullable ClassLoader classloader) {
Class<?> conditionClass = ClassUtils.resolveClassName(conditionClassName, classloader);
return (Condition) BeanUtils.instantiateClass(conditionClass);
}
£
condition.matches() жǷMetadataReader õ ScannedGenericBeanDefinitionһĸֵ ScannedGenericBeanDefinition Ĺ췽ˣ
ScannedGenericBeanDefinition#ScannedGenericBeanDefinition
public ScannedGenericBeanDefinition(MetadataReader metadataReader) {
Assert.notNull(metadataReader, "MetadataReader must not be null");
this.metadata = metadataReader.getAnnotationMetadata();
setBeanClassName(this.metadata.getClassName());
}
ȽϼͲˡ
ǧգڵõ beanDefinitionʱ beanDefinition ḻǽһչ beanDefinition ϢˡЩϢ bean``bean``@Lazy ע⡢@Primary ע⡢@DependsOn עȣ£
public abstract class AnnotationConfigUtils {
...
/**
* һḻ BeanDefinition
*/
static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd,
AnnotatedTypeMetadata metadata) {
// @Lazy
AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
else if (abd.getMetadata() != metadata) {
lazy = attributesFor(abd.getMetadata(), Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
}
// @Primary
if (metadata.isAnnotated(Primary.class.getName())) {
abd.setPrimary(true);
}
// @DependsOn
AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
if (dependsOn != null) {
abd.setDependsOn(dependsOn.getStringArray("value"));
}
// @Role
AnnotationAttributes role = attributesFor(metadata, Role.class);
if (role != null) {
abd.setRole(role.getNumber("value").intValue());
}
// @Description
AnnotationAttributes description = attributesFor(metadata, Description.class);
if (description != null) {
abd.setDescription(description.getString("value"));
}
}
}
registerBeanDefinition(definitionHolder, this.registry): BeanDefinition beanFactoryBeanDefinition beanFactory IJȽϼؼĴ£
|-ClassPathBeanDefinitionScanner#registerBeanDefinition
|-BeanDefinitionReaderUtils#registerBeanDefinition
|-GenericApplicationContext#registerBeanDefinition
|-DefaultListableBeanFactory#registerBeanDefinition
DefaultListableBeanFactory#registerBeanDefinition
this.beanDefinitionMap.put(beanName, beanDefinition);
ClassPathBeanDefinitionScanner#registerBeanDefinition DefaultListableBeanFactory#registerBeanDefinitionȻһЩƣɲҵؼĴ롣
ˣϵ class ļ spring ɨ裬ڱ BeanDefinition BeanFactory ˡ
ıȽϳҪ spring ɨ·õ beanDefinition Ḷ́Ҫ£
ResourceResouce õ· class ļ ResouceResouce ͨ asm õ MetadataReaderע⣺ MetadataReader class ļ MetadataReaderMetadataReader ҵҪ spring йܵ MetadataReaderתΪ ScannedGenericBeanDefinition``ScannedGenericBeanDefinition Ϊ BeanDefinition ࣻScannedGenericBeanDefinition ϢBeanDefinition ӵ BeanFactoryˣתΪ BeanDefinition ɡ
Ļֵעĵط
@Component``@Service ǿԶעԭõ BeanDefinition ž spring ijʼˣƪټ
ԭӣhttps://my.oschina.net/funcy/blog/4614071 ߸ˮƽд֮ӭָԭףҵתϵȨҵתע