spring源码解析-(2)Bean的包扫描

包扫描的过程

测试代码:

// 扫描指定包下的所有类
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
// 扫描指定包下的所有类
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
scanner.scan("com.test.entity");
for (String beanDefinitionName : registry.getBeanDefinitionNames()) {System.err.println(beanDefinitionName);
}

注意:下述源码在阅读时应尽量避免死磕,先梳理整体流程,理解其动作的含义,具体细节可以在看完整体之后再细致打磨。如果整个流程因为一些细节而卡住,那就丧失了看源码的意义,我们需要的是掌握流程和优秀的设计理念,而不是一字一句的抄下来。

scan方法详解

源码如下:翻译通过CodeGeeX进行自动生成并自己进行修改。

public int scan(String... basePackages) {// 获取扫描开始时的BeanDefinition数量int beanCountAtScanStart = this.registry.getBeanDefinitionCount();// 执行扫描doScan(basePackages);// 如果需要,注册注解配置处理器if (this.includeAnnotationConfig) {AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}// 返回扫描结束时的BeanDefinition数量与扫描开始时的数量之差return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

通过上面的代码,我们可以看到核心的方法为doScan,具体源码如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {// 确保至少传入一个基础包Assert.notEmpty(basePackages, "At least one base package must be specified");// 创建一个存放BeanDefinitionHolder的集合Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();for (String basePackage : basePackages) {// 1找出候选的组件Set<BeanDefinition> candidates = findCandidateComponents(basePackage);// 遍历候选的组件for (BeanDefinition candidate : candidates) {// 解析组件的作用域元数据ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);// 设置组件的作用域candidate.setScope(scopeMetadata.getScopeName());// 2生成组件的名称String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);// 如果候选的组件是抽象BeanDefinition的实例if (candidate instanceof AbstractBeanDefinition) {// 执行后处理器,对候选的组件进行处理postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);}// 如果候选的组件是带有注解的BeanDefinition的实例if (candidate instanceof AnnotatedBeanDefinition) {// 3处理带有注解的BeanDefinition的公共定义AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);}// 如果检查候选的组件if (checkCandidate(beanName, candidate)) {// 创建一个BeanDefinitionHolder,并将其添加到集合中BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);// 应用代理模式definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);// 将BeanDefinition注册到容器中registerBeanDefinition(definitionHolder, this.registry);}}}// 返回存放BeanDefinitionHolder的集合return beanDefinitions;
}

现在我们一步步的使用debug查看整个doScan的大体过程

1. 通过传入的路径扫描并得到对应的BeanDefinition

截取的部分代码片段如下:

for (String basePackage : basePackages) {// 找出候选的组件Set<BeanDefinition> candidates = findCandidateComponents(basePackage);// other
}

findCandidateComponents方法的源码:

从 Spring 5.0 开始新增了一个 @Indexed 注解(新特性,@Component 注解上面就添加了 @Indexed 注解), 这里不会去扫描指定路径下的 .class 文件,而是读取所有 META-INF/spring.components 文件中符合条件的类名,直接添加 .class 后缀就是编译文件,而不要去扫描。

public Set<BeanDefinition> findCandidateComponents(String basePackage) {// 如果componentsIndex不为空,并且indexSupportsIncludeFilters()为trueif (this.componentsIndex != null && indexSupportsIncludeFilters()) {// 从componentsIndex中添加候选组件return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);}// 否则else {// 扫描候选组件return scanCandidateComponents(basePackage);}
}

我们着重看scanCandidateComponents方法,源码如下:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {// 创建一个LinkedHashSet,用于存储扫描到的候选组件Set<BeanDefinition> candidates = new LinkedHashSet<>();try {// 1.1拼接包搜索路径String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +resolveBasePackage(basePackage) + '/' + this.resourcePattern;// 1.2获取资源Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);// 判断是否开启日志记录boolean traceEnabled = logger.isTraceEnabled();boolean debugEnabled = logger.isDebugEnabled();for (Resource resource : resources) {if (traceEnabled) {logger.trace("Scanning " + resource);}try {// 1.3获取元数据读取器MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);// 判断是否是候选组件if (isCandidateComponent(metadataReader)) {// 1.4创建一个ScannedGenericBeanDefinition,用于存储元数据读取器ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);sbd.setSource(resource);// 判断是否是候选组件if (isCandidateComponent(sbd)) {if (debugEnabled) {logger.debug("Identified candidate component class: " + resource);}// 将候选组件添加到candidates中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 (FileNotFoundException ex) {if (traceEnabled) {logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage());}} catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex);}}} catch (IOException ex) {throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);}return candidates;
}

1.1 拼接搜索路径

源码如下:

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

简单的字符串拼接,其代码本身没有任何技术含量,但debug的时候,可以通过其显示的值来帮助我们更好的理解项目。

在这里插入图片描述

debug的结果为:classpath*:com/test/entity/**/*.class

可以看到,是从根路径的com.test.enetity下开始寻找所有的字节码文件。

1.2 根据包扫描路径获取资源

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

具体方法:

public Resource[] getResources(String locationPattern) throws IOException {// 断言locationPattern不能为空Assert.notNull(locationPattern, "Location pattern must not be null");// 如果locationPattern以CLASSPATH_ALL_URL_PREFIX开头if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {// 是一个类路径资源(同一个名称可能存在多个资源)if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {// 是一个类路径资源模式return findPathMatchingResources(locationPattern);}else {// 所有具有给定名称的类路径资源return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));}}else {// 通常只在后缀前加上前缀,// 并且在Tomcat中,只有在“war:”协议下,才会使用“*/”分隔符。int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :locationPattern.indexOf(':') + 1);if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {// 是一个文件模式return findPathMatchingResources(locationPattern);}else {// 具有给定名称的单个资源return new Resource[] {getResourceLoader().getResource(locationPattern)};}}
}

接着我们进入到findPathMatchingResources的方法中,具体源码如下:

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {// 确定根目录路径String rootDirPath = determineRootDir(locationPattern);// 获取locationPattern中根目录路径之后的部分String subPattern = locationPattern.substring(rootDirPath.length());// 获取根目录资源Resource[] rootDirResources = getResources(rootDirPath);// 创建一个LinkedHashSet集合Set<Resource> result = new LinkedHashSet<>(16);// 遍历根目录资源for (Resource rootDirResource : rootDirResources) {// 解析根目录资源rootDirResource = resolveRootDirResource(rootDirResource);// 获取根目录资源的URLURL rootDirUrl = rootDirResource.getURL();// 如果存在equinoxResolveMethod且根目录URL的协议以"bundle"开头if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {// 调用equinoxResolveMethod方法,获取解析后的URLURL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);// 如果解析后的URL不为空if (resolvedUrl != null) {// 将解析后的URL赋值给rootDirUrlrootDirUrl = resolvedUrl;}// 将解析后的URL封装为ResourcerootDirResource = new UrlResource(rootDirUrl);}// 如果根目录URL的协议以"vfs"开头if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {// 调用VfsResourceMatchingDelegate的findMatchingResources方法,获取匹配的资源result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));}// 如果根目录URL是jar文件或者根目录资源是jar资源else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {// 调用doFindPathMatchingJarResources方法,获取匹配的jar资源result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));}// 否则else {// 调用doFindPathMatchingFileResources方法,获取匹配的文件资源result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));}}// 如果存在日志日志功能if (logger.isTraceEnabled()) {// 打印日志logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);}// 返回结果return result.toArray(new Resource[0]);
}

1.3 获取元数据

源码:可以看到通过1.2中获取到的资源将通过如下方法获取一个名为MetadataReader的对象,那么这个元数据读取器究竟是干什么的?

// getMetadataReaderFactory工厂模式构建MetadataReader
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);

