30分钟教你写一个mybatis框架

Posted 顶风少年的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了30分钟教你写一个mybatis框架相关的知识,希望对你有一定的参考价值。

XMLConfigBuilder解析mybatis配置文件,创建一个Configuration对象,该对象是mybatis的核心配置类。对配置文件中的<environments>标签解析,<environments>包含多个<environment>每个包含<dataSource>根据<environments>标签的default属性选择一个environment,读取对应的<dataSource>配置信息。根据<dataSource>的type属性,确定要使用的连接池。使用<dataSource>中配置的数据库信息进而创建数据源DataSource,将DataSource设置到Configuration中。

技术图片
<configuration>
    <!-- mybatis 数据源环境配置 -->
    <environments default="dev">
        <environment id="dev">
            <!-- 配置数据源信息 -->
            <dataSource type="DBCP">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/petstore?serverTimezone=GMT%2B8&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>



    <!-- 映射文件加载 -->
    <mappers>
        <!-- resource指定映射文件的类路径 -->
        <mapper resource="mapper/manageUser.xml"></mapper>
        <!-- <mapper resource="mapper/UserMapper.xml"></mapper> -->
        <!-- <mapper resource="mapper/UserMapper.xml"></mapper> -->
        <!-- <mapper resource="mapper/UserMapper.xml"></mapper> -->
        <!-- <mapper resource="mapper/UserMapper.xml"></mapper> -->
    </mappers>
</configuration>
View Code
技术图片
package builder;

import mapping.Configuration;
import org.apache.commons.dbcp.BasicDataSource;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;
import java.util.Properties;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-08 10:42
 * @notify 解析mybatisConfig的类,当前解析 environments  mappers
 * 创建Configuration 设置DataSource
 *  mappers 则获取输入流,交给XMLMapperBuilder解析每一个mapper文件
 * @version 1.0
 */
public class XMLConfigBuilder {

    private Configuration configuration = new Configuration();

    public Configuration parse(Element rootElement) throws Exception {
        //解析<environments>
        Element environments = rootElement.element("environments");
        parseEnvironments(environments);
        //解析<mappers>
        Element mappers = rootElement.element("mappers");
        parseMappers(mappers);
        return configuration;
    }

    //解析<environments>
    private void parseEnvironments(Element environments) {
        //查询environments default="dev"
        String aDefault = environments.attributeValue("default");
        //获取全部的 environment
        List<Element> environment = environments.elements("environment");
        //循环所有的 environment
        for (Element env : environment) {
            //如果当前 environment 的id和默认的id相同则继续向下解析
            if (env.attributeValue("id").equals(aDefault)) {
                parseEnvironment(env);
            }
        }
    }

    //解析environment
    private void parseEnvironment(Element environment) {
        //解析<dataSource type="DBCP">
        Element dataSource = environment.element("dataSource");
        parseDataSource(dataSource);
    }

    //解析dataSource
    private void parseDataSource(Element dataSource) {
        //获取连接池类型
        String type = dataSource.attributeValue("type");
        //设置 <dataSource type="DBCP"> 连接池
        if (type.equals("DBCP")) {
            //创建 DBCP连接池
            BasicDataSource dataSource1 = new BasicDataSource();
            //创建配置类
            Properties properties = new Properties();
            //获取全部的property
            List<Element> propertys = dataSource.elements("property");
            //循环拿到<property name="driver" value="com.mysql.jdbc.Driver"/>
            for (Element prop : propertys) {
                //获取标签name属性值
                String name = prop.attributeValue("name");
                //获取标签value属性值
                String value = prop.attributeValue("value");
                //设置到配置类
                properties.put(name, value);
            }
            //设置连接池属性
            dataSource1.setDriverClassName(properties.get("driver").toString());
            dataSource1.setUrl(properties.get("url").toString());
            dataSource1.setUsername(properties.get("username").toString());
            dataSource1.setPassword(properties.get("password").toString());
            //给Configuration设置数据源信息
            configuration.setDataSource(dataSource1);
        }
    }

