2.1.构建 XmlConfigBuilder 准备解析 XML 文件的基础环境
前面说过,XmlConfigBuilder
对象主要用于解析mybatis的全局配置文件,并以此来获取Configuration
对象的实例。
XmlConfigBuilder
对外暴露了六个构造方法,这六个方法根据mybatis配置文件的输入流类型可以分为两大类:
分别负责处理字节流形式的配置文件和处理字符流形式的配置文件。
// 处理字节流类型的mybatis配置
XMLConfigBuilder(InputStream inputStream);
XMLConfigBuilder(InputStream inputStream, String environment);
XMLConfigBuilder(InputStream inputStream, String environment, Properties props);
// 处理字符流类型的mybatis配置
XMLConfigBuilder(Reader reader);
XMLConfigBuilder(Reader reader, String environment);
XMLConfigBuilder(Reader reader, String environment, Properties props);
在具体的代码实现上,这两类构造方法的调用最终会根据文件流的不同落在下面这两个方法上(因为前面我们传入的是字节流,所以此处触发的是第一个方法):
/**
* @param inputStream 配置文件输入流
* @param environment 当前环境ID
* @param props 用户自定义属性
*/
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(
inputStream,/*XML文件输入流*/
true, /*开启验证*/
props, /*Property参*/
new XMLMapperEntityResolver() /*XML实体解析器*/
) /*新建一个XML解析器的实例*/
, environment/*环境对象*/
, props /*Property参数*/
);
}
/**
* @param reader 配置文件输入流
* @param environment 当前环境ID
* @param props 用户自定义属性
*/
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader,
true/*是否进行DTD校验*/
, props /*参数*/
, new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/
)/*构建一个XPath解析器*/
, environment /*当前环境*/
, props /*参数*/
);
}
在上面这两个方法内,不管传入的是字节流还是字符流,mybati都会使用传入的配置文件输入流创建一个XPathParser
对象,然后利用XPathParser
对象调用XMLConfigBuilder
的私有构造方法XMLConfigBuilder(XPathParser parser, String environment, Properties props)
来完成XmlConfigBuilder
对象的构造过程。
/**
* @param parser XPath解析器
* @param environment 环境ID
* @param props 用户自定义属性
*/
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 初始化Configuration对象的同时注册部分别名以及语言驱动
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
// 设置变量
this.configuration.setVariables(props);
// 初始化解析标签
this.parsed = false;
// 初始化环境容器
this.environment = environment;
// 初始化Xml地址解析器
this.parser = parser;
}
XPathParser
是mybatis内部定义的一个基于XPath
语言来读取XML
文档中的数据的解析工具类。
XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。
mybatis在创建XPathParser
对象时,传入了一个new XMLMapperEntityResolver()
对象,该对象的作用我们后面再说,现在我们先忽略掉他以及XPathParser
对象的具体实现,继续看XmlConfigBuilder
的构造过程。
new XPathParser(reader,
true/*是否进行DTD校验*/
, props /*参数*/
, new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/
)
在XmlConfigBuilder
的私有构造方法中,mybatis会调用Configuration
的无参构造方法生成一个Configuration
对象,并将Configuration
对象传入XmlConfigBuilder
的父类BaseBuilder
的构造方法中完成BaseBuilder
的初始化工作。
// 初始化Configuration对象的同时注册部分别名以及语言驱动
super(new Configuration());
Configuration
的无参构造方法内包含了一些基础数据的准备工作,其中主要包括注册常用的类型别名,注册脚本语言驱动器和配置默认的脚本语言驱动器。
- 类型别名: 点击了解类型别名
类型别名是为 Java 类型设置一个短的名字。 它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
- 脚本语言驱动器:点击了解脚本语言驱动器,相关内容在标题为"动态 SQL 中的可插拔脚本语言"段落内
可以查看简单的实例测试:
org.apache.ibatis.submitted.language.LanguageTest#testLangVelocityWithMapper
public Configuration() {
// 注册别名
// 注册JDBC别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
// 注册事务管理别名
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
// 注册JNDI别名
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
// 注册池化数据源别名
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
// 注册为池化的数据源
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
// 注册永久缓存
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
// 注册先入先出的缓存
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
// 注册最近最少使用缓存
typeAliasRegistry.registerAlias("LRU", LruCache.class);
// 注册软缓存
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
// 注册弱缓存
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
// 注册处理数据库ID的提供者
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
// 注册基于XML的语言驱动
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
//注册静态语言驱动(通常无需使用)
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
// 注册Sl4j日志
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
//注册Commons日志
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
//注册log4j日志
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
//注册log4j2日志
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
//注册jdk log日志
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
//注册标准输出日志
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
//注册无日志
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
// 注册CGLIB
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
// 注册JAVASSIST
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
// 默认使用XML语言驱动
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
// 支持原始语言驱动
languageRegistry.register(RawLanguageDriver.class);
}
!! 需要留意的是,在这里mybatis定义了默认的脚本语言驱动器是别名为XML
的 XMLLanguageDriver。
我们之前说过XMLConfigBuilder
对象是BaseBuilder
的子类,在BaseBuilder
里有三个final
修饰的属性:
/**
* Mybatis配置信息
*/
protected final Configuration configuration;
/**
* 类型别名注册表
*/
protected final TypeAliasRegistry typeAliasRegistry;
/**
* 类型转换处理器注册表
*/
protected final TypeHandlerRegistry typeHandlerRegistry;
他们的初始化赋值操作发生在BaseBuilder
的构造方法中:
public BaseBuilder(Configuration configuration) {
// 保持引用
this.configuration = configuration;
// 从Mybatis配置中同步过来类型别名注册表
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
// 从Mybatis配置中同步过来类型处理器注册表
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
在上面的这个构造方法中,BaseBuilder
缓存了对Configuration
对象的引用,同时把自身的typeHandlerRegistry
和TypeAliasRegistry
属性指向了Configuration
对象的typeHandlerRegistry
和TypeAliasRegistry
属性。
public class Configuration {
...
/**
* 类型处理器注册表
*/
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
/**
* 类型别名注册表,主要用在执行SQL语句的出入参以及一些类的简写
*/
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
...
}
- 类型处理器:点击了解mybatis类型处理器
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
在调用BaseBuilder
的构造方法之后,XMLConfigBuilder
会将用户自定义的变量集合传递给Configuration
对象,让其保存起来,供后面解析配置时使用。
// 缓存用户定义的变量
this.configuration.setVariables(props);
之后重置解析标记,将其状态修改为未进入解析操作
,表示允许调用parse()
方法:
// 初始化解析标签
this.parsed = false;
之后记录当前使用的环境信息,便于后面解析配置时使用:
// 初始化环境容器
this.environment = environment;
然后,保存当前使用的Xml地址解析器:
// 初始化Xml地址解析器
this.parser = parser;
到这,就完成了XMLConfigBuilder
对象的构造过程,除了在构造方法中涉及到的几个属性外,XMLConfigBuilder
还有一个常量属性localReflectorFactory
,该属性在代码中硬编码为DefaultReflectorFactory
,他的作用后面我们会学到,先不管他:
/**
* 用于创建{@link org.apache.ibatis.reflection.Reflector}对象的工厂
*/
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
现在我们先回过头来继续看XMLConfigBuilder
依赖的XPathParser
对象。
XPathParser
的构造方法有很多,除了需要用户传入对应的XML
文档之外,还可以接收一些其他可选传的参数。
在这里,传入的XML文档可以有多种形式,可以是XML的地址链接,输入流,甚至是Document对象都可以。
可选传的参数有三个:
第一个是
boolean
类型的validation
,该参数表示是否对XML
文档进行DTD
校验,默认值为false
。第二个参数是
Properties
类型的variables
,该参数表示用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用。第三个参数是
EntityResolver
类型的entityResolver
对象,该参数的作用是为调用方寻找DTD验证文件。EntityResolver
这个参数我们前面有提到过,他在XmlConfigBuilder
构造方法中被硬编码为XMLMapperEntityResolver
类型的对象。new XPathParser(reader, true/*是否进行DTD校验*/ , props /*参数*/ , new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/ )/*构建一个XPath解析器*/
他的代码比较简单,定义了六个常量:
/** * IBATIS 全局配置文件的systemId(用于兼容IBATIS) */ private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd"; /** * IBATIS Mapper配置文件的systemId(用于兼容IBATIS) */ private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd"; /** * MYBATIS 全局配置文件的systemId */ private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd"; /** * MYBATIS Mapper配置文件的systemId */ private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd"; /** * MYBATIS全局配置文件对应的DTD文件 */ private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd"; /** * MYBATIS MAPPER 配置文件对应的DTD文件 */ private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
同时实现了
EntityResolver
的resolveEntity
方法,在该方法中,XMLMapperEntityResolver
会为传入的systemId
匹配一个对应的DTD
验证文件:/** * 从本地获取DTD文件 * * @param publicId 公共标志符号 * @param systemId 系统标志符 * @return DTD */ @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException { try { if (systemId != null) { String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH); if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) { // mybatis全局配置文件`config.xml`对应的DTD文件 return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId); } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) { // mybatis数据映射配置文件`*Mapper.xml`对应的DTD文件 return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId); } } return null; } catch (Exception e) { throw new SAXException(e.toString()); } }
- systemId: 系统标志符,通常用于直接指出对应DTD资源的实际位置,往往是应用程序范围内唯一的。
- publicId: 公共标志符,是一个全球唯一的标志,通常与特定的DTD关联,间接的指出DTD文件的位置,通常用于跨多个应用的场景。
其实
EntityResolver
及其实现类XMLMapperEntityResolver
是一个标准的策略模式实现。策略模式是一种常见的行为性设计模式,作用:我们可以定义一系列的算法,并将这些算法封装起来,使他们可以互相替换,策略模式让算法可以独立于使用他们的客户端而独立变化.
因为我们在创建sqlSessionFactory
对象时,传入的是配置文件的字节流,所以这里实际调用XPathParser
的构造方法是XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver)
(其余的构造方法和该方法代码类似):
/**
* XPathParser
*
* @param inputStream XML文件输入流
* @param validation 是否进行DTD验证
* @param variables 用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用
* @param entityResolver 自定义寻找DTD验证文件的解析器
*/
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
// 调用通用的构造方法
commonConstructor(validation, variables, entityResolver);
// 将用户传入的XML文件流转换为Document对象
this.document = createDocument(new InputSource(inputStream));
}
这个构造方法中的代码可以分为两个部分,一个是用于初始化属性的通用方法:commonConstructor
,另一个是用来解析获取XML文件对应的Document
对象的方法:createDocument
.
其中commonConstructor
方法的内容相对比较简单,唯一值得一提的就是为xpath
属性硬编码赋值了XPath
类型的对象实例,代码如下:
/**
* 公共构造参数,主要是用来配置DocumentBuilderFactory
*
* @param validation 是否进行DTD校验
* @param variables 属性配置
* @param entityResolver 实体解析器
*/
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
// 是否进行DTD校验
this.validation = validation;
// 配置XML实体解析器
this.entityResolver = entityResolver;
// 属性配置
this.variables = variables;
// 初始化XPath
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
createDocument
方法则主要是根据当前的配置解析XML
文件获取对应的Document
对象:
/**
* 根据输入源,创建一个文本对象
*
* @param inputSource 输入源
* @return 文本对象
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 是否验证被解析的文档
factory.setValidating(validation);
// 是否提供对XML命名空间的支持
factory.setNamespaceAware(false);
//忽略注释
factory.setIgnoringComments(true);
//忽略空白符
factory.setIgnoringElementContentWhitespace(false);
//是否解析CDATA节点转换为TEXT节点
factory.setCoalescing(false);
//是否展开实体引用节点
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// 配置XML文档的解析器,现在主要是XMLMapperEntityResolver
builder.setEntityResolver(entityResolver);
// 配置错误处理器
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
// 解析出DOM树
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
在执行完这两个方法之后,XPathParser
的构造过程就已经结束了,这时候XPathParser
不仅持有Document
对象还持有一个Xpath
访问器,接下来XPathParser
的很多操作都会使用Xpath
来读取Document
对象中的内容。
下面是XPathParser
对象的属性定义:
public class XPathParser {
/**
* XML对象
*/
private final Document document;
/**
* 是否进行DTD验证
*/
private boolean validation;
/**
* DTD实体解析器
*/
private EntityResolver entityResolver;
/**
* 用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用。
*/
private Properties variables;
/**
* XML地址访问器
*/
private XPath xpath;
}
到这里,XMLConfigBuilder
对象及其依赖的对象的构造过程基本上就完成了,接下来就是执行解析mybatis配置文件的工作了。