MetadataReader接口中只定义了三个方法,源码如下:

public interface MetadataReader {Resource getResource();ClassMetadata getClassMetadata();AnnotationMetadata getAnnotationMetadata();
}

根据debug追随到的源码,发现其进入到了MetadataReader的实现类SimpleMetadataReader中,其构造器如下:

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {// 这里使用到了经典的访问者模式,可以自行了解一下SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);getClassReader(resource).accept(visitor, PARSING_OPTIONS);this.resource = resource;this.annotationMetadata = visitor.getMetadata();
}

感兴趣的可以着重看下getClassReader(resource).accept(visitor, PARSING_OPTIONS);这句话中大概做了那些事情。

具体源码就不展示,这里大概就是将class文件读取并操作二进制代码。(可以通过《深入了解Java虚拟机》这本书来了解一下java的字节码相关内容)

1.4 创建ScannedGenericBeanDefinition

通过构造器的方式将MetadataReader中读取到的内容放入BeanDefinition中。

public ScannedGenericBeanDefinition(MetadataReader metadataReader) {Assert.notNull(metadataReader, "MetadataReader must not be null");this.metadata = metadataReader.getAnnotationMetadata();setBeanClassName(this.metadata.getClassName());setResource(metadataReader.getResource());
}

2. 获取Bean的名称

