Spring - @Import注解
文章目录
- 基本用法
- 源码分析
- ConfigurationClassPostProcessor
- ConfigurationClass
- SourceClass
- getImports
- processImports
- 处理 ImportSelector
- ImportSelector 接口
- DeferredImportSelector
- 处理 ImportBeanDefinitionRegistrar
- ImportBeanDefinitionRegistrar 接口
- 处理Configuration
本文源码基于spring-context-5.3.36 版本
基本用法
- 直接填class数组方式
@Import注解填入要导入的类的Class即可
@Import({ A.class , B.class... })
@Configuration
public class TestDemo {}
- ImportSelector方式
实现ImportSelector接口,selectImport返回需要导入的类的全限定名称
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {return new String[0];}
}
然后@Import中指定ImportSelector的实现类的Class
- ImportBeanDefinitionRegistrar方式
实现ImportBeanDefinitionRegistrar接口
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {// 通过 BeanDefinitionRegistry 构造 BeanDefinition 并进行注册}
}
然后@Import中指定ImportBeanDefinitionRegistrar的实现类的Class
总结
- 以上三种用法方式皆可混合在一个@Import中使用,注意第一种和第二种都是以全类名的方式注册,而第三中可自定义名称
- 第一种最简单,第二种较简单,第三种需要操作BeanDefinition,较为复杂
- @Import注解不一定非要和@Configuration搭配使用,也可以和@Component等注解使用,效果一样
@Import({ A.class , B.class... })
@Component
public class TestDemo {}
源码分析
入口
BeanDefinitionRegistryPostProcessor执行阶段生效
例如 ConfigurationClassPostProcessor
ConfigurationClassPostProcessor
ConfigurationClass
代表定义的@Configuration修饰的类,包含一些bean方法。配置加载都有一个解析过程,对ConfigurationClass的解析就是由ConfigurationClassParser#parse
方法完成的,它会解析每个配置类上的配置,包括@Import注解这个配置
public void parse(Set<BeanDefinitionHolder> configCandidates) {// 拿到所有的BeanDefinitionfor (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) {...}}this.deferredImportSelectorHandler.process();
}
SourceClass
org.springframework.context.annotation.ConfigurationClassParser.SourceClass
主要用于处理和解析配置类的源信息
private class SourceClass implements Ordered {// Class或者MetadataReaderprivate final Object source;// 注解元数据private final AnnotationMetadata metadata;
}
主要作用如下:
- 表示源类:SourceClass 代表 Spring 配置类中的一个类,它封装了获取该类相关的源信息的方法,比如类的名称、注解、方法等。
- 提供元信息:SourceClass 提供了一些元数据的方法,帮助开发者获取类的详细信息,比如父类、实现的接口、注解信息等。
getImports
SourceClass可以简单的理解为java里的Class,同样,SourceClass可以代表普通类,也可以代表注解。同时SourceClass携带有一些注解元数据信息。其实getImports方法的过程就和根据反射获取一个类上的所有注解(包括修饰注解的注解)这个过程差不多
使用深度优先遍历
- 首先定义一个visited记录已经访问过的 SourceClass
- 对于每个访问的 SourceClass ,如果它被
@Import
注解修饰,则获取@Import
注解的属性值
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {Set<SourceClass> imports = new LinkedHashSet<>();Set<SourceClass> visited = new LinkedHashSet<>();collectImports(sourceClass, imports, visited);return imports;
}
调用的collectImports()
方法是一个递归操作,从第一个SourceClass开始,获取其所有注解,然后调用collectImports递归进行收集。因为@Import可能放在注解上形成复合注解
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)throws IOException {if (visited.add(sourceClass)) {for (SourceClass annotation : sourceClass.getAnnotations()) {String annName = annotation.getMetadata().getClassName();if (!annName.equals(Import.class.getName())) {collectImports(annotation, imports, visited);}}imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));}
}
最后返回的imports就是从根SourceClass开始遍历的所有@Import注解的值,这个值是一个java中Class的集合
processImports
getImports的返回值作为processImports方法的第三个参数
/*** * @param configClass 配置类,一般是@Configuration修饰的类* @param currentSourceClass 扫描@Import注解的根节点* @param importCandidates currentSourceClass上携带的所有@Import注解的属性值* @param exclusionFilter 用于排除* @param checkForCircularImports 是否检查@Import形成环的情况*/
private void processImports(ConfigurationClass configClass,SourceClass currentSourceClass,Collection<SourceClass> importCandidates,Predicate<String> exclusionFilter,boolean checkForCircularImports) {if (importCandidates.isEmpty()) {return;}// 检测可能存在的循环@Import情况if (checkForCircularImports && isChainedImportOnStack(configClass)) {this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));} else {// 下面代码保留了整体结构,省略了不重要的细节this.importStack.push(configClass);try {for (SourceClass candidate : importCandidates) {// 一些if/else判断if (candidate.isAssignable(ImportSelector.class)) {...} else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {...} else {// 既不是ImportSelector也不是ImportBeanDefinitionRegistrar// 当作@Configuration类处理this.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());// 继续走处理配置类的流程,会继续进行processImports方法processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);}}} catch (BeanDefinitionStoreException e) {// 省略}finally {this.importStack.pop();}}
}
处理 ImportSelector
如果是ImportSelector
处理细节如下所示
// 实例化ImportSelector对象
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);
}// 区分是否是DeferredImportSelector
if (selector instanceof DeferredImportSelector) {this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
} else {// 所有导入的类名称String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());// 将ImportSelector返回的类名转换为SourceClass// 使用Class.forName转换成Class实例,构造SourceClass对象Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);// 递归调用processImportsprocessImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
ImportSelector 接口
public interface ImportSelector {/*** 根据配置类的元数据信息,返回类的全限定名称* importingClassMetadata:@Import注解修饰的那个类**/String[] selectImports(AnnotationMetadata importingClassMetadata);@Nullabledefault Predicate<String> getExclusionFilter() {return null;}
}
DeferredImportSelector
Deferred意思是延迟,相比于ImportSelector会立即被处理,DeferredImportSelector的selectImport方法会在当前配置类所有的Bean信息解析完毕后才进行处理
DeferredImportSelector的作用是用于调整Import的Bean和当前配置类配置的Bean的先后关系
因为有条件注解的存在,所以需要Bean之间的注册有先后关系,条件注解才能发挥作用
处理 ImportBeanDefinitionRegistrar
如果SourceClass类型是ImportBeanDefinitionRegistrar的实现类简单,则调用org.springframework.context.annotation.ConfigurationClass#addImportBeanDefinitionRegistrar
方法
// 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());
ImportBeanDefinitionRegistrar 接口
ImportBeanDefinitionRegistrar接口用于手动注册BeanDefinition
public interface ImportBeanDefinitionRegistrar {/*** 注册BeanDefinition* BeanDefinitionRegistryPostProcessor此时还未注册* @param importingClassMetadata 导入的类的注解元数据* @param registry 当前BeanDefinitionRegistry,即BeanFactory实现* @param importBeanNameGenerator 导入的Bean的命名策略*/default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {registerBeanDefinitions(importingClassMetadata, registry);}default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {}
}
- BeanNameGenerator:默认实现是
ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR
或者通过ConfigurationClassPostProcessor#setBeanNameGenerator
进行设置 - BeanDefinitionRegistry registry:其实就是BeanFactory
@Configuration配置类解析完毕后,下一步就是将让配置生效。如下图所示,通过ConfigurationClassBeanDefinitionReader来加载BeanDefinition
ConfigurationClassBeanDefinitionReader加载BeanDefinition的过程中条件注解会生效
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();for (ConfigurationClass configClass : configurationModel) {loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);}
}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()) {registerBeanDefinitionForImportedConfigurationClass(configClass);}// 配置类的所有@Bean修饰的方法for (BeanMethod beanMethod : configClass.getBeanMethods()) {loadBeanDefinitionsForBeanMethod(beanMethod);}// @ImportedResources用于导入Spring的配置文件// 而Spring的配置文件中也可以定义BeanloadBeanDefinitionsFromImportedResources(configClass.getImportedResources());// 加载ImportBeanDefinitionRegistrar中需要注册的BeanloadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
处理Configuration
如果@Import的内容既不是ImportSelector也不是ImportBeanDefinitionRegistrar,那么就把它当作@Configuration修饰的类进行处理,无论它是不是真的被@Configuration修饰
// 既不是ImportSelector也不是ImportBeanDefinitionRegistrar
// 当作@Configuration类处理
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 继续走处理配置类的流程,会继续进行processImports方法
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
假如@Import的是一个普通类,比如下面这样,什么都没有。由于@Import的存在,SimpleClass还是会被当作Bean注册进容器
public class SimpleClass {
}
如果给它加上@Configuration配置类可以有的配置,它也是会生效的
public class SimpleClass {@Beanpublic A a() {return new A();}
}
相关文章:
Spring - @Import注解
文章目录 基本用法源码分析ConfigurationClassPostProcessorConfigurationClass SourceClassgetImportsprocessImports处理 ImportSelectorImportSelector 接口DeferredImportSelector 处理 ImportBeanDefinitionRegistrarImportBeanDefinitionRegistrar 接口 处理Configuratio…...
新能源汽车储充机器人:能源高效与智能调度
新能源汽车储充机器人:开启能源高效利用与智能调度的未来之门 随着全球能源危机的日益加剧和环境污染问题的不断恶化,新能源汽车成为了未来交通领域的重要发展方向。然而,新能源汽车的普及不仅需要解决电池技术的瓶颈,还需要构建一…...
【Linux网络】详解TCP协议(2)
🎉博主首页: 有趣的中国人 🎉专栏首页: Linux网络 🎉其它专栏: C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好,本片文章将会讲解 TCP协议的三次握手和四次挥手 的相关内容。 如果看到最后您觉得…...
STM32DMA学习日记
STM32 DMA学习日记 写于2024/9/28晚 文章目录 STM32 DMA学习日记1. DMA简介2. I/O方式2.1 程序查询方式2.2 程序中断方式2.3 DMA方式 3.DMA框图4. 相关寄存器4.1 DMA中断状态寄存器(DMA_ISR)4.2 DMA中断标志清除寄存器(DMA_IFCR)…...
【高性能内存池】page cache 5
page cache 1 page cache的框架2 central cache从page cache申请n页span的过程3 page cache 的结构3.1 page cache类框架3.2 central cache向page cache申请span3.3 获取k页的span page cache的结构和central cache是一样的,都是哈希桶的结构,并且挂载的…...
Vue 3 魔法揭秘:CSS 解析与 scoped 背后的奇幻之旅
文章目录 一、背景二、源码分析transformMain 返回值transformStyle 方法compileStyleAsync 方法scopedPlugin 方法template 添加 __scopeId 三、总结 一、背景 Vue 3 文件编译流程详解与 Babel 的使用 上文分析了 vue3 的编译过程,但是在对其中样式的解析遗留了一…...
如何获取钉钉webhook
第一步打开钉钉并登录 第二步创建团队 并且 添加自定义 机器人 即可获取webhook...
网页WebRTC电话和软电话哪个好用?
关于WebRTC电话与软件电话哪个更好用,这实际上取决于多个因素,并没有一个绝对的答案。不过,我可以根据WebRTC技术的一些特点,以及与传统软件电话相比的优劣势,为你提供一个清晰的对比。 首先,让我们了解一下…...
MySQL Mail服务器集成:如何配置发送邮件?
MySQL Mail插件使用指南?怎么优化 MySQL发邮件性能? MySQL Mail服务器的集成,使得数据库可以直接触发邮件发送,极大地简化了应用架构。AokSend将详细介绍如何配置MySQL Mail服务器,以实现邮件发送功能。 MySQL Mail&…...
【Rockchip系列】官方函数:imcopy
imcopy 函数原型 IM_STATUS imcopy(const rga_buffer_t src,rga_buffer_t dst,int sync 1,int *release_fence_fd NULL);功能说明 imcopy函数用于执行单次快速图像拷贝操作,将图像从源缓冲区拷贝到目标缓冲区。 参数说明 参数描述src[必填] 源图像缓冲区&…...
Matlab实现鲸鱼优化算法优化回声状态网络模型 (WOA-ESN)(附源码)
目录 1.内容介绍 2部分代码 3.实验结果 4.内容获取 1内容介绍 鲸鱼优化算法(Whale Optimization Algorithm, WOA)是一种基于座头鲸捕食行为的群智能优化算法。该算法通过模仿座头鲸使用螺旋形路径和包围猎物的策略来探索和开发解空间,以找到…...
迈威通信闪耀工博会,以创新科技赋能工业自动化
昨日,在圆满落幕的第24届中国国际工业博览会上,迈威通信作为工业自动化与智慧化领域的先行者,以“创新打造新质通信,赋能工业数字化”为主题精彩亮相,向全球业界展示了我们在工业自动化领域的最新成果与创新技术。此次…...
C# DotNetty客户端
1. 引入DotNetty包 我用的开发工具是VS2022,不同工具引入可能会有差异 工具——>NuGet包管理器——>管理解决方案的NuGet程序包 搜索DotNetty 2.新建EchoClientHandler.cs类 用于接收服务器返回数据 public class EchoClientHandler : SimpleChannelIn…...
4G模组SIM卡电路很简单,但也要注意这些坑
上次水SIM卡相关的文章,还是上一次; 上一篇文章里吹牛说,跟SIM卡相关的问题还有很多,目的是为下一篇文章埋下伏笔;伏笔埋是埋下了,但如果债老是不还,心里的石头就总悬着,搞不好老板…...
常见电脑品牌BIOS设置与进入启动项快捷键
常见电脑品牌BIOS与引导项快捷键速查表 | 电脑品牌 | BIOS快捷键 | 引导项快捷键 | 备注 ||------------|------------|--------------|------------------------------ || 联想 | F2/F1 | F12 | 笔记本通常为F2,台式机通常为F1 || IBM/ThinkPad | F1 | 未知 | ||…...
C语言中的日志机制:打造全面强大的日志系统
前言 在软件开发中,良好的日志记录机制对于调试、监控程序状态和维护系统的稳定性至关重要。本文将介绍如何在C语言中构建一个全面强大的日志系统,并提供一些示例代码。 1. 日志的基本概念 日志级别:用于分类日志信息的重要性,…...
局域网广域网,IP地址和端口号,TCP/IP 4层协议,协议的封装和分用
前言 在古老的年代,如果我们要实现两台机器进行数据传输, A员工就得去B员工的办公电脑传数据(B休息,等A传完),这样就很浪费时间 所以能不能不去B的工位的同时,还能传数据。这时候网络通信就出来…...
LabVIEW项目编码器选择
在LabVIEW项目中,选择增量式(Incremental Encoder)和绝对式(Absolute Encoder)编码器取决于项目的具体需求。增量式编码器和绝对式编码器在工作原理、应用场景、精度和成本等方面存在显著差异。以下从多方面详细阐述两…...
Spring Boot实现房产租赁业务逻辑
1 绪论 1.1 研究背景 中国的科技的不断进步,计算机发展也慢慢的越来越成熟,人们对计算机也是越来越更加的依赖,科研、教育慢慢用于计算机进行管理。从第一台计算机的产生,到现在计算机已经发展到我们无法想象。给我们的生活改变很…...
汽车3d动画渲染选择哪个?选择最佳云渲染解决方案
面临汽车3D动画渲染挑战?选择正确的云渲染服务至关重要。探索最佳解决方案,优化渲染效率,快速呈现逼真动画。 汽车3d动画渲染选择哪个? 对于汽车3D动画渲染,选择哪个渲染器取决于你的项目需求、预算和期望的效果。Ble…...
火语言RPA流程组件介绍--网页/元素截图
🚩【组件功能】:对整个网页、可见区域或者某个元素进行截图 ,保存至指定文件夹,仅适用于内置浏览器 配置预览 配置说明 截图类型 整个网页/可见区域/元素截图 目标元素 支持T或# 通过自动捕获工具捕获(选择元素工具使用方法)…...
VSCode编程配置再次总结
VScode 中C++编程再次总结 0.简介 1.配置总结 1.1 launch jsion文件 launch.json文件主要用于运行和调试的配置,具有程序启动调试功能。launch.json文件会启用tasks.json的任务,并能实现调试功能。 左侧任务栏的第四个选项运行和调试,点击创建launch.json {"conf…...
银行管理系统
摘 要 伴随着信息技术与互联网技术的不断发展,人们进到了一个新的信息化时代,传统管理技术性没法高效率、容易地管理信息内容。为了实现时代的发展必须,提升管理高效率,各种各样管理管理体系应时而生,各个领域陆续进到…...
极狐GitLab 17.4 重点功能解读【四】
GitLab 是一个全球知名的一体化 DevOps 平台,很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版,专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料: 极狐GitLab 官网极狐…...
[每日一练]利用自连接实现数量查询
该题目来源于力扣: 1731. 每位经理的下属员工数量 - 力扣(LeetCode) 题目要求: 表:Employees----------------------- | Column Name | Type | ----------------------- | employee_id | int | | name …...
Linux云计算 |【第四阶段】RDBMS1-DAY3
主要内容: 子查询(单行单列、多行单列、单行多列、多行多列)、分页查询limit、联合查询union、插入语句、修改语句、删除语句 一、子查询 子查询就是指的在一个完整的查询语句之中,嵌套若干个不同功能的小查询,从而一…...
初始MYSQL数据库(8)—— JDBC编程
找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程(ಥ_ಥ)-CSDN博客 所属专栏: MYSQL 目录 JDBC的概念 JDBC的使用 加载驱动包 建立连接 创建 statement 对象 定义并执行SQL语句 处理结果集 关闭资源 SQL注入 …...
Vue $router.push打开新窗口
Vue $router.push打开新窗口 最近有粉丝小伙伴问我:$router.push方法用于在当前窗口中跳转路由,但有时候我们需要在新的窗口或标签页中打开一个路由改怎么实现呢? 那么这里就介绍下实现逻辑和代码案例! 文章目录 Vue $router.pus…...
SQL进阶技巧:如何利用if语句简化where或join中的条件 | if条件语句的优雅使用方法
目录 0 问题场景 1 数据准备 2 问题分析 2.1 需求一 2.2需求二 3 小结 0 问题场景 有两张表,一张用户下单表user_purchase(用户ID粒度)包含用户ID、订单ID和下单消耗金额和一张用户维表user_info包含用户ID、用户年龄和用户是否实名认证。 user_purchase user_info 需…...
SpringCloud-Alibaba第二代微服务快速入门
1.简介 Spring Cloud Alibaba其实是阿里的微服务解决方案,是阿里巴巴结合自身微服务实践,开源的微服务全家桶,在Spring Cloud项目中孵化成为Spring Cloud的子项目。第一代的Spring Cloud标准中很多组件已经停更,如:Eureak,zuul等。所以Sprin…...
什么是网站流量优化/软文发稿平台有哪些
文章目录1. 编码与调制编码调制1. 编码与调制 基带信号:将数字信号1和0直接用两种不同的电压表示,再送到数字信道上去传输(基带传输) 宽带信号:将基带信号进行调制后形成的频分复用模拟信号,再传送到模拟信道上去传输…...
做进口零食批发网站/百度手机
实际环境和特殊需求往往会将简单问题复杂化,比如计算机IP地址,对于一个连接中socket,可以直接获得本端和对端的IP、端口信息。但在一些特殊场合我们可能需要更多的信息,比如系统中有几块网卡,他们的Mac地址是多少&…...
烟台建站模板源码/高清免费观看电视网站
1、卡方检验概述 卡方检验被誉为二十世纪科学技术所有分支中的20大发明之一,它的发明者卡尔皮尔逊是一位历史上罕见的百科全书式的学者,研究领域涵盖了生物、历史、宗教、哲学、法律。是英国著名的统计学家、生物统计学家、应用数学家,又是名副其实的历史学家、科学哲学家、…...
论坛类网站怎么建设/手游推广平台有哪些
与之前的版本相比,Tableau 2020.1.3在诸多方面进行了优化和升级,主要新增功能如下: 1. 自定义“发现”窗格 在最新版本中,可以自定义显示在Tableau Desktop中的“开始”页面上的“发现”窗格,以显示自定义内容&#…...
网络优化方案/惠州百度关键词优化
花了几个小时终于把Sublime的配置搞定了,能够在里面写vex和Python,同时另外设置了Python对houdini模块的以及其他扩展包的自动填充功能。 这里简单讲一下安装sublime,因为这个不是重点,所以只介绍他的基本步奏了,本来就…...
怎么用vs做网站开发/深圳网络营销外包公司推荐
VirtualAlloc 分配的内存是以 4K 为最小单位、连续的内存地址(但映射到真实的内存时它不一定是连续的), 前面说了, 它不适合分配小内存(譬如只有几个字节的变量); 局部的变量在 "栈" 中有程序自动管理, 那么那些全局的小变量怎么办呢? 这就要用到 "堆".这…...