死磕Spring之IoC篇 - BeanDefinition 的加载阶段(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读
Spring 版本:5.1.14.RELEASE
开始阅读这一系列文章之前,建议先查看《深入了解 Spring IoC(面试题)》这一篇文章
该系列其他文章请查看:《死磕 Spring 之 IoC 篇 - 文章导读》
BeanDefinition 的加载阶段(XML 文件)
上一篇文章 《Bean 的“前身”》 对 BeanDefinition 进行了介绍,Bean 是根据 BeanDefinition 配置元信息对象生成的。我们在 Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)、面向注解,那么 Spring 是如何将这两种方式定义的信息转换成 BeanDefinition 对象的,接下来会先分析面向资源(XML、Properties)这种方式 Spring 是如何处理的
下来熟悉一段代码:
dependency-lookup-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- <context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" /> -->
<bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
<property name="id" value="1"/>
<property name="name" value="小马哥"/>
</bean>
</beans>
// 创建 BeanFactory 容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// XML 配置文件 ClassPath 路径
String location = "classpath:/META-INF/dependency-lookup-context.xml";
// 加载配置
int beanDefinitionsCount = reader.loadBeanDefinitions(location);
System.out.println("Bean 定义加载的数量:" + beanDefinitionsCount);
// 依赖查找
System.out.println(beanFactory.getBean("user"));;
这段代码是 Spring 中编程式使用 IoC 容器,我们可以看到 IoC 容器的使用过程大致如下:
- 创建 BeanFactory 对象(底层 IoC 容器)
- 创建 BeanDefinitionReader 对象(资源解析器),关联第
1步创建的 BeanFactory - 通过 BeanDefinitionReader 加载 XML 配置文件资源,解析出所有的 BeanDefinition 对象
- 进行依赖查找
上面的第 3 步会解析 Resource 资源,将 XML 文件中定义的 Bean 解析成 BeanDefinition 配置元信息对象,并往 BeanDefinitionRegistry 注册中心注册,此时并没有生成对应的 Bean 对象,需要通过依赖查找获取到 Bean。当然,我们在实际场景中一般不会这样使用 Spring,这些工作都会有 Spring 来完成。接下来我们一起来看看 Sping 是如何加载 XML 文件的
BeanDefinitionReader 体系结构
org.springframework.beans.factory.support.BeanDefinitionReader 接口的类图如下所示:

总览:
org.springframework.beans.factory.support.BeanDefinitionReader接口,BeanDefinition 读取器org.springframework.beans.factory.support.AbstractBeanDefinitionReader抽象类,提供通用的实现,具体的资源加载逻辑在由子类实现org.springframework.beans.factory.xml.XmlBeanDefinitionReader,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册org.springframework.beans.factory.support.PropertiesBeanDefinitionReader,Properties 文件资源解析器
BeanDefinitionReader 接口
org.springframework.beans.factory.support.BeanDefinitionReader 接口,BeanDefinition 读取器,定义了加载资源的方法,代码如下:
public interface BeanDefinitionReader {
/** 返回 BeanDefinition 注册中心 */
BeanDefinitionRegistry getRegistry();
/** 返回 Resource 资源加载器,默认为 PathMatchingResourcePatternResolver */
@Nullable
ResourceLoader getResourceLoader();
/** 返回类加载器 */
@Nullable
ClassLoader getBeanClassLoader();
/** 返回 Bean 的名称生成器,默认为 DefaultBeanNameGenerator */
BeanNameGenerator getBeanNameGenerator();
/** 从 Resource 资源中加载 BeanDefinition 并返回数量 */
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}
AbstractBeanDefinitionReader 抽象类
org.springframework.beans.factory.support.AbstractBeanDefinitionReader 抽象类,实现了 BeanDefinitionReader 和 EnvironmentCapable 接口,代码如下:
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
private final BeanDefinitionRegistry registry;
@Nullable
private ResourceLoader resourceLoader;
@Nullable
private ClassLoader beanClassLoader;
private Environment environment;
private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
this.registry = registry;
// Determine ResourceLoader to use.
if (this.registry instanceof ResourceLoader) {
this.resourceLoader = (ResourceLoader) this.registry;
}
else {
this.resourceLoader = new PathMatchingResourcePatternResolver();
}
// Inherit Environment if possible
if (this.registry instanceof EnvironmentCapable) {
this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
}
else {
this.environment = new StandardEnvironment();
}
}
@Override
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int count = 0;
for (Resource resource : resources) {
count += loadBeanDefinitions(resource);
}
return count;
}
@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return loadBeanDefinitions(location, null);
}
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 获得 ResourceLoader 对象
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
}
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
// 获得 Resource 数组,因为 Pattern 模式匹配下,可能有多个 Resource 。例如说,Ant 风格的 location
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
// 加载 BeanDefinition 们
int count = loadBeanDefinitions(resources);
if (actualResources != null) {
// 添加到 actualResources 中
Collections.addAll(actualResources, resources);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
}
return count;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
// 获得 Resource 对象
Resource resource = resourceLoader.getResource(location);
// 加载 BeanDefinition 们
int count = loadBeanDefinitions(resource);
if (actualResources != null) {
// 添加到 actualResources 中
actualResources.add(resource);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
}
return count;
}
}
@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int count = 0;
for (String location : locations) {
count += loadBeanDefinitions(location);
}
return count;
}
// ... 省略相关代码
}
在实现的方法中,最终都会调用 int loadBeanDefinitions(Resource resource) 这个方法,该方法在子类中实现
XmlBeanDefinitionReader
org.springframework.beans.factory.xml.XmlBeanDefinitionReader,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册
构造函数
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
* 禁用验证模式
*/
public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;
/**
* 自动获取验证模式
*/
public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;
/**
* DTD 验证模式
*/
public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;
/**
* XSD 验证模式
*/
public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;
/** Constants instance for this class. */
private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);
/**
* 验证模式,默认为自动模式。
*/
private int validationMode = VALIDATION_AUTO;
private boolean namespaceAware = false;
private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
/**
* 解析过程中异常处理器
*/
private ProblemReporter problemReporter = new FailFastProblemReporter();
private ReaderEventListener eventListener = new EmptyReaderEventListener();
private SourceExtractor sourceExtractor = new NullSourceExtractor();
@Nullable
private NamespaceHandlerResolver namespaceHandlerResolver;
private DocumentLoader documentLoader = new DefaultDocumentLoader();
@Nullable
private EntityResolver entityResolver;
private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
/**
* XML 验证模式探测器
*/
private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
/**
* 当前线程,正在加载的 EncodedResource 集合。
*/
private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<>(
"XML bean definition resources currently being loaded");
/**
* Create new XmlBeanDefinitionReader for the given bean factory.
* @param registry the BeanFactory to load bean definitions into,
* in the form of a BeanDefinitionRegistry
*/
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}
}
loadBeanDefinitions 方法
loadBeanDefinitions(Resource resource) 方法,解析 Resource 资源的入口,方法如下:
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// <1> 获取当前线程正在加载的 Resource 资源集合,添加当前 Resource,防止重复加载
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) { // 将当前资源加入记录中。如果已存在,抛出异常,防止循环加载同一资源出现死循环
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// <2> 从 Resource 资源获取 InputStream 流对象(支持编码)
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// <3> 【核心】执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} finally {
// 关闭流
inputStream.close();
}
} catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
} finally {
// <4> 从当前线程移除当前加载的 Resource 对象
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
将 Resource 封装成 EncodedResource 对象,目的是让资源对象可设置编码
- 获取当前线程正在加载的 Resource 资源集合,添加当前 Resource,防止重复加载
- 从 Resource 资源获取 InputStream 流对象(支持编码)
- 【核心】调用
doLoadBeanDefinitions(InputSource inputSource, Resource resource)方法,执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册 - 从当前线程移除当前加载的 Resource 对象
doLoadBeanDefinitions 方法
doLoadBeanDefinitions(InputSource inputSource, Resource resource) 方法,执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册,方法如下:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// <1> 获取 XML Document 实例
Document doc = doLoadDocument(inputSource, resource);
// <2> 根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
// 省略 catch 各种异常
}
- 调用
doLoadDocument(InputSource inputSource, Resource resource)方法,获取 XML Document 实例 - 调用
registerBeanDefinitions(Document doc, Resource resource)方法,根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量
doLoadDocument 方法
doLoadDocument(InputSource inputSource, Resource resource) 方法,获取 Resource 资源对应的 XML Document 实例,方法如下:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
// <3> 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
return this.documentLoader.loadDocument(inputSource,
getEntityResolver(), // <1> 获取 `org.xml.sax.EntityResolver` 实体解析器,ResourceEntityResolver
this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware()); // <2> 获取 XML 文件验证模式,保证 XML 文件的正确性
}
- 获取
org.xml.sax.EntityResolver实体解析器,ResourceEntityResolver,根据 publicId 和 systemId 获取对应的 DTD 或 XSD 文件,用于对 XML 文件进行验证,这个类比较关键,在后续文章会讲到 - 获取 XML 文件验证模式,保证 XML 文件的正确性,通常情况下都是 XSD 模式
- 获取指定的验证模式,如果手动指定,则直接返回,通常情况下不会
- 从 Resource 资源中获取验证模式,根据 XML 文件的内容进行获取,如果包含
DOCTYPE内容则为 DTD 模式,否则为 XSD 模式 - 如果还没有获取到验证模式,则默认为 XSD 模式
- 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
- 创建 DocumentBuilderFactory 对象
factory,开启校验 - 根据
factory创建 DocumentBuilder 对象builder,设置 EntityResolver(第1步创建的)、ErrorHandler 属性 - 通过
builder对inputSource(Resource 资源)进行解析,返回一个 Document 对象
- 创建 DocumentBuilderFactory 对象
上述过程目的就是获取到 Resource 资源对应的 Document 对象,需要经过校验和解析两个过程
registerBeanDefinitions 方法
registerBeanDefinitions(Document doc, Resource resource) 方法,根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量,方法如下:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// <1> 创建 BeanDefinitionDocumentReader 对象
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// <2> 获取已注册的 BeanDefinition 数量
int countBefore = getRegistry().getBeanDefinitionCount();
// <3> 创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象)
// <4> 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// <5> 计算新注册的 BeanDefinition 数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
- 创建 DefaultBeanDefinitionDocumentReader 对象
documentReader - 获取已注册的 BeanDefinition 数量
- 创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象),注意这里会初始化一个 DefaultNamespaceHandlerResolver 对象,用于处理自定义标签(XML 文件),比较关键,在后续文章会讲到
- 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册,调用
DefaultBeanDefinitionDocumentReader#registerBeanDefinitions(Document doc, XmlReaderContext readerContext)方法 - 计算新注册的 BeanDefinition 数量并返回
拓展:DTD 与 XSD 的区别?
DTD(Document Type Definition),即文档类型定义,为 XML 文件的验证机制,属于 XML 文件中组成的一部分。DTD 是一种保证 XML 文档格式正确的有效验证方式,它定义了相关 XML 文档的元素、属性、排列方式、元素的内容类型以及元素的层次结构。其实 DTD 就相当于 XML 中的 “词汇”和“语法”,我们可以通过比较 XML 文件和 DTD 文件 来看文档是否符合规范,元素和标签使用是否正确。
DTD 在一定的阶段推动了 XML 的发展,但是它本身存在着一些缺陷:
- 它没有使用 XML 格式,而是自己定义了一套格式,相对解析器的重用性较差;而且 DTD 的构建和访问没有标准的编程接口,导致解析器很难简单的解析 DTD 文档
- DTD 对元素的类型限制较少;同时其他的约束力也比较弱
- DTD 扩展能力较差
- 基于正则表达式的 DTD 文档的描述能力有限
XSD(XML Schemas Definition),即 XML Schema 语言,针对 DTD 的缺陷由 W3C 在 2001 年推出。XML Schema 本身就是一个 XML 文档,使用的是 XML 语法,因此可以很方便的解析 XSD 文档。相对于 DTD,XSD 具有如下优势:
- XML Schema 基于 XML,没有专门的语法
- XML Schema 可以像其他 XML 文件一样解析和处理
- XML Schema 比 DTD 提供了更丰富的数据类型
- XML Schema 提供可扩充的数据模型
- XML Schema 支持综合命名空间
- XML Schema 支持属性组
总结
我们在 Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)、面向注解,对于第一种方式如果定义的是一个 XML 文件,Spring 会通过 XmlBeanDefinitionReader 加载该 XML 文件,获取该 Resource 资源的 org.w3c.dom.Document 对象,这个过程会经过校验、解析两个步骤
死磕Spring之IoC篇 - BeanDefinition 的加载阶段(XML 文件)的更多相关文章
- 死磕Spring之IoC篇 - BeanDefinition 的解析阶段(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - BeanDefinition 的解析过程(面向注解)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 文章导读
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 解析自定义标签(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 开启 Bean 的加载
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - Bean 的创建过程
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - @Autowired 等注解的实现原理
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - Spring 应用上下文 ApplicationContext
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - @Bean 等注解的实现原理
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
随机推荐
- docker学习一
由于本人学习精力有限,不能像大学时那样,每个笔记都认真的手敲记录,一些地方会截图展示,并会添加自己的理解和备注,建议大家也做自己的笔记,看别人的可以,但是自己动手并思考记忆更深刻. 1.什么是虚拟化 ...
- CentOS环境下搭建青岛大学OJ
1.安装必要的依赖sudo yum updatesudo yum -y install epel-releasesudo yum -y install python-pipsudo yum clean ...
- hdu 6704 K-th occurrence(后缀数组+可持久化线段树)
Problem Description You are given a string S consisting of only lowercase english letters and some q ...
- 【uva 1312】Cricket Field(算法效率--技巧枚举)
题意:一个 L*R 的网格里有 N 棵树,要求找一个最大空正方形并输出其左下角坐标和长.(1≤L,R≤10000, 0≤N≤100) 解法:枚举空正方形也就是枚举空矩阵,先要固定一个边,才好继续操作. ...
- Codeforces Round #647 (Div. 2) - Thanks, Algo Muse! A、Johnny and Ancient Computer B、Johnny and His Hobbies C、Johnny and Another Rating Drop
题目链接:A.Johnny and Ancient Computer 题意: 给你两个数a,b.问你可不可以通过左移位运算或者右移位运算使得它们两个相等.可以的话输出操作次数,不可以输出-1 一次操作 ...
- Codeforces Round #643 (Div. 2) E. Restorer Distance (贪心,三分)
题意:给你\(n\)个数,每次可以使某个数++,--,或使某个数--另一个++,分别消耗\(a,r,m\).求使所有数相同最少的消耗. 题解:因为答案不是单调的,所以不能二分,但不难发现,答案只有一个 ...
- JavaScript——变量及作用域
- .net面试--值类型和引用类型
注:下面的示意图主要是为了辅助理解,不代表内存真实情况. Introduction 类型基础是C#的基础概念,了解类型基础及背后的工作原理更有助于我们在编码的时候明白数据在内存中的分配与传递.C#提供 ...
- transformers---FloatProgress not found. Please update jupyter and ipywidgets.
问题 运行 huggingface transformers 的 demo,报错FloatProgress not found.具体如下: import torch from transformers ...
- [Golang]-4 错误处理、Panic、Defer
目录 错误和异常 案例 Panic Defer 使用 defer+recover 来处理错误 参考文章: Go 语言使用一个独立的·明确的返回值来传递错误信息的.这与使用异常的 Java 和 Ruby ...