Mybatis源码分析之Configration类 之XML的解析利器XpathParase

前言

   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供我们使用,

而且从以上的代码学习里,我们最应该深有体会的是封装的好处,即方便调用,以及原方法的拦截处理。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值