String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

我们可以看一下spring是如何获取bean的名称的

public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {if (definition instanceof AnnotatedBeanDefinition) {String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);if (StringUtils.hasText(beanName)) {// Explicit bean name found.return beanName;}}// Fallback: generate a unique default bean name.return buildDefaultBeanName(definition, registry);
}

可以看到该方法是有两个分支,如果通过注解中可以拿到名字,则直接返回,具体方法如下:

// 确定基于注解的bean名称
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {// 获取注解的元数据AnnotationMetadata amd = annotatedDef.getMetadata();// 获取注解的类型Set<String> types = amd.getAnnotationTypes();// 初始化bean名称String beanName = null;// 遍历注解类型for (String type : types) {// 获取注解的属性AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);if (attributes != null) {// 获取注解的元注解类型Set<String> metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> {Set<String> result = amd.getMetaAnnotationTypes(key);return (result.isEmpty() ? Collections.emptySet() : result);});// 判断注解是否为stereotype注解,并且包含name和value属性if (isStereotypeWithNameValue(type, metaTypes, attributes)) {// 获取name属性的值Object value = attributes.get("value");if (value instanceof String) {String strVal = (String) value;// 判断字符串长度是否大于0if (StringUtils.hasLength(strVal)) {// 判断beanName是否为空,或者与strVal不相等if (beanName != null && !strVal.equals(beanName)) {// 抛出异常throw new IllegalStateException("Stereotype annotations suggest inconsistent " +"component names: '" + beanName + "' versus '" + strVal + "'");}// 更新beanNamebeanName = strVal;}}}}}return beanName;
}

或者,根据类的class直接创建名称,代码如下:

protected String buildDefaultBeanName(BeanDefinition definition) {// 获取bean的类名String beanClassName = definition.getBeanClassName();// 断言bean的类名不能为空Assert.state(beanClassName != null, "No bean class name set");// 获取bean的简短名称String shortClassName = ClassUtils.getShortName(beanClassName);// 将简短名称的首字母转换为小写return Introspector.decapitalize(shortClassName);
}

3. 处理带有注解的BeanDefinition的公共定义

AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);

在此方法中,我们可以了解到,注册为Bean的类上可以添加的注解有几种,代码如下:

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {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"));}}if (metadata.isAnnotated(Primary.class.getName())) {abd.setPrimary(true);}AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);if (dependsOn != null) {abd.setDependsOn(dependsOn.getStringArray("value"));}AnnotationAttributes role = attributesFor(metadata, Role.class);if (role != null) {abd.setRole(role.getNumber("value").intValue());}AnnotationAttributes description = attributesFor(metadata, Description.class);if (description != null) {abd.setDescription(description.getString("value"));}
}

4 将定义好的BeanDefinition放入set,并返回set。

