第一种简单易懂方式:首先从springBoot启动类入口@SpringBootApplication进来
可以看到有七个组合注解,上面四个元数据注解就不多说了,接着往@SpringBootConfiguration这个注解进去可以看有一个@Configuration注解,这个注解的意思也就是表明了是一个配置类,那么也就意味着我们的@SpringBootApplication注解也是一个配置类
@ComponentScan注解用来扫描该类父包下面的所有子包,
那么以上注解讲完,剩下一个@EnableAutoConfiguration(开启自动配置)注解也就是来做我们SpringBoot自动装配干活的地方了,我们接着进去往下看
进来后发现也有四个元注解,以及@AutoConfigurationPackage这个注解的作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理。
重点来了:注意看@Import注解引入了一个AutoConfigurationImportSelector类,@Import的作用就是替代了Spring老版本的Import标签,来给我们引入其他的配置类,@Enable..注解的核心就是用@Import注解向Spring注入其他bean以及配置类。继续进去AutoConfigurationImportSelector类可以看到实现了ImportSelector接口
实现了selectImports方法,这个方法返回的是一个String[],返回的数组Spring会帮我们注入到IOC容器里面,
那么我们接着往这个方法去看,getAutoConfigurationEntry(annotationMetadata); 那么在这里的话 我们从代码往上从下说一下他都干了什么。
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取@EnableAutoConfiguration注解扫描到的要去装配的类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//这里首先是第一步过滤要自动装配的Bean,移除重复的
configurations = removeDuplicates(configurations);
// 排除需要排除的类,具体操作是通过@SpringBootApplication 注解中的 exclude、excludeName、环境属性中的spring.autoconfigure.exclude配置
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//这里是根据spring-autoconfigure-metadata.properties 中配置的规则来过滤一些不用装配进去的类
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
那么再接着讲spring-autoconfigure-metadata.properties这个配置的规则,其实都是去基于@conditional这个注解去判断是否要装配进容器。这个注解里面必须放入一个实现了Condition 这个接口的类然后实现他的matches方法,也就是matches方法返回ture则装配到容器。
接着进入getCandidateConfigurations()方法。大家可以翻译一下下面的一句信息,也就是(在 META-INFspring.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;
}
在这里就可以发现其实就是SpringBoot基于Spi定义的一个规范,让想要通过Spring容器来管理对象的类在META-INF/spring.factories 写入要装载进来的类。接着打开SpringBoot的META-INF目录可以发现要装载的类文件和我们上面讲到的过滤文件在一个目录
接着打开Spring.factories 这个文件这些以AutoConfiguration结尾的类就是要去扫描装配的类
接着继续往下走具体是在哪个方法获取到这些文件的,接着进入getCandidateConfigurations()方法里面的SpringFactoriesLoader.loadFactoryNames()方法。
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
接着往下走进入这个loadSpringFactories(classLoader)
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
//通过类加载器去扫描资源,FACTORIES_RESOURCE_LOCATION 这个常量就是META-INF/spring.factories
//也就是意味着是在这里扫描META-INF/spring.factories这个文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
第二种:源码方式入口,SpringBoot启动到底是哪一步执行的自动装配
1. 从run方法入口进来,接下来我只会说一些比较稍微重要的方法,然后就不一步一步过一些没用的方法。每个方法干的事情我都会注释在上面
首先会去调用SpringBootApplication 构造方法
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");4
//记录主方法的配置类名称
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//通过class.forname 拿到主程序这个类 servlet类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 调用SpringFactorirsLoader的loadFactoryNames方法在spring.factories这个配置文件加载应用程序上下文初始器 以ApplicationContextInitializer结尾的KEY
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//调用SpringFactorirsLoader的loadFactoryNames方法在spring.factories这个配置文件加载应用程序上下文初始器 以ApplicationListener结尾的KEY
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//获取当前main方法的堆栈跟踪 拿到当前的主类 也就是springboot启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
注意看图,
接着直接进入run方法
public ConfigurableApplicationContext run(String... args) {
// 创建一个任务执行观察器
StopWatch stopWatch = new StopWatch();
// 调用开始时间记录开始时间
stopWatch.start();
//设置应用程序上下文
ConfigurableApplicationContext context = null
//设置异常报告器
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 设置了一个名为java.awt.headless的系统属性
// 其实是想设置该应用程序,即使没有检测到显示器,也允许其启动.
//对于服务器来说,是不需要显示器的,所以要这样设置.
configureHeadlessProperty();
//获取监听器 调用getSpringFactoriesInstances 加载的是EventPublishingRunListener这个监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
//启动监听器时间
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//打印springboot标签
Banner printedBanner = printBanner(environment);
// 创建应用程序上下文
context = createApplicationContext();
//去spring.factories这个配置文件读取ExceptionReporter结尾的全限定名实例化
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//准备上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新上下文,完成spring的容器初始化 加载Bean
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
//返回上下文对象--> Spring容器对象
return context;
}
接着进入prepareContext方法在这里面有一个Load方法很重要,接着我们进去看看
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置环境对象
context.setEnvironment(environment);
//设置应用程序上下文
postProcessApplicationContext(context);
//上下文的一些初始化操作
applyInitializers(context);
//发布对应的监听事件
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans 创建一个Bean工厂 DefaultListableBeanFactory类型的工厂
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//注册一些实体对象
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources 将配置类加载到当前集合
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
//加载有Component注解配置的类
load(context, sources.toArray(new Object[0]));
//事件发布
listeners.contextLoaded(context);
}
其实这里只会去注册我们的启动类,判断它是否带Component注解,了解springbootApplicaiton注解的应该都知道在Configuration注解里面就是拿Component注解修饰的
接着进入我们spring的refresh()方法
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing. 设置启动时间 以及一些属性源的初始化
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
//获取当前工厂对象
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//配置当前工厂的一些属性以及后处理器
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
//ApplicationContext 实现中注册特殊的 BeanPostProcessors 等。 此时Bean还没有实例化
postProcessBeanFactory(beanFactory);
//启动执行所有的BeanFactoryPostProcessor
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
注意invokeBeanFactoryPostProcessors(beanFactory); 这个方法,是去实现我们整个BeanFactory的一些处理器,springboot的自动装配扩展也就是在这里进行的。
进去这个方法会发现有100多行的代码,其实你仔细看他们具体做的事情都是一样的。
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();
// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}
// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}
else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);
// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);
// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();
}
在这里面会去循环执行我们的beanFactoryPostProcessors,注意这个PostProcessor其实我们是搜不到的,再接着往下走invoke发现会进入ConfigurationClassPostProcessor这个类
ConfigurationClassPostProcessor对象的创建和方法执行的断点如下:
this.refreshContext(context);--> AbstractApplicationContext.refresh() --> invokeBeanFactoryPostProcessors()>PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()->invokeBeanDefinitionRegistryPostProcessors()
进入ConfigurationClassPostProcesso的 processConfigBeanDefinitions这个方法
注意看圈中的这句话,如果没有找到 @Configuration 类,则立即返回,这个方法非常重要,在这个方法也就是会去解析我们以@Configuration注解标识的类,接着进入parser(解析)
接着进入doProcessConfigurationClass这个方法仔细看这个就是最终干活的方法,处理我们的所有注解 可以一步一步看注释。这里用到的就是递归循环一个类去拿出所有的注解,因为我们不知道一个类它里面是否有多少注解
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 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.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), 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;
}
注意这一段代码块
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);
处理我们import注解导入的类,运行完doProcessConfigurationClass这个方法其实接下来就会去执行我们的自动装配了,返回到pars方法
注意这里标识的方法,
可以看到我们自动装配的类AutoConfigurationImportSelector里面有一个内部类也有一个这样的方法,里面调用了getAutoConfigurationEntry(getAutoConfigurationMetadata(),annotationMetadata);
这个不就是我们自动装配的方法吗?
那么以上就是我们SpringBoot启动中自动装配的源码流程了,看源码虽然很头疼,但是多看几遍就会了,一定要注意代码上的注释,看源码不是为了什么,是为了更方便的去理解框架。