    //解析<mappers>
    private void parseMappers(Element mappers) throws Exception {
        //拿到所有的<mapper resource="mapper/UserMapper.xml"></mapper>
        List<Element> mapperElements = mappers.elements("mapper");
        //遍历解析每一个 mapper.xml
        for (Element mapperElement : mapperElements) {
            parseMapper(mapperElement);
        }
    }

    //解析每一个mapper标签
    private void parseMapper(Element mapperElement) throws Exception {
        //TODO 此处还有url等方式
        String resource = mapperElement.attributeValue("resource");
        //根据文件名获取输入流
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resource);
        //dom4j解析
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(inputStream);
        //获取跟标签
        Element rootElement = document.getRootElement();
        XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(configuration);
        mapperBuilder.parse(rootElement);
    }


}
View Code

循环<mappers>标签获取多个<mapper>循环解析mapper配置文件,使用XMLMapperBuilder类获取每个mapper配置文件的namespace,在解析不同的sql标签(<insert><select>)。

技术图片
<mapper namespace="manageUser">
    <!-- select标签,封装了SQL语句信息、入参类型、结果映射类型 -->
    <select id="getManageUserById"
            parameterType="pojo.ManageUser"
            resultType="pojo.ManageUser" statementType="prepared">
        SELECT * FROM manage_user WHERE id = #{id}
    </select>

    <insert  id="insertManage"
             parameterType="pojo.ManageUser"
             statementType="prepared">
        insert into manage_user values(#{id},#{username},#{password},#{create_date});
    </insert>

    <select id="getManageUserByUserName"
            parameterType="pojo.ManageUser"
            resultType="pojo.ManageUser" statementType="prepared">
        SELECT * FROM manage_user WHERE id = #{id}
        <if test="username!=null and username!=‘‘">
            and username = ${username}
        </if>
    </select>
</mapper>
View Code
技术图片
package builder;


import mapping.Configuration;
import org.dom4j.Element;

import java.util.List;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-08 11:13
 * @notify 解析mapper文件获取namespace,读取到<insert>和<select>标签,交给XMLStatementBuilder
 * @version 1.0
 */
public class XMLMapperBuilder {

    private String namespace = "";

    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    //解析每一个mapper文件
    public void parse(Element rootElement) throws Exception {
        //查询namespace
        namespace = rootElement.attributeValue("namespace");
        //获取select标签
        List<Element> selectElements = rootElement.elements("select");
        parse(selectElements, "select");

        List<Element> insertElements = rootElement.elements("insert");
        parse(insertElements, "insert");

    }

    public void parse(List<Element> selectElements, String sqlType) throws Exception {
        for (Element selectElement : selectElements) {
            XMLStatementBuilder xmlStatementBuilder = new XMLStatementBuilder(configuration);
            xmlStatementBuilder.parseStatementElement(selectElement, namespace, sqlType);
        }
    }


}
View Code

XMLStatementBuilder类解析具体的sql标签。每一个sql标签都是一个MappedStatement对象,而每一个mapper文件中有N个sql标签,一个项目又有M个mapper文件。所以一个Configuration中有一个map,key是statementid,(由mapper文件的namespace和sql标签的id组成)value是MappedStatement。一个MappedStatement对象由statementid,入参类型,返回值类型,sqlType属于<select>还是<insert>等等 statementType的值分别对应:statement不进行预编译,prepared预编译,callable执行存储过程。最后还有一个专门用来存储sql语句的对象SqlSource,SqlSource是一个接口。

技术图片
package builder;

import mapping.Configuration;
import mapping.MappedStatement;
import org.dom4j.Element;
import sqlnode.impl.MixedSqlNode;
import sqlsource.SqlSource;
import sqlsource.impl.DynamicSqlSource;
import sqlsource.impl.RawSqlSource;
import utils.ResolveType;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-08 11:24
 * @notify 解析<insert><select>标签,读取入参,返回值,id等信息,标签内容交给XMLScriptBuilder
 * @version 1.0
 */
public class XMLStatementBuilder {
    private Configuration configuration;

    public XMLStatementBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    //解析标签
    public void parseStatementElement(Element selectElement, String namespace, String sqlType) throws Exception {
        //读取id
        String statementId = selectElement.attributeValue("id");
        //如果id不存在则返回
        if (statementId == null || selectElement.equals("")) {
            return;
        }
        //拼接namespace
        statementId = namespace + "." + statementId;
        //查询 parameterType 属性
        String parameterType = selectElement.attributeValue("parameterType");
        //通过类名获取Class
        Class<?> parameterClass = ResolveType.resolveType(parameterType);
        //查询 resultType 属性
        String resultType = selectElement.attributeValue("resultType");
        Class<?> resultClass = null;
        if (resultType != null && !resultType.equals("")) {
            //通过类名获取Class
            resultClass = ResolveType.resolveType(resultType);
        }

        //获取statementType属性
        String statementType = selectElement.attributeValue("statementType");
        //设置默认的statementType属性
        statementType = statementType == null || statementType == "" ? "prepared" : statementType;
        // 解析SQL信息
        SqlSource sqlSource = createSqlSource(selectElement);

        // TODO 建议使用构建者模式去优化
        MappedStatement mappedStatement = new MappedStatement(statementId, parameterClass, resultClass, statementType,
                sqlSource, sqlType);
        //设置Configuration参数
        configuration.addMappedStatement(statementId, mappedStatement);

    }

    //获取sqlSource
    private SqlSource createSqlSource(Element selectElement) throws Exception {
        XMLScriptBuilder xmlScriptBuilder = new XMLScriptBuilder();
        SqlSource sqlSource = xmlScriptBuilder.parseScriptNode(selectElement);
        return sqlSource;
    }


}
View Code

XMLScriptBuilder解析sql脚本,这一部分较为复杂。首先我们先明确,我们需要解析的内容是sql标签中的标签体,也就是sql脚本,需要将sql脚本组成一个SqlSource。

<select>
  select * from manage_user whereid = #{id}
  <if test="username!=null and username!=‘‘">
    and username = ${username}
  </if>
</select>

这样的sql脚本包含两个节点,一个是只包含普通的文本的sql节点,另一个则是if标签的sql节点。当然,真实的mybatis还包含<where>等节点。封装sql节点引入一个接口,SqlNode。每种节点最终都需要放到SqlSource中,我们可以在SqlSource中使用一个集合来存储,但是我们还有一个更好的选择,使用MixedSqlNode。现在MixedSqlNode中存储多个SqlNode根据不同的节点不同,我们将是文本节点和包含${}的节点封装成TextSqlNode,将只包含文本且只包含#{}的节点封装成StaticTextSqlNode。较为麻烦的是if标签,因为一个if标签里可能会包含带有#{}的文本内容,或者带有${}的文本内容,或许,if标签里还有if标签。这里我们必须要用到递归解析了。我们假设if标签中包含TextSqlNode和另一个if标签,此时我们就需要把两个标签放到if标签中,辛好我们有MixedSqlNode,于是if标签中的test表达式和MixedSqlNode组成了IfSqlNode现在我们解析了全部的sql脚本将所有的SqlNode封装到MixedSqlNode,然后组装一个SqlSource。前边我们SqlSource是一个接口,现在我们将sql脚本中只包含StaticTextSqlNode节点的SqlSource封装成RawSqlSource,而包含TextSqlNode和IfSqlNode节点SqlSource封装成DynamicSqlSource

技术图片
package builder;/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-08 11:30
 * @notify
 * @version 1.0
 */

import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.Text;
import sqlnode.SqlNode;
import sqlnode.impl.IfSqlNode;
import sqlnode.impl.MixedSqlNode;
import sqlnode.impl.StaticTextSqlNode;
import sqlnode.impl.TextSqlNode;
import sqlsource.SqlSource;
import sqlsource.impl.DynamicSqlSource;
import sqlsource.impl.RawSqlSource;

import java.util.ArrayList;
import java.util.List;

public class XMLScriptBuilder {

    private boolean isDynamic = false;

    //解析标签体
    public SqlSource parseScriptNode(Element selectElement) throws Exception {
        MixedSqlNode mixedSqlNode = parseDynamicTags(selectElement);
        SqlSource sqlSource;
        if (isDynamic) {//如果包含${}或者其他的子标签则为动态的
            sqlSource = new DynamicSqlSource(mixedSqlNode);
        } else {//全部的sqlNode都是文本,并且只包含#{}
            sqlSource = new RawSqlSource(mixedSqlNode);
        }
        return sqlSource;
    }


    private MixedSqlNode parseDynamicTags(Element selectElement) {
        //存储一个 <select> 中的所有sqlNode
        List<SqlNode> sqlNodes = new ArrayList<>();
        //查询总结点数量
        int nodeCount = selectElement.nodeCount();
        //遍历全部的sql节点
        for (int i = 0; i < nodeCount; i++) {
            //获取当前节点
            Node node = selectElement.node(i);
            //如果是纯文本的
            if (node instanceof Text) {
                //拿到文本节点
                String sqlText = node.getText().trim();
                if (!sqlText.equals("")) {
                    //如果包含 ${} 则创建 TextSqlNode 该节点只包含文本和${}
                    if (sqlText.indexOf("${") > -1) {
                        //如果包含${}或者其他的子标签则为动态的
                        isDynamic = true;
                        //将TextSqlNode添加到节点集合中
                        sqlNodes.add(new TextSqlNode(sqlText));
                    } else {
                        //将StaticTextSqlNode添加到节点集合中
                        sqlNodes.add(new StaticTextSqlNode(sqlText));
                    }
                }
            } else if (node instanceof Element) {
                //如果包含${}或者其他的子标签则为动态的
                isDynamic = true;
                //拿到节点名称
                String nodeName = node.getName();
                //如果是 if则表示是if标签
                if (nodeName.equals("if")) {
                    //将node转换成element
                    Element element = (Element) node;
                    //拿到if的条件
                    String test = element.attributeValue("test");
                    /*
                    此处递归调用,因为if标签中还有子节点
                    设sql为
                    select * from user
                    <if test=‘name!=null and name!=‘‘‘>
                        and name = #{name}
                    </if>
                    此时 select * from user 已经转换成了sqlNode
                    接下来的if标签,里边也包含子节点,所以递归,
                    第二次进入 (parseDynamicTags)
                    会创建一个StaticTextSqlNode,将该SqlNode添加到
                     List<SqlNode> sqlNodes = new ArrayList<>();
                     而这个集合最终会被封装成一个MixedSqlNode返回到第一次
                     调用(parseDynamicTags),所以此处使用
                     MixedSqlNode接收,并将该MixedSqlNode传递给IfSqlNode,然后
                     添加到sqlNodes中
                    */
                    MixedSqlNode mixedSqlNode = parseDynamicTags(element);
                    sqlNodes.add(new IfSqlNode(test, mixedSqlNode));
                }
            }
        }
        //返回节点集合包装类
        return new MixedSqlNode(sqlNodes);
    }
}
View Code

以上过程只是对sql解析的大概描述。现在我们缕清思路,Configuration包含的是mybatis的全局配置,其中包含DataSource和所有的mapper配置文件信息。形成了一个MappedStatement,MappedStatement包含的是sql标签的属性和内容,属性包含id,入参,出参,statement类型等,内容则是多个SqlNode形成的SqlSource。我们没有使用集合来存储SqlNode,而是在SqlSource内部持有一个MixedSqlNode此类是SqlNode接口的实现类,MixedSqlNode里有一个list集合存放各种类型的SqlNode,例如TextSqlNode,IfSqlNode等。

SqlSource接口,该接口中只有一个方法。getBoundSql(Object param);返回一个BoundSql对象。我们先不管BoundSql是干嘛的。

技术图片
package sqlsource;
/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-04 14:41
 * @notify 获取sql语句
 * @version 1.0
 */

import mapping.BoundSql;

public interface SqlSource {
    /*
    传入参数,这个param就是sql的入参
    boundSQL 信息返回拼接的sql可能是${} 或者 #{} 如果是${}则可以直接获取sql
    如果是 #{} 还需要有方法获取 ?代表的属性。
     */
    BoundSql getBoundSql(Object param)throws Exception;
}
View Code

接着看SqlSource的实现类。

 

技术图片
package sqlsource.impl;

import mapping.BoundSql;
import mapping.DynamicContext;
import sqlnode.SqlNode;
import sqlsource.SqlSource;
import sqlsource.SqlSourceParser;
import tokenparser.GenericTokenParser;
import tokenparser.ParameterMappingTokenHandler;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-04 14:52
 * @notify #{} 中的内容进行处理 RawSqlSource只包含 StaticTextSqlNode
 * @version 1.0
 */
public class RawSqlSource implements SqlSource {

    private SqlNode mixedSqlNode;

    public RawSqlSource(SqlNode mixedSqlNode) throws Exception {
        this.mixedSqlNode = mixedSqlNode;
    }

    @Override
    public BoundSql getBoundSql(Object param) throws Exception {
        //执行该sqlSource中的所有sqlNode
        DynamicContext dynamicContext = new DynamicContext(null);
        mixedSqlNode.apply(dynamicContext);
        //将 #{ } 替换成 ? 然后将内容存储到 ParameterMapping 中
        SqlSourceParser parser = new SqlSourceParser(dynamicContext);
        SqlSource sqlSource = parser.parse();
        return sqlSource.getBoundSql(param);
    }

}
View Code

 

调用RawSqlSourcegetBoundSql(Object param);方法,创建了一个DynamicContext对象。这个对象有一个StringBuilder对象,和Object类型参数。

 

技术图片
package mapping;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-04 18:02
 * @notify 每个SqlSource有多个SqlNode,而DynamicContext负责将多个sqlNode解析出来的sql信息拼接到一块
 * @version 1.0
 */
public class DynamicContext {
    //多个sqlNode调用自己的apply()都把解析好的sql放到这里。
    private StringBuilder sb = new StringBuilder();
    //sql入参
    private Object param;

    public String getSql() {
        return sb.toString();
    }

    public DynamicContext(Object param) {
        this.param = param;
    }

    public void appendSql(String sqlText) {
        sb.append(sqlText);
        sb.append(" ");
    }

    public Object getParam() {
        return param;
    }

}
View Code

 

接着我们调用SqlNode的apply(DynamicContext context)。其实当前的SqlNode其实就是MixedSqlNode

 

技术图片
package sqlnode;


import mapping.DynamicContext;

/*
* @auther 顶风少年 
* @mail dfsn19970313@foxmail.com
* @date 2020-01-04 18:00
* @notify 
* @version 1.0
*/
public interface SqlNode {
    void apply(DynamicContext context)throws Exception;
}
View Code

 

技术图片
package sqlnode.impl;

/*
 * @auther 顶风少年
 * @mail dfsn19970313@foxmail.com
 * @date 2020-01-04 18:10
 * @notify 因为一个sqlSource有多个sqlNode 所以将所有的sqlNode封装成一个对象。
 * 集中的管理所有的sqlNode
 * @version 1.0
 */

import mapping.DynamicContext;
import sqlnode.SqlNode;

import java.util.List;

public class MixedSqlNode implements SqlNode {

    //封装SqlNode集合信息
    private List<SqlNode> sqlNodes;

    public MixedSqlNode(List<SqlNode> sqlNodes) {
        this.sqlNodes = sqlNodes;
    }

    /*
     * 对外提供对数据封装的操作
     * */
    @Override
    public void apply(DynamicContext context) throws Exception {
        //执行sqlSource中所有sqlNode的apply()不同的sqlNode有不同的解析方式。最终都会将自己解析的sql放到DynamicContext中
        for (SqlNode sqlNode : sqlNodes) {
            sqlNode.apply(context);
        }
    }
}
View Code

但我们调用MixedSqlNode的apply(DynamicContext context);其实就是循环调用每一个SqlNode自己的apply(DynamicContext context);前边我们说过,只包含StaticTextSqlNode节点的才能被封装成RawSqlSource而StaticTextSqlNode节点只包含文本内容且文本内容中只有#{}。所以apply方法

以上是关于30分钟教你写一个mybatis框架的主要内容,如果未能解决你的问题,请参考以下文章

Android开发之手把手教你写ButterKnife框架

手把手教你写DI_1_DI框架有什么?

教你十分钟构建好 SpringBoot + SSM 框架

手把手教你写一个简易的微前端框架

阿里P8大神教你十分钟构建好SpringBoot + SSM框架 成功晋升

Android开发之手把手教你写ButterKnife框架