if (checkCandidate(beanName, candidate)) {// 创建一个BeanDefinitionHolder,并将其添加到集合中BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);// 应用代理模式definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);// 将BeanDefinition注册到容器中registerBeanDefinition(definitionHolder, this.registry);
}

思考总结:

1、可以看到,在spring中使用了很多设计模式,设计模式在解决一系列问题上非常有帮助,如创建Bean的Bean工厂,访问并根据字节码操作的访问者模式,需要理解其思想,在遇到问题的时候,往设计模式上想一想。后续可能要深入的学习一下设计模式。

2、关于数据类型的使用,其实本人在实际开发的过程中大部分时间都使用list,Map,其中也遇到过很多次内存溢出,在合理的选择数据类型上,有必要仔细斟酌。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/25016.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatis中 set标签

1、set标签特点&#xff1a; set标签用于更新语句中set标签解析为set关键字set可以去除跟新语句中无用的逗号通常是和if标签一起使用 2、set标签的使用 编写接口方法编写sql语句 注意 当set标签中有条件成立时就会附加set关键字&#xff0c;字段为null时该列不会被更新。se…

域内攻击 ---> AS-REP Roasting

今天&#xff0c;我们就来讲一下另外一种Roasting攻击 AS-REP Roasting 1.AS-REP Roasting原理 其实和kerberoasting一样&#xff0c;这种攻击也是一种暴力破解的攻击&#xff0c;完全取决于攻击者的字典。 但是不同于Kerberoasting&#xff0c;他不需要用户进行注册SPN&…

M3ID和CD的区别

M3ID的公式&#xff1a; CD的公式&#xff08;概率空间版本&#xff09;&#xff1a; CD的公式&#xff08;logits空间版本&#xff09;&#xff1a; 为简单对比&#xff0c;主要比较概率空间版本。logits空间版本已有证明和概率空间版本等效&#xff0c;在此不做详细讨论&a…

Linux网络 - json,网络计算服务器与客户端改进

文章目录 前言一、json1.引入库2. 使用步骤2.Calculator.hpp3.Task.hpp4.serverCal.hpp 新客户端 前言 本章内容主要对上一章的网络计算器客户端和服务器进行一些Bug修正与功能改进。 并学习如何使用json库和daemon函数。 一、json 在我们自己的电脑上一些软件的文件夹中&…

如何删除电脑端口映射?

在使用电脑进行网络连接时&#xff0c;有时需要进行端口映射以实现不同设备之间的信息远程通信。当这些端口映射不再需要时&#xff0c;我们需要及时删除它们以确保网络的安全和稳定。本文将介绍如何删除电脑端口映射的方法。 操作系统自带的工具 大多数操作系统都提供了自带…

电机控制系列模块解析(29)—— 逆变器带输出LC滤波器

一般长线驱动&#xff08;港口和油矿&#xff09;和超高速电机&#xff08;高频&#xff09;等驱动系统可能会要求加装输出LC滤波器。 此图片来源于会议PPT 一、逆变器带输出LC滤波器 逆变器输出端配置LC滤波器&#xff08;电感L与电容C组成的无源滤波电路&#xff09;旨在改…

pdf文件如何防篡改内容

PDF文件防篡改内容的方法有多种&#xff0c;以下是一些常见且有效的方法&#xff0c;它们可以帮助确保PDF文件的完整性和真实性&#xff1a; 加密PDF文档&#xff1a; 原理&#xff1a;通过设置密码来保护PDF文档&#xff0c;防止未经授权的访问和修改。注意事项&#xff1a;密…

创新实训2024.06.03日志:完善Baseline Test框架、加入对Qwen-14B的测试

1. Baseline Test框架重构与完善 在之前的一篇博客中&#xff08;创新实训2024.05.29日志&#xff1a;评测数据集与baseline测试-CSDN博客&#xff09;&#xff0c;我介绍了我们对于大模型进行基线测试的一些基本想法和实现&#xff0c;包括一些基线测试的初步结果。 后来的一…

通过无障碍控制 Compose 界面滚动的实战和原理剖析

前言 针对 Compose UI 工具包&#xff0c;开发者不仅需要掌握如何使用新的 UI 组件达到 design 需求&#xff0c;更需要了解和实现与 UI 的交互逻辑。 比如 touch 事件、Accessibility 事件等等。 Compose 中对 touch 事件的处理和原理&#xff0c;笔者已经在《通过调用栈快…

【Linux】进程(9):进程控制1

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解Linux进程&#xff08;9&#xff09;进程控制1&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 1 fork函数2 进程终止&#xff08;A&#xff09;终止是…

java中异常-异常概述+异常体系结构

一、异常概述 1、什么是异常&#xff1f; java程序在运行时出现的不正常情况 2、java中提供的默认的异常处理机制 java中对java程序运行时可能会出现的每种不正常情况都创建了一个唯一对应的类&#xff0c;在java程序运行时如果出现不正常情况&#xff0c;java程序就会创建…

Java | Leetcode Java题解之第140题单词拆分II

题目&#xff1a; 题解&#xff1a; class Solution {public List<String> wordBreak(String s, List<String> wordDict) {Map<Integer, List<List<String>>> map new HashMap<Integer, List<List<String>>>();List<List…

USB Type-C 和 USB供电数据和电源角色

USB Type-C 连接器生态系统随着现代平台和设备需求的变化而不断发展。 USB Type-C 连接器生态系统可满足现代平台和设备不断变化的需求&#xff0c;并且符合更小、更薄且更轻便的外形设计趋势。此外&#xff0c;针对 Type-C 连接器修改 USB PD 有助于满足高耗电应用的需求。 …

数据总线、位扩展、字长

数据总线&#xff08;Data Bus&#xff09; 定义 数据总线是计算机系统中的一组并行信号线&#xff0c;用于在计算机内部传输数据。这些数据可以在中央处理器&#xff08;CPU&#xff09;、内存和输入/输出设备之间传输。 作用 数据传输&#xff1a;数据总线负责在计算机各…

c++【入门】求圆环的面积

限制 时间限制 : 1 秒 内存限制 : 128 MB 题目 如下图所示的圆环铁片&#xff0c;中间是空心的&#xff0c;已知圆环外圆的半径是r1厘米&#xff08;如&#xff1a;10cm&#xff09;&#xff0c;内圆半径是r2厘米&#xff08;如&#xff1a;6cm&#xff09;&#xff0c;请编…

心链13---主页切换功能 + loading特效 + 导航栏完善 + 队伍页接口修改

心链 — 伙伴匹配系统 直接取出所有用户&#xff0c;依次和当前用户计算分数&#xff0c;取 TOP N&#xff08;54 秒&#xff09; 优化方法&#xff1a; 切忌不要在数据量大的时候循环输出日志&#xff08;取消掉日志后 20 秒&#xff09;Map 存了所有的分数信息&#xff0c;占…

C++ | Leetcode C++题解之第140题单词拆分II

题目&#xff1a; 题解&#xff1a; class Solution { private:unordered_map<int, vector<string>> ans;unordered_set<string> wordSet;public:vector<string> wordBreak(string s, vector<string>& wordDict) {wordSet unordered_set(w…

SpringBoot+Vue网上购物商城系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 系统角色对应功能 用户商家管理员 系统功能截图

docker部署redis实践

1.拉取redis镜像 # 拉取镜像 sudo docker pull redis2.创建映射持久化目录 # 创建目录 sudo mkdir -p $PWD/redis/{conf,data}3. 运行redis 容器&#xff0c;查看当前redis 版本号 # 运行 sudo docker run --name redis -d -p 6379:6379 redis # 查看版本号 sudo docker ex…

java异常处理知识点总结

一.前提知识 首先当运行出错的时候&#xff0c;有两种情况&#xff0c;一种叫做“错误”&#xff0c;另一种叫做“异常”。错误指的是运行过程中遇到了硬件或操作系统出错&#xff0c;这种情况程序员是没办法处理的&#xff0c;因为这是硬件和系统的问题&#xff0c;不能靠代码…