有了昨天第一讲的mybatis的结构的猜想与设计,想必我们对mybatis的架构有了一个整体的了解,我们今天就来一步一步探索mybatis的源码解析。
我们在第一讲的时候,mybatis的官网提供了两种方式,我们来看下这两种方式:
xml方式:
不使用xml的方式:
看着这两种方式,我就有自己的疑惑了,这第一种和第二种有什么区别呢?
我们看下第二种方式,就是自己手动创建一个TransactionFactory和数据源DataSource,然后给configuration(configuration)这个类非常重要,等下重点去介绍一下。
我觉的第一种方式其实和第二种方式一样的,就是解析xml文件,把TransactionFactory和数据源DataSource在xml配置里面解析出来,放在configuration,然后就可以和不使用xml的时候用同一样的方法了。
带着我们的猜疑,我们来看下使用xml的时候构建SqlSessionFactory的build方法。
看下这三个箭头,我们应该很清楚了,我们的猜想果然没错,就是我们那个猜想。带着喜悦的心情,我们来看下这个xml是如何把这些配置对象转成这个Configuration,还有就是这个configuration类是个什么东西。
我们现在进入这个build方法:
首先他new了一个XMLConfigBuilder对象,这个对象是个什么东东呢?看名字觉得它应该是一个可以解析xml的类?
进去看下他的源码,看看是个什么鬼东西?
哦,原来是有一个XPathParser对象,这个XPathParser对象其实就是封装了jdk的xpath,对表达式进行解析,并转化成为指定的数据类型,其属性如下:
private Document document; private boolean validation; private EntityResolver entityResolver; private Properties variables; private XPath xpath;
document:要解析的xml文件被转化成的Document对象。
validation:获取document时是否要开启校验,开启校验的话会根据xml配置文件中定义的dtd文件校验xml格式,默认不开启校验。
entityResolver:实体解析器,用于从本地工程目录中引入dtd文件,而不是根据网络URL去加载校验文件。
variables:mybatis-config.xml配置文件中,<Properties>节点引入或定义的属性。
xpath:封装的XPath对象,用来解析表达式。
我们再来看下他的构造方法:
所有的构造方法做的事情都如下所示:
public XPathParser(InputStream inputStream, boolean validation, Properties variables) {
commonConstructor(validation, variables, null);
this.document = createDocument(new InputSource(inputStream));
}
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
createDocument()
方法源码如下:
// 3. 将InputSource对象转化为Document对象
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation); // 是否要校验由外界传入的值确定
factory.setNamespaceAware(false); // 如果要使用mybatis的XSD Schema,此处必须设为true,但源码里是false,说明官方默认用dtd来做校验,舍弃了XSD Schema
factory.setIgnoringComments(true); // 默认忽略注释
factory.setIgnoringElementContentWhitespace(false); // 只有开启校验才能去除xml中的空白节点,但是不知是否开启校验,所以这里设为了false
factory.setCoalescing(false);
factory.setExpandEntityReferences(true); // 默认开启使用扩展实体引用
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver); // 使用传入的EntityResolver对象
builder.setErrorHandler(new ErrorHandler() { // 定义解析xml文档的错误处理器,如果发生了错误或致命错误则直接抛出异常,如果是警告默认不做处理
@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 {}
});
return builder.parse(inputSource);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
【源码解析】
要解析一个xml文件为Document
,需要DocumentBuilder
类的支持,实际上DocumentBuilder
本身就支持多种形式的xml的解析(可看末尾的附录),这里之所以先统一转成InputSource
,大概是为了代码简洁一点吧!
DocumentBuilder
对象由DocumentBuilderFactory
创建,DocumentBuilderFactory
可以设置是否校验、是否开启命名空间、是否忽略注释空白节点等,详细可以看上面源码注释。
如果开启了xml格式校验,DocumentBuilder
就要设置实体解析器,这里使用了构造函数传入的实体解析器,如果解析xml过程中检测到格式不对或者其他报错,则需要抛出异常信息,所以这里又设置了错误处理器ErrorHandler
,对于错误和致命错误直接抛出异常,对于警告则忽略。
方法功能
通过上面的构造,接下来可以根据表达式解析获取document
中的特定节点了,XPathParser
提供了eval() 系列方法,根据需要调用指定类型的eval方法,所有的eval()方法都是对evaluate()
方法返回值的处理,该方法中调用了XPath
解析表达式,源码如下:
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
大多数eval*()都是返回一个常规Java数据类型的包装类,eg:
public Boolean evalBoolean(String expression) {
return evalBoolean(document, expression);
}
public Boolean evalBoolean(Object root, String expression) {
return (Boolean) evaluate(expression, root, XPathConstants.BOOLEAN);
}
XPathParser
还支持解析文档中的节点并将其封装成XNode
对象,如果表达式表示的是一个节点,则调用evalNode()
方法;如果表达式表示的是一组节点,则调用evalNodes()
方法,源码如下:
XNode的结构如下:
public class XNode {
private final Node node;
private final String name;
private final String body;
private final Properties attributes;
private final Properties variables;
private final XPathParser xpathParser;
}
public List<XNode> evalNodes(String expression) {
return evalNodes(document, expression);
}
public List<XNode> evalNodes(Object root, String expression) {
List<XNode> xnodes = new ArrayList<XNode>();
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
} else {
return new XNode(this, node, variables);
}
}
XPathParser
还支持解析文档中的节点并将其封装成XNode
对象,如果表达式表示的是一个节点,则调用evalNode()
方法;如果表达式表示的是一组节点,则调用evalNodes()
方法,源码如下:
public List<XNode> evalNodes(String expression) {
return evalNodes(document, expression);
}
public List<XNode> evalNodes(Object root, String expression) {
List<XNode> xnodes = new ArrayList<XNode>();
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
} else {
return new XNode(this, node, variables);
}
}
看来这么多是不是很懵逼,别着急,给你们来个测试用例看一下就ojbk了。
测试用例:
配置文件随便找一个xml的配置文件即可:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<!-- settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings-->
<settings>
<setting name="logImpl" value="log4j" /> <!-- maven项目输出debug日志设置 -->
<setting name="useActualParamName" value="true" />
</settings>
<typeAliases>
<package name="com.learn.ssm.chapter5.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${database.driver}"/>
<property name="url" value="${database.url}"/>
<property name="username" value="${database.username}"/>
<property name="password" value="${database.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。 -->
<!-- package name="com.learn.ssm.chapter5.mapper"/-->
<mapper resource="com/carry.mybatis/UserMapp.xml"/>
</mappers>
</configuration>
写一个测试方法:
package com.carry.mybatis.Test;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import java.io.InputStream;
public class TestXpath {
public static void main(String[] args) {
// 如果XML文件直接放在源目录(Source Directory)下,则可以直接解析
// 如果放在源目录下的子目录中,则必须加上相对工程路径
// InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("org/apache/ibatis/parsing/test/mybatis-config.xml");
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis.xml");
XPathParser xPathParser = new XPathParser(inputStream, false, null, null);
XNode xNode = xPathParser.evalNode("/configuration");
XNode xNode1 = xNode.evalNode("settings");
System.out.println(xNode1.getName());
System.out.println(xPathParser);
}
}
看下执行结果:
可以通过这个xNode.evalNode去一直获取
这个xml解析其实不是重点,我只是想让大家对这个流程比较熟悉而已,让大家清楚一点。这个逻辑没有必要深究,了解即可。我们就带着轻松愉快的心情来看看这个东东吧。
我们会调用这个org.apache.ibatis.builder.xml.XMLConfigBuilder#parse方法,我们来康康它干了啥。
哦,原来如此,把我们根路径的configuration拿到,再一步步解析就ok了,这个parse就可以返回这个org.apache.ibatis.session.Configuration对象,点进去盘它
这一些列的方法更好猜测,每一个大节点,都被他封装成了一个方法,然后配置到Configuration对象里面,这边我们就不一一看了,随便找一个康康吧,那就选择typeHandlerElement(root.evalNode("typeHandlers"));这个看下吧。
这边我有个疑问,这个typeHandler有人不知道吗?这个东西可厉害了啊。typeHandler简称类型处理器
对这个的介绍,我直接看官网给的介绍,说的很详细,也很易懂,我就不自己写了。
类型处理器(typeHandlers)
MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。
提示 从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。
你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。比如:
// ExampleTypeHandler.java @MappedJdbcTypes(JdbcType.VARCHAR) public class ExampleTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } }
<!-- mybatis-config.xml --> <typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers>
使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:
- 在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType="String");
- 在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。
可以通过两种方式来指定关联的 JDBC 类型:
- 在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType="VARCHAR");
- 在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。
当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。
最后,可以让 MyBatis 帮你查找类型处理器:
<!-- mybatis-config.xml --> <typeHandlers> <package name="org.mybatis.example"/> </typeHandlers>
注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。
你可以创建能够处理多个类的泛型类型处理器。为了使用泛型类型处理器, 需要增加一个接受该类的 class 作为参数的构造器,这样 MyBatis 会在构造一个类型处理器实例的时候传入一个具体的类。
//GenericTypeHandler.java public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> { private Class<E> type; public GenericTypeHandler(Class<E> type) { if (type == null) throw new IllegalArgumentException("Type argument cannot be null"); this.type = type; } ...
这一段都是官网上的介绍,我反正看明白了,我觉得大家应该现在对typeHandler了解了吧。那我们接着这个typeHandler是怎样放到那个Configuration对象里面的
这就很清楚了,如果你配置了自定义的类型处理就走if没有就把jdbcType和javaType拿出来,找到对应的type类型,最后放在一个org.apache.ibatis.type.TypeHandlerRegistry里面的allTypeHandlersMap里面
最终这个parse方法返回了一个Configuration对象的方法,而且这个对象还是携带了一些配置文件里面的信息的对象哦。
最终SqlSessionFactory对象构建完成了哦
看到这我们是不是觉得源码也太水了吧。不要着急,好戏刚刚开场,下面我们来进入源码的中心,看看几个设计模式是怎样使用的。
详情请看第三讲: