前言
MyBatis框架底层是JDBC,它的所有代码都围绕着JDBC的执行流程而展开的(当然Mybatis也向里面加了一点东西,这些东西我们先不要管,因为会影响我们的分析),分析Mybatis的源码,必须要熟记JDBC的执行流程及关键点,JDBC执行的过程示例及关键因素如下
//====== 需要准备的信息 这些信息都是需要变动的============= //数据库配置信息 Properties map = new Properties(); map.put("username", "root"); map.put("driver", "数据库驱动"); map.put("url", "数据库地址"); map.put("password", "密码"); map.put("password", "密码"); //要执行的SQl String sql="xxxx"; //=====jdbc的核心Api========= //Connection连接器 Connection connection = DriverManager.getConnection(map.getProperty("url"), map.getProperty("username"), map.getProperty("password")); //事务 connection.setAutoCommit(false); //创建执行器 Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_UPDATABLE); //执行Sql boolean b = statement.execute("XXXXX"); //返回结果集 ResultSet resultSet = statement.getResultSet(); //提交事务 connection.commit(); //关闭链接 connection.close();
当我们以硬编码的方式执行MyBatis的逻辑时,只需要执行以下代码即可完成相关的数据库查询
ClassPathResource resource = new ClassPathResource("config/config.xml"); SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(reource.getInputStream()); sqlSessionManager.openSession(true); // SqlSession sqlSession = sqlSessionFactory.openSession(true); //BlogMapper为我们自定义的接口。 BlogMapper myMapper = sqlSession.getMapper(BlogMapper.class); //接下来省略掉调用BlogMapper方法的代码 ......
在上述代码里,我们没有调用jdbc的任何Api就完成了数据库的查询,那么很显然是SqlSession封装了JDBC的执行流程。如果我们不看SqlSession的源码,按照自己的思路分析,我们该如何封装这个流程?一个最基本的原则,动静分离,所谓动静分离,便是将变化的部分与不变的部分摘离出来。那么我们回头看,JDBC里有哪些动态变化的部分呢?从JDBC执行流程的代码里可以看出来,变化的部分分别是:数据库配置信息、需要执行的Sql语句、是否开启事务;不变的部分、Connection、Statement、ResultSet。Mybatis是如何处理这些的呢。
XPathParaser的分析前言
Mybatis提供了通过配置文件,完成动态信息的配置。那么,Mybatis的是如何解析这些XML的?我们都知道java解析xml的工具,分别有DOM、SAX、JDOM、DOM4j等技术,而Mybatis
则选了Dom4j进行解析,Dom4j技术简单来说,可以理解成,其将整个XML的内容的,以Element为一个节点的形式,组成一个树状的数据结构。使用Dom4j解析成树之后,如何完成节点的定位,这就是XPath组件。XPath技术主要通过XPath语法,完成对这棵树完成搜索和解析。
Xpath的Api使用
抛开Mybatis,XPath是如何使用的呢?Xpath的创建案例过程如下:
/**
* 先创建Dom4j,获取Dom4j的解析对象Document
*/
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
/**
* 加载类路径下的二进制流
*/
ClassPathResource classPathResource = new ClassPathResource("xml对应的路径");
InputStream inputStream = classPathResource.getInputStream();
/**
* 创建Dom4j的Document
*/
Document document = builder.parse(inputStream);
/**
* 创建Xpath对象
*/
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
/**
* 执行Xpath的语法表达式,从Document对象中获取想要的数据类型
*/
Object object = xpath.evaluate("xpath的语法表达式",document,XPathConstants.NODESET);
从上面示例中我们可以看到,对于XPath对象的创建,只需要提供一个XML的路径或者XML对应的流即可。而对于XPath的使用方法,他主要提供了evaluate完成搜索解析操作,evaluate拥有多个重载方法,最具有代表性的是
public Object evaluate(String expression, Object item, QName returnType)
这个方法拥有三个参数:
第一个expression指的是Xpath的语法表达式,这个可以参考XPath的语法,这个语法我们没必要全部掌握,只掌握基础的节点定位即可。
第二个参数是Object item,他可以是Document对象或者他的node节点,这里我们将其当做Document就可以了。
第三个参数指的是通过表达式搜索,返回的结果类型Xpath一共提供了Stirng、Number、Boolean、Node、NodeList等,但是需要注意的是,这几个返回值类型和我们所想的返回是不同的。
比如:有这么几个节点
第一段:
<select id="12333" resultMap="s">
select * from <include refid="">s<ele>sssss</ele>ss</include> dffo tes <![CDATA[ birth_day <= #{dayStart} ]]>
</select>
第二段:
<select>123</select>
第三段
<select>false</select>
如果我们用表达式expression为/select,
1.如果returnType为String,那么返回值是 select * from ssssssss dffo tes birth_day <= #{dayStart},也就是说,String是返回节点下所有的文本信息。
2.如果使用returnType为number,则返回NaN,如果修改表达式为/select[2],则返回的是123.0而且它对应的double类型的对象。
3.如果是Boolen,这个和我们想的类型有所偏差,如果修改表达式为/select[2]它将返回true,如果表达式为为/select[3],那么他返回的也是true,如果表达式改为/select[4],返回的是false,也就说,这个类型是判断expression对应的节点是否存在的。
4.至于Node和NodeList我们很好理解了,如果你选Node,那么他返回的是第一个select节点,如果你选择NodeList,那么他返回的是所有的select节点。
至此,XPath的大概用法,我们就介绍完了。
从示例代码里我们可以到,示例中除了XML对应的路径信息,以及Xpath的语法表达式、XPath的返回值等变量外,其它Api是固定不变的。那么为了书写方便(所谓的方便就是可以写更少的代码,来完成这个XPath的功能),我们想当然的会将这些代码封装起来,将变化的部分暴露出来。
说到这个封装,大家是不是感觉这个和Mybatis封装JDBC的套路很相似?既然Mybatis都封装了JDBC,那么像这个XML解析工具,他自然也会选择封装了。
Mybatis的Xml解析利器-XPathParaser
Mybatis对XPath代码进行了封装,封装的对象就是XPathParser,XPathPathParaser就是Mybatis迎来解析XML的工具,那么它是如何封装的呢?Mybatis的XPathParser提供了多个构造参数,但是最终来看无论是第String xml、Reader、还是Inputstream,其实他们最终都调用了createDocument创建了Document对象。
那么createDocument方法是做什么的,它的源码如下,我们终于发现了Dom4j的创建流程,这里还有两个参数EntityResolver和boolean类型的validation,这两个参数就是构造方法里的EntityResolver和boolean的validation入参,至于他的作用,去搜索Dom4j即可。
dom4j的代码找到了,那么Xpath的代码在哪里呢?我们看到构造函数里,第一个调用的方法是commonConstructor,在这里面就封装了Xpath的代码。
经过这些封装,XpathParase就具备了Xpath的所有的能力。但是经过一下的源码分析,我们发现还有一个参数,即Properties类型的variables没有用到,那么很显然,这个参数一定和XpathParase提供超越了Xpath本身的功能有关,那么这个功能是什么呢?这个在分析完其封装了Xpath本身功能后,再来分析它的作用。
XPath提供的方法
XpathParase提供的功能进行分类归纳,一共来说可以分为如下几类:
第一类:使用XPath的evaluate,returnType为 XPathConstants.STRING
将节点内容解析恒Short、Integer、Long、Float、String等,为什么会将这几个单独拿出来说,那是因为,这个个底层都调用了同一个方法。 我们可以看到无论解析成Short、Integer、Long还是Float,他底层都调用evalString这个方法。
而evalString方法代码如下,可以到,它的底层是调用的XPath的evaluate方法,而返回值类型为String。它同时还是用了PropertyParser进行了拦截,
关于PropertyParser的使用我们不再本文展开(展开讲它的内容,请参考PropertyParser),只知道它这个parse方法是为了处理${}符的。
总结1:因此我们可以总结,这一类方法的作用,就是直接获取xml的内容,然后转化成对应的值。
第二类使用XPath的evaluate,returnType为 XPathConstants.NUMBER
将节点内容解析成Double,为什么Float是通过String实现。
第三类使用XPath的evaluate,returnType为 XPathConstants.BOOLEAN
根据前面说的,这个方法没什么好说的,也就是说evalBoolean的方法是判断expression表达式是否可以定位到正确的node节点。
第四类使用XPath的evaluate,returnType为 XPathConstants.NODE
这个方法是获取到Dom4j的节点,但是XPathParaser,对这个Node节点进行了进一步的封装,目的自然是为了获取,封装成了XNode,
XNode的作用,我们在讲完第五类使用后进行分析。
第五类使用XPath的evaluate,returnType为 XPathConstants.NODESET
这个方法没什么好说的,和第四类方法一样,同样接node节点进行了封装。
总结:
从上面的分析方法中,我们可以发现XPathParaser对XPath的封装都很简单。但是通过对方法的处理,我们也可以很明显的感受到,封装方法的好处,即对原有的方法进行拦截,从而赋予新的功能,这也是我们真正需要体会和学习的。
XNode源码分析
XNode是一个相对封闭的类,这个的作用是要是用来存储Node的相关数据,那么究竟是哪些数据被XNode封装了呢?
我们总结下来分别有这么几类方法:
第一类:获取XNode的基本属性,这里的主要是name、atrributes、body的获取。
name属性代表意义:
name=node.getNodeName();可以看出来name代表属性名称。
attributes属性的代表意义
attributes的意义就是解析标签的所有属性,以key/value形式封装到Properties里
body属性的代表意义
body的意义代表如果本node的类型是 Node.CDATA_SECTION_NODE和Node.TEXT_NODE类型,则获取它的文本内容,但如果不是,则获取它的第一个该类型的子节点对应的内容。
第二类基于body的内容封装
分别做body做了boolean、int、long、double、float、String类型转换。
/**
* 获取XNode的第一个TEXTNode的内容
* @return
*/
public String getStringBody() {
return getStringBody(null);
}
public String getStringBody(String def) {
if (body == null) {
return def;
} else {
return body;
}
}
public Boolean getBooleanBody() {
return getBooleanBody(null);
}
/**
* 这个是获取
*
* @param def
* @return
*/
public Boolean getBooleanBody(Boolean def) {
if (body == null) {
return def;
} else {
return Boolean.valueOf(body);
}
}
/**
* 与XpathParase的如出一辙
* @return
*/
public Integer getIntBody() {
return getIntBody(null);
}
public Integer getIntBody(Integer def) {
if (body == null) {
return def;
} else {
return Integer.parseInt(body);
}
}
public Long getLongBody() {
return getLongBody(null);
}
public Long getLongBody(Long def) {
if (body == null) {
return def;
} else {
return Long.parseLong(body);
}
}
public Double getDoubleBody() {
return getDoubleBody(null);
}
public Double getDoubleBody(Double def) {
if (body == null) {
return def;
} else {
return Double.parseDouble(body);
}
}
public Float getFloatBody() {
return getFloatBody(null);
}
public Float getFloatBody(Float def) {
if (body == null) {
return def;
} else {
return Float.parseFloat(body);
}
}
第三类基于atrribute的内容封装
分别做atrribute做了boolean、int、long、double、float、String、枚举类型转换。
/**
* 获取属性名称为{@code name}的值,这个值对应着{@code enumType}枚举类的一个入参
* @param enumType
* @param name
* @param <T>
* @return
*/
public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name) {
return getEnumAttribute(enumType, name, null);
}
public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name, T def) {
String value = getStringAttribute(name);
if (value == null) {
return def;
} else {
return Enum.valueOf(enumType, value);
}
}
public String getStringAttribute(String name) {
return getStringAttribute(name, null);
}
/**
* 解析器 获取name对应的值 如果name对应的值不存在 则使用ref
*
* @param name
* @param def
* @return
*/
public String getStringAttribute(String name, String def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return value;
}
}
public Boolean getBooleanAttribute(String name) {
return getBooleanAttribute(name, null);
}
public Boolean getBooleanAttribute(String name, Boolean def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return Boolean.valueOf(value);
}
}
public Integer getIntAttribute(String name) {
return getIntAttribute(name, null);
}
public Integer getIntAttribute(String name, Integer def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return Integer.parseInt(value);
}
}
public Long getLongAttribute(String name) {
return getLongAttribute(name, null);
}
public Long getLongAttribute(String name, Long def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return Long.parseLong(value);
}
}
public Double getDoubleAttribute(String name) {
return getDoubleAttribute(name, null);
}
public Double getDoubleAttribute(String name, Double def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return Double.parseDouble(value);
}
}
public Float getFloatAttribute(String name) {
return getFloatAttribute(name, null);
}
public Float getFloatAttribute(String name, Float def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return Float.parseFloat(value);
}
}
第四类使用XPathParase对子节点进行定位封装。
XNode使用XPathParase对子节点定位进行了封装,调用者只需传入表达式即可。
/**
* 这里复用了XPathParaser的方法
* @param expression
* @return
*/
public String evalString(String expression) {
return xpathParser.evalString(node, expression);
}
public Boolean evalBoolean(String expression) {
return xpathParser.evalBoolean(node, expression);
}
public Double evalDouble(String expression) {
return xpathParser.evalDouble(node, expression);
}
public List<XNode> evalNodes(String expression) {
return xpathParser.evalNodes(node, expression);
}
public XNode evalNode(String expression) {
return xpathParser.evalNode(node, expression);
}
第五类对node路径获取封装
对Node的路径获取封装,分别包括获取parent节点、获取从根节点到该节点的路径、获取节点唯一标识符等三个方法。
获取parent节点很好理解,我们在解析XML的某个节点时,难免会用到它的父节点做一些东西,既然XML的节点进行了XNode的封装,父节点当然也要使用XNode封装,以减少我们的工作量。getParent在Mybatis里发挥作用的地方就是处理<sql></sql>标签和<include></include>标签的时候。
public XNode getParent() {
Node parent = node.getParentNode();
if (!(parent instanceof Element)) {
return null;
} else {
return new XNode(xpathParser, parent, variables);
}
}
/**
* 获取节点的路径 暂未用到
*
* @return
*/
public String getPath() {
StringBuilder builder = new StringBuilder();
Node current = node;
while (current instanceof Element) {
if (current != node) {
builder.insert(0, "/");
}
builder.insert(0, current.getNodeName());
current = current.getParentNode();
}
return builder.toString();
}
/**
* 代码执行过程,对{@link java.lang.StringBuilder}使用了头插法
*
* 依次形成了从顶级parent到本Node节点的唯一性标识对应的value组成的值
* 这个值的结构形成如下[顶级parent对应的唯一标识值]_[下一级对应的值]_[再下一级]...[父类node对应的]_[自己的]
*
* 该方法在Mybatis解析xml出错时,可以做异常信息定位抛出来
* @return
*/
public String getValueBasedIdentifier() {
StringBuilder builder = new StringBuilder();
XNode current = this;
while (current != null) {
if (current != this) {
builder.insert(0, "_");
}
//获取节点的唯一性标识,这个标识首选时属性为id,其次时value,然后时property对应的值
String value = current.getStringAttribute("id",
current.getStringAttribute("value",
current.getStringAttribute("property", null)));
if (value != null) {
value = value.replace('.', '_');
builder.insert(0, "]");
builder.insert(0,
value);
builder.insert(0, "[");
}
builder.insert(0, current.getName());
current = current.getParent();
}
return builder.toString();
}
总结
经过以上源码分析,我们可以归纳总结出以下几个点:
1.XPathParaser内部封装了XPath的执行代码段,只曝露出简单的API供调用者使用。
2.XPathParaser使用XPath的语法进行节点定位,并且对对应节点的内容,分别以Stirng、Number、Boolean、Node、NodeList等几种类型返回。这里尤其需要注意的是Boolean类型的返回
它的含义并不是代表着节点的内容是不是Boolean类型解析,而是代表着节点是否存在。当然,如果我们想要获取对节点内容的封装,使用XNode的getxxBody方法即可。
3.XPathParase对于返回值为Node、NodeList类型的语法进行了封装,将Node节点的相关信息,封装到了XNode里,并暴露出一些常用的API供我们使用,
而且从以上的代码学习里,我们最应该深有体会的是封装的好处,即方便调用,以及原方法的拦截处理。