手写Mybatis框架

一、综述

(一)Mybatis执行流程

  Mybatis源码主流程如下图所示: 

     1、配置文件加载

      全局配置文件加载:加载数据库信息和Mapper.xml文件

    2、配置文件加载后返回一个SqlSessionFactory对象:对象中包含Configuration对象,该对象中包含所有的配置信息

    3、对外提供SqlSession接口,封装相关增删改查方法,供SqlSessionFactory调用

    4、对外提供Executor接口,真正的处理数据库操作

    5、从Configuration中获取对应的MappedStatement对象和入参,对sql进行解析,并执行

    6、将JDBC原生执行结果转换为xml文件中指定的返回结果

(二)MyBatis对象介绍

类名 作用 包含内容 子类/实现类 作用 包含内容  
Configuration MyBatis全量配置信息对象 数据库信息DataBase和MappedStatement对象的map集合        
MappenStatement 一个Mapper中方法的解析结果对象 statementId、入参对象、出参对象、执行方式、SqlSource        
SqlSource接口 提供获取BoundSql的方法   RowSqlSource 在构造时就使用入参SqlNode的apply获取对应sql信息,然后将#{}替换成 ?  包含一个SqlSource,该类型为StaticSqlSource  
DynamicSqlSource

1、在调用getBoundSql方法时,才使用属性SqlNode的apply方法获取sql

2、使用其属性SqlNode的apply方法拼装sql语句

3、将#{}替换成 ? 后封装成一个StaticSqlSource

3、使用StaticSqlSource中的getBoundSql方法获取BoundSql对象

SqlNode  
StaticSqlSource 上面的两种SqlSource最终解析后都会以StaticSqlSource形式存在,只不过RowSqlSource是在初始化时解析,而DynamicSqlSouce是在调用getBoundSql方法时解析 sql和入参名称集合  
SqlNode 提供apply方法拼装sql信息   MixedSqlNode

1、混合的SqlNode,非叶子节点

2、apply方法:循环SqlNode调用apply方法

SqlNode集合  
TextSqlNode

1、文本SqlNode,可能包含#{}

2、apply方法:将#{}替换成?

sql  
StaticTextSqlNode

1、静态文本SqlNode

2、apply方法:拼装sql

sql  
IfSqlNode等

1、if条件的SqlNode

2、使用OGNL表达式判断,满足条件后调用对应的apply方法

test、SqlNode  
BoundSql 存储解析后的sql语句和入参信息 sql语句和ParamterMapping集合        
ParamterMapping 存储入参名称,后续使用反射从入参对象的指定名称中获取值 name        

二、全局配置文件加载

(一)配置文件主信息加载 

    1、所有的配置信息都要包含在Configuration对象中,并最终以SqlSessionFactory对象的形式返回

    那么,首先创建一个SqlSessionFactory接口,里面包含openSession方法,同时创建一个默认的实现类DefaultSqlSessionFactory,由于SqlSessionFactory重要包含Configuration对象,因此在其默认实现类中,需要有一个Configuration对象的属性,并需要有构造函数来设置。

SqlSessionFactory接口:

package com.lcl.galaxy.mybatis.frame.sqlsession;

public interface MySqlSessionFactory {

    MySqlSession openSession();
}

SqlSessionFactory默认实现类DefaultSqlSessionFactory:

package com.lcl.galaxy.mybatis.frame.sqlsession;

import com.lcl.galaxy.mybatis.frame.config.MyConfiguration;

public class MyDefaultSqlSessionFactory implements MySqlSessionFactory {

    private MyConfiguration myConfiguration;

    public MyDefaultSqlSessionFactory(MyConfiguration myConfiguration){
        this.myConfiguration = myConfiguration;
    }

    @Override
    public MySqlSession openSession() {
        return new MyDefualtSqlSession(myConfiguration);
    }
}

    2、加载主配置文件

    这里使用构建者模式,最终构建出SqlSessionFactory,其解析方法为:根据主配置文件路径获取InputStream ---> 创建Document ---> 按照MyBatis的语义去解析Document对象  --->  将所有对象封装成一个Configuration对象

package com.lcl.galaxy.mybatis.frame.sqlsession;

import com.lcl.galaxy.mybatis.frame.config.MyConfiguration;
import com.lcl.galaxy.mybatis.frame.config.MyResources;
import com.lcl.galaxy.mybatis.frame.config.MyXmlConfigParser;
import com.lcl.galaxy.mybatis.frame.util.DocumentUtils;
import org.dom4j.Document;

import java.io.InputStream;
import java.net.URL;

public class MysessionFactorBuilder {
    public static MySqlSessionFactory build(String resource) {
        InputStream inputStream = MyResources.class.getClassLoader().getResourceAsStream(resource);
        return build(inputStream);
    }

    public static MySqlSessionFactory build(InputStream inputStream) {
        MyXmlConfigParser myXmlConfigParser = new MyXmlConfigParser();
        Document document = DocumentUtils.readInputStream(inputStream);
        MyConfiguration myConfiguration = myXmlConfigParser.parse(document.getRootElement());
        return new MyDefaultSqlSessionFactory(myConfiguration);
    }


}

(二)数据库信息加载

    从上面的代码可以看到,最终是调用XmlConfigParser的parse方法对得到的Element进行解析的,那么接下来就是对RootElement的解析,我们看mybatis的主配置文件时可以发现,其主要的配置内容可以分为两类,一个是以environments标签设置的数据库信息和以mappers标签设置的mapper集合,那么在XmlConfigParser的parse方法中,可以分别对两种不同的标签分别做解析

    public MyConfiguration parse(Element rootElement) {
        parseEnvironments(rootElement.element("environments"));
        parseMappers(rootElement.element("mappers"));
        return myConfiguration;
    }

   可以看到,分别封装了两个方法对不同的标签做解析,这里先说对environments标签的解析,可以对照一下下面的主配置文件,主要就是解析数据库驱动、数据库地址、用户名和密码。

<?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>
    <typeAliases>
        <typeAlias type="com.lcl.galaxy.mybatis.simple.common.domain.UserDo" alias="user"/>
    </typeAliases>
    <!--配置环境-->
    <environments default="mysql">
        <!-- 配置mysql的环境-->
        <environment id="mysql">
            <!-- 配置事务 -->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置连接池-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://********"></property>
                <property name="username" value="********"></property>
                <property name="password" value="5H5eLQsp6yO4"></property>
            </dataSource>
        </environment>
    </environments>
    <!-- 配置映射文件的位置 -->
    <mappers>
        <!-- 使用resource加载xml文件 -->
        <mapper resource="mapper/frame/UserMapper.xml"></mapper>
        <!-- 使用package加载package下所有的注解Mapper -->
        -->
    </mappers>
</configuration>

  解析时,需要解析environments中defualt标签的值(默认使用数据库信息),然后在environments标签中可能有多个environment标签,这里要使用标签的id属性值和defualt属性值一致的数据库信息。

   private void parseEnvironments(Element environments) {
        Properties properties = null;
        String aDefault = environments.attributeValue("default");
        List<Element> elements = environments.elements("environment");
        for (Element element: elements) {
            String id = element.attributeValue("id");
            if(id.equals(aDefault)){
                parseEnvironment(element);
                break;
            }
        }
    }

    private void parseEnvironment(Element element) {
        Properties properties = null;
        BasicDataSource dataSource = new BasicDataSource();
        Element dataSourceElement = element.element("dataSource");
        String type = dataSourceElement.attributeValue("type");
        type = type == null || type.equals("") ? "POOLED":type;
        if (type.equals("POOLED")){
            properties = parseProperties(dataSourceElement);
            dataSource.setDriverClassName(properties.getProperty("driver"));
            dataSource.setUrl(properties.getProperty("url"));
            dataSource.setUsername(properties.getProperty("username"));
            dataSource.setPassword(properties.getProperty("password"));
        }
        myConfiguration.setDataSource(dataSource);
    }

    private Properties parseProperties(Element dataSourceElement) {
        Properties properties = new Properties();
        List<Element> elements = dataSourceElement.elements("property");
        for (Element property: elements) {
            properties.put(property.attributeValue("name"),property.attributeValue("value"));
        }
        return properties;
    }

  这里解析出来的是一个数据库的相关对象,同时将DataSource对象设置到Configuration对象中。

package com.lcl.galaxy.mybatis.frame.config;

import lombok.Data;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Data
public class MyConfiguration {
    private DataSource dataSource;
    private Map<String, MyMappedStatement> mappedStatementMap = new HashMap<>();

    public MyMappedStatement getMyMappedStatement(String statementId){
        return mappedStatementMap.get(statementId);
    }

    public void SetMyMappedStatement(String statementId, MyMappedStatement mappedStatement){
        mappedStatementMap.put(statementId, mappedStatement);
    }
}

(三)Mapper集合加载

    可以看到Configuration中还保存了Map<String, MyMappedStatement> mappedStatementMap属性,该属性是解析Mapper.xml文件得到的。

    由于mappers标签中会配置多个mapper标签,因此需要先加载到mappers集合后,再对集合中的每一个mapper标签进行解析。

    private void parseMappers(Element mappers) {
        List<Element> elementList = mappers.elements("mapper");
        for (Element mapperElement: elementList)  {
            String resource = mapperElement.attributeValue("resource");
            InputStream inputStream = MyResources.getResourceAsStream(resource);
            Document mapperDocument = DocumentUtils.readInputStream(inputStream);
            MyXmlMapperParse myXmlMapperParse = new MyXmlMapperParse(myConfiguration);
            myXmlMapperParse.parse(mapperDocument);
        }
    }

  首先是解析mapper.xml文件的namespace,然后在去解析sql,例如select、update、delete、insert等标签,为了简单,这里手写源码就只写一个select标签。在一个mapper文件中可能会有多个select标签,因此在获取到所有的select标签集合后,循环解析。

package com.lcl.galaxy.mybatis.frame.config;

import org.dom4j.Document;
import org.dom4j.Element;

import java.util.List;

public class MyXmlMapperParse {

    private MyConfiguration myConfiguration;

    public MyXmlMapperParse(MyConfiguration myConfiguration){
        this.myConfiguration = myConfiguration;
    }

    public void parse(Document mapperDocument) {
        Element rootElement = mapperDocument.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List<Element> elements = rootElement.elements("select");
        for (Element element: elements) {
            MyXmlStatementParser myXmlStatementParser = new MyXmlStatementParser(myConfiguration);
            myXmlStatementParser.parse(element, namespace);
        }

    }
}

三、mapper.xml文件加载

(一)创建MyMappedStatement 

    在解析select模块时,可以解析到statementId(namespace+selectId)、入参类型、出参类型和执行方式,其中出入参类型是通过反射获取,同时会获取一个SqlSource对象。同时将以上内容封装到MappedStatement中;前面说的Configuration中还保存了Map<String, MyMappedStatement> mappedStatementMap,就是保存的这里封装的MappedStatement对象。

    因此mappedStatementMap中存储的就是一个个sql信息,key是namespace+id,value就是MappedStatement对象。

package com.lcl.galaxy.mybatis.frame.config;

import com.lcl.galaxy.mybatis.frame.sqlsource.MySqlSource;
import org.dom4j.Element;

public class MyXmlStatementParser {

    private static final String CONNECTOR = ".";

    private MyConfiguration myConfiguration;
    public MyXmlStatementParser(MyConfiguration myConfiguration) {
        this.myConfiguration = myConfiguration;
    }

    public void parse(Element element, String namespace) {
        String id = element.attributeValue("id");
        String statementId = namespace + CONNECTOR + id;
        String parameterType = element.attributeValue("parameterType");
        String resultType = element.attributeValue("resultType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
        Class<?> resultTypeClass = resolveClass(resultType);
        String statementType = element.attributeValue("statementType");
        statementType = statementType == null || statementType.equals("")? "PREPARED":statementType;
        MySqlSource mySqlSource = createMySqlSource(element);
        MyMappedStatement mappedStatement = new MyMappedStatement(statementId,parameterTypeClass,resultTypeClass,statementType,mySqlSource);
        myConfiguration.getMappedStatementMap().put(statementId,mappedStatement);
    }

    private MySqlSource createMySqlSource(Element element) {
        MyXmlScriptParser myXmlScriptParser = new MyXmlScriptParser(myConfiguration);
        return myXmlScriptParser.parseScriptNode(element);
    }

    private Class<?> resolveClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
package com.lcl.galaxy.mybatis.frame.config;


import com.lcl.galaxy.mybatis.frame.sqlsource.MySqlSource;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class MyMappedStatement {
    private String statementId;
    private Class<?> paramterType;
    private Class<?> resultType;
    private String statementType;
    private MySqlSource mySqlSource;
}

(二)SqlSource创建

    在上面已经提到,MappedStatement中封装了SqlSource、入参对象、出参对象、执行类型、statementId这些信息,除了SqlSouce,其余的都已经在上面的代码中获取,接下来就是获取SqlSource(跟着上面的代码中createMySqlSource方法继续往下写)。

    首先创建一个SqlSource接口,并提供getBoundSql方法,该方法返回一个BoundSql对象,该对象中存在一个sql语句和对应的入参对象。

package com.lcl.galaxy.mybatis.frame.sqlsource;

public interface MySqlSource {

    MyBoundSql getBoundSql(Object param);

}
package com.lcl.galaxy.mybatis.frame.sqlsource;

import lombok.Data;

import java.util.List;

@Data
public class MyBoundSql {

    private String sql;
    private List<MyParameterMapping> myParameterMappings;

    public MyBoundSql(String sql, List<MyParameterMapping> myParameterMappingList) {
        this.sql = sql;
        this.myParameterMappings = myParameterMappingList;
    }
}

    SqlSouce接口有三个实现类,分别是DynamicSqlSource、RowSqlSource、StaticSqlSource

      (1)RowSqlSource:文本SqlSource,对应的是StaticSqlNode,所以在构建RowSqlNode对象时,就已经将其进行解析完成,并封装成为StaticSqlSource;所以,如果是最顶层的RowSqlSource,会在Mapper文件加载时,就已经解析完成sql语句和对应入参对象;而如果非最顶层的RowSqlSource,则会在执行被调用执行sql时进行解析。因此在getBound方法中,只需要返回已经处理过的sql和入参对象即可,不需要再次解析。

      (2)DynamicSqlSource:动态SqlSource,由于是动态sql,因此getBoundSql方法需要在调用时才可以确定执行语句,因此该实现方法在执行时在写

      (3)StaticSqlSource:上面两种SqlSouce解析后的结果,都封装到该SqlSource中

      可以总体说明一下其三者的区别:

去别点 StaticSqlSource RowSqlSource DynamicSqlSource
语句内容 封装后面两种SqlSource解析后的结果 纯文本sql,可能包含#{},但不包含${}和动态sql的sql信息 动态sql,可能包含${}或其他动态sql的sql信息
加载顺序

在该对象构造时就完成sql解析

解析方式:由于#{}已被替换为 ?,入参集合为#{}内的属性

在sql调用时加载时已组装sql和入参对象

原因:由于存在${}或其他动态内容,因此需要在执行时根据入参内容组装

入参对象赋值 入参对象为#{}内的内容 入参对象需要在执行时具体确定
对应SqlNode TextSqlNode DynamicSqlNode、IfSqlNode、WhereSqlNode、ForeachSlqNode.....
处理方式 类似于JDBC中的PreparedStatement的处理,是预处理 类似于JDBC的Statement,是字符串的拼接

    然后根据Element创建SqlNode,并判断该SqlNode是动态的还是非动态的,如果是动态的,则将创建一个DynamicSqlSource封装到MappedStatement中,如果是非动态的,则创建一个RawSqlSource并封装到MappedStatement中。

    public MySqlSource parseScriptNode(Element element) {
        MyMixedSqlNode rootSqlNode = parseDynamicTags(element);
        MySqlSource mySqlSource = null;
        if(isDynamic){
            mySqlSource = new MyDynamicSqlSource(rootSqlNode);
        }else {
            mySqlSource = new MyRawSqlSource(rootSqlNode);
        }
        return mySqlSource;
    }

    上面已经说过,DynamicSqlSource是需要在被执行时才会对sql进行解析,因此其构造方法单单是对SqlNode的赋值,而RowSqlSource是只需要在mapper文件加载时加载一次即可,因此在其构造方法中,需要对sql进行解析,这里解析是调用其对应的SqlNode的apply方法进行解析(apply方法主要是解析将sql解析为JDBC可执行的sql)。

package com.lcl.galaxy.mybatis.frame.sqlsource;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.sqlnode.MySqlNode;

public class MyRawSqlSource implements MySqlSource {

    private MySqlSource mySqlSource;

    public MyRawSqlSource(MySqlNode mySqlNode) {
        MyDynamicContext context = new MyDynamicContext(null);
        mySqlNode.apply(context);
        MySqlSourceParser mySqlSourceParser = new MySqlSourceParser();
        mySqlSource = mySqlSourceParser.parse(context.getSql());
    }

}

    解析完毕后,对获取到的sql语句进行处理,判断其是否包含#{},如果包含,则将其替换为  ?  ,并将#{}中的属性设置为入参属性,然后将解析后的sql和入参集合封装到StaticSqlSource并返回。

package com.lcl.galaxy.mybatis.frame.sqlsource;

import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser;
import com.lcl.galaxy.mybatis.frame.util.ParameterMappingTokenHandler;
import com.lcl.galaxy.mybatis.frame.util.TokenHandler;

public class MySqlSourceParser {
    public MySqlSource parse(String sqlText) {
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser tokenParser = new GenericTokenParser("#{","}" ,tokenHandler);
        String sql = tokenParser.parse(sqlText);
        return new MyStaticSqlSource(sql, tokenHandler.getParameterMappings());
    }
}

    至于替换工具及OGNL表达式工具,在最后附的有代码

(三)SqlNode创建  

    在创建SqlNode之前,先创建一个动态sql上下文对象DynamicContext,它用来存储sql语句和绑定的值。

    在该类中,定义一个StringBuilder对象,用来拼装sql语句,然后提供一个Map<String, Object>对象用来存储对应的绑定值;同时提供构造方法,入参为绑定值,并将该绑定值放入map集合中。

package com.lcl.galaxy.mybatis.frame.config;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

@Data
public class MyDynamicContext {
    private StringBuilder sb = new StringBuilder();
    private Map<String, Object> bingds = new HashMap<>();


    public MyDynamicContext(Object param){
        bingds.put("_param", param);
    }


    public Map<String, Object> getBingds(){
        return bingds;
    }

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

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

    然后是创建一个SqlNode接口,提供了一个入参为动态sql上下文DynamicContext对象的apply方法,用来做sql解析

package com.lcl.galaxy.mybatis.frame.sqlnode;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;

public interface MySqlNode {
    void apply(MyDynamicContext context);
}

    该接口实现类有MixSqlNode、TextSqlNode、StaticTextSqlNode、IfSqlNode、WhereSqlNode、ForeachSqlNode等,其作用如下:

实现类 描述 属性 对应SqlSource SQL解析时间
MixSqlNode 这是一个混合的SqlNode SqlNode集合    
StaticTextSqlNode

静态的SqlNode

静态文本SqlNode,里面不包含${}(一定带有${},可能会带有#{})

sql RowSqlSource Mapper文件加载
TextSqlNode

文本SqlNode

文本SqlNode,里面包含了带有${}的纯文本(一定带有${},可能会带有#{})

sql DynamicSqlSource 在调用执行时,才会解析
IfSqlNode

if条件的SqlNode

里面包含了OGNL表达式的test语句和SqlNode,具体包含了SqlNode是那一类,需要具体解析

test、SqlNode   在调用执行时,会使用OGNL表达式判断test条件是否成功,如果成功则根据属性值SqlNode具体类型进行加载

    这里说一下SqlSource和SqlNode的关系,首先,一个SqlSource中只有一个SqlNode,该SqlNode即为MixedSqlNode,在MixedSqlNode中又包含一个SqlNode集合,集合中可能存在各种SqlNode对象,因此SqlNode是一个树形结构,在对顶端,只会是TextSqlNode、StaticTextSqlNode、IfSqlNode、WhereSqlNode和ForeachSqlNode这些SqlNode。

MixedSqlNode:由于MixedSqlNode不会是最顶端的SqlNode,因此需要循环其中的SqlNode集合并调用apply方法进行sql解析

package com.lcl.galaxy.mybatis.frame.sqlnode;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;

import java.util.List;

public class MyMixedSqlNode implements MySqlNode {

    private List<MySqlNode> mySqlNodeList;

    public MyMixedSqlNode(List<MySqlNode> mySqlNodeList) {
        this.mySqlNodeList = mySqlNodeList;
    }

    @Override
    public void apply(MyDynamicContext context) {
        for (MySqlNode mySqlNode : mySqlNodeList){
            mySqlNode.apply(context);
        }
    }
}

TextSqlNode:该SqlNode因为是一个纯文本,因此需要判断该文本sql是否为动态的(是否包含${},如果包含,在执行sql时要将对应的值替换到该该值中),同时提供一个String sql变量来接收该文本值;

        这里我暂时没有写apply方法,因为带有${}的sql需要根据具体的入参来进行设置,所以到后面sql执行时在写负责sql解析的apply方法。

package com.lcl.galaxy.mybatis.frame.sqlnode;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser;
import com.lcl.galaxy.mybatis.frame.util.OgnlUtils;
import com.lcl.galaxy.mybatis.frame.util.SimpleTypeRegistry;
import com.lcl.galaxy.mybatis.frame.util.TokenHandler;

public class MyTextSqlNode implements MySqlNode{

    private String sql;

    public MyTextSqlNode(String sql) {
        this.sql = sql;
    }

    public boolean isDynamic() {
        if(this.sql.indexOf("${") > -1){
            return true;
        }
        return false;
    }

StaticTextSqlNode:该SqlNode是静态纯文本对象,里面一定不包含${},但是有可能包含#{},里面也提供了一个sql属性用来赋值,直接追加到DynamicContext对象的sql中。

package com.lcl.galaxy.mybatis.frame.sqlnode;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser;
import com.lcl.galaxy.mybatis.frame.util.ParameterMappingTokenHandler;

public class MyStaticTextSqlNode implements MySqlNode {

    private String sql;
    public MyStaticTextSqlNode(String sql) {
        this.sql = sql;
    }

    @Override
    public void apply(MyDynamicContext context) {

        context.appendSql(" " + sql);
    }
}

  在第二点中创建SqlSource时,首先是创建了一个混合的SqlNode(MixedSqlNode),其创建方法是,先拿到该Element下的所有node节点,然后判断其类型,

    如果是文本类型,则使用sql构建一个TextSqlNode对象,然后调用该对象的isDynamic方法,判断是否存在动态的语句(是否包含${}),如果是动态的则将该TextSqlNode放入MixedSqlNode的SqlNode集合中,并sql语句的动态标志为true;如果不是动态的,则使用sql封装成一个StaticTextSqlNode,并放入MixedSqlNode的SqlNode集合中。

    如果非文本类型,则首先需要根据额node的名称获取对应的Handler,这里需要在进行配置文件解析时就对不同的node名称设置Handler,然后在handler中对后续的内容进行解析

   private MyMixedSqlNode parseDynamicTags(Element element) {
        List<MySqlNode> mySqlNodeList = new ArrayList<>();
        for (int i=0; i< element.nodeCount(); i++){
            Node myNode = element.node(i);
            if(myNode instanceof Text){
                String sql = myNode.getText().trim();
                if(sql == null || "".equals(sql)){
                    continue;
                }
                MyTextSqlNode myTextSqlNode = new MyTextSqlNode(sql);
                if (myTextSqlNode.isDynamic()){
                    mySqlNodeList.add(myTextSqlNode);
                    isDynamic = true;
                }else {
                    mySqlNodeList.add(new MyStaticTextSqlNode(sql));
                }
            }else if(myNode instanceof Element){
                Element node2Element = (Element) myNode;
                String nodeName = node2Element.getName().toLowerCase();
                MyNodeHandler myNodeHandler = nodeHandlerMap.get(nodeName);
                myNodeHandler.handleNode(node2Element, mySqlNodeList);
                parseScriptNode(node2Element);
                isDynamic = true;
            }
        }
        return new MyMixedSqlNode(mySqlNodeList);
    }

    在这里,为了演示,只写了一个IfSqlNodeHandler,在该handler中,首先解析其test属性中的内容进行封装,后续执行时,需要使用OGNL表达式判断是否满足;其次需要解析 if 标签内的sql语句,这里同样可能存在多种SqlNode,因此也需要一个MixedSqlNode进行接收,所以,这里就重新递归调用上面的parseDynamicTags方法,直到解析出所有的根节点为可执行的TextSqlNode或StaticSqlNode为止。

    public MyXmlScriptParser(MyConfiguration myConfiguration){
        this.myConfiguration = myConfiguration;
        initNodeHandlerMap();
    }

    private void initNodeHandlerMap() {
        nodeHandlerMap.put("if", new MyIfNodeHandler());
        nodeHandlerMap.put("where", new MyWhereNodeHandler());
        nodeHandlerMap.put("foreach", new MyForeachNodeHandler());
    }
package com.lcl.galaxy.mybatis.frame.sqlnode.handler;

import com.lcl.galaxy.mybatis.frame.sqlnode.MySqlNode;
import org.dom4j.Element;

import java.util.List;

public interface MyNodeHandler {
    void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList);
}
    private class MyIfNodeHandler implements MyNodeHandler {
        @Override
        public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {
            MyMixedSqlNode myMixedSqlNode = parseDynamicTags(node2Element);
            String test = node2Element.attributeValue("test");
            MyIfSqlNode myIfSqlNode = new MyIfSqlNode(test, myMixedSqlNode);
            mySqlNodeList.add(myIfSqlNode);
        }
    }

    private class MyWhereNodeHandler implements MyNodeHandler {
        @Override
        public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {

        }
    }

    private class MyForeachNodeHandler implements MyNodeHandler {
        @Override
        public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {

        }
    }

     当解析到MixedSqlNode后,将test和MixedSqlNode封装到IfSqlNode中。

     这里IfSqlNode同样还没有写负责sql解析的apply方法,因为该种sql解析需要在sql调用时根据入参判断是否需要拼装。

package com.lcl.galaxy.mybatis.frame.sqlnode;

import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.OgnlUtils;

public class MyIfSqlNode implements MySqlNode{


    private String test;
    private MySqlNode mySqlNode;

    public MyIfSqlNode(String test, MyMixedSqlNode myMixedSqlNode) {
        this.test = test;
        this.mySqlNode = myMixedSqlNode;
    }
}

四、提供SqlSession接口

(一)在调用时动态解析sql

  创建SqlSession接口,提供一个查询方法,该放入入参为statementId和入参对象param,然后写一个SqlSession的默认实现类DefualtSqlSession,提供对该方法的实现,在实现方法中,首先根据入参的statementId从Configuration对象种获取到MappedStatement,然后获取MappedStatement中的内容进行处理。

package com.lcl.galaxy.mybatis.frame.sqlsession;

import java.util.List;

public interface MySqlSession {
    <T> T selectOne(String statementId, Object param);
    <T> List<T>  selctList(String statementId, Object param);
}
package com.lcl.galaxy.mybatis.frame.sqlsession;

import com.lcl.galaxy.mybatis.frame.config.MyConfiguration;
import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement;
import com.lcl.galaxy.mybatis.frame.executor.MyCachingExecutor;
import com.lcl.galaxy.mybatis.frame.executor.MyExecutor;
import com.lcl.galaxy.mybatis.frame.executor.MySimpleExecutor;

import java.util.List;

public class MyDefualtSqlSession implements MySqlSession {
    private MyConfiguration myConfiguration;
    public MyDefualtSqlSession(MyConfiguration myConfiguration) {
        this.myConfiguration = myConfiguration;
    }

    @Override
    public <T> T selectOne(String statementId, Object param) {
        List<Object> objects = this.selctList(statementId, param);
        if(objects == null || objects.size() == 0){
            return null;
        }else if(objects.size() != 1){
            throw new RuntimeException("查询出多条数据");
        }
        return (T) objects.get(0);
    }

    @Override
    public <T> List<T> selctList(String statementId, Object param) {
        MyMappedStatement myMappedStatement = myConfiguration.getMyMappedStatement(statementId);
        MyExecutor myExecutor = new MyCachingExecutor(new MySimpleExecutor());
        return myExecutor.query(myMappedStatement, myConfiguration, param);
    }
}

  可以发现,上面的代码中有新创建的Executor接口,该接口中提供sql执行的方法query,其中入参为statementId,param和主配置信息Configuration

  其实按照MyBatis的设计,是有一级缓存和二级缓存的,因此,需要提供一个CachingExecutor实现类来对二级缓存做处理,由于现在Mybatis的二级缓存基本上已不再使用,这里为了演示核心处理逻辑,在该实现类中不再处理,直接调用新建的Executor具体处理类BaseExecutor来处理

package com.lcl.galaxy.mybatis.frame.executor;

import com.lcl.galaxy.mybatis.frame.config.MyConfiguration;
import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement;

import java.util.List;

public class MyCachingExecutor implements MyExecutor {


    private MyExecutor myExecutor;

    public MyCachingExecutor(MyExecutor myExecutor) {
        this.myExecutor = myExecutor;
    }

    @Override
    public <T> List<T> query(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) {
        return myExecutor.query(myMappedStatement, myConfiguration, param);
    }
}
package com.lcl.galaxy.mybatis.frame.executor;

import com.lcl.galaxy.mybatis.frame.config.MyConfiguration;
import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class MyBaseExecutor implements MyExecutor {


    private Map<String, Object> oneLevelMap = new HashMap();

    @Override
    public <T> List<T> query(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) {

        String sql = myMappedStatement.getMySqlSource().getBoundSql(param).getSql();
        Object object = oneLevelMap.get(sql);
        if(object != null){
            return (List<T>) object;
        }
        object = queryFromDataBase(myMappedStatement, myConfiguration, param);
        return (List<T>) object;
    }

    protected abstract List<Object> queryFromDataBase(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param);
}

  由于MaBatis中提供一级缓存,且一级缓存必须使用,因此在BaseExecutor中,提供了一个一级缓存oneLevelMap,首先调用MappedStatement中获取SqlSource,然后调用SqlSource中的getBoundSql方法来获取到要执行的sql,然后使用sql语句从一级缓存中获取对应的结果,如果没有结果,则调用queryFromDataBase方法进行查询。这里比较重要的一点就是如何从SqlSource的getBound方法中获取对应的sql。

  这里就要重新回来写SqlSource接口的三个实现类的getBoundSql方法,StaticSqlSource、RowSqlSource、DynamicSqlSource,由于RowSqlSource在mapper文件加载时就会解析sql并将其封装成StaticSqlSource,因此在执行时,只有StaticSqlSource和DynamicSqlSource会被调用,其中StaticSqlSource已经封装好了BoundSql,因此直接就可以获取,而DynamicSqlSource是需要根据入参对象来进行解析的,因此需要在getBoundSql方法中调用apply方法进行追加sql,然后对其sql进行解析,对有#{}的替换成 ? ,且将SQL语句和替换后的入参对象封装成StaticSqlSource并返回。

(二)执行sql

  在上一步拿到BoundSql后,即可真正的进行数据查询,这里可以写一个MyBaseExecutor的子类,并重写queryFromDataBase方法,在该方法中,根据封装到MappedStatement中的StaticSqlSource和传入的入参对象param封装成一个BoundSql对象,然后就是创建数据库连接,预执行,赋值,执行,获取结果和结果转换。

    @Override
    protected List<Object> queryFromDataBase(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) {
        List<Object> resultList = new ArrayList<>();
        try {
            Connection connection = getConnection(myConfiguration);
            MyBoundSql myBoundSql = getBoundSql(myMappedStatement.getMySqlSource(), param);
            if ("PREPARED".equals(myMappedStatement.getStatementType().toUpperCase())){
                PreparedStatement preparedStatement = createStatement(connection, myBoundSql);
                handlerParamter(preparedStatement, myBoundSql, param);
                log.info("sql语句:{}", myBoundSql.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                handleResult(resultList, myMappedStatement, resultSet);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return resultList;
    }

  1、获取数据库连接

    private Connection getConnection(MyConfiguration myConfiguration) throws SQLException {
        DataSource dataSource = myConfiguration.getDataSource();
        Connection connection = dataSource.getConnection();
        return connection;
    }

  2、获取BoundSql

    private MyBoundSql getBoundSql(MySqlSource mySqlSource, Object param) {
        MyBoundSql boundSql = mySqlSource.getBoundSql(param);
        return boundSql;
    }

  3、预执行

    private PreparedStatement createStatement(Connection connection, MyBoundSql myBoundSql) throws Exception {
        PreparedStatement preparedStatement = connection.prepareStatement(myBoundSql.getSql());
        return preparedStatement;
    }

  4、设置参数

  这里写的比较简答,应该是判断类型为简单类型时,则直接设置第一个参数即可(这里只写了int和String两个类型);如果非简单类型,则需要获取BoundSql中的入参对象集合,循环该集合获取每一个入参的名称,通过反射获取到入参param对应的类对象,然后根据名称从该对象中获取字段Field,然后通过反射获取到param对象中相应属性的值,并替换sql中的占位符。

    private void handlerParamter(PreparedStatement preparedStatement, MyBoundSql myBoundSql, Object param) throws Exception {
        if(param instanceof Integer){
            preparedStatement.setInt(1, (Integer) param);
        }else if(param instanceof String){
            preparedStatement.setString(1, String.valueOf(param));
        }else {
            List<MyParameterMapping> myParameterMappings = myBoundSql.getMyParameterMappings();
            for (int i=0; i< myParameterMappings.size(); i++){
                MyParameterMapping myParameterMapping = myParameterMappings.get(i);
                String name = myParameterMapping.getName();
                Class<?> clazz = param.getClass();
                Field field = clazz.getDeclaredField(name);
                field.setAccessible(true);
                Object object = field.get(param);
                preparedStatement.setObject(i+1, object);
            }
        }
    }

  5、执行sql

ResultSet resultSet = preparedStatement.executeQuery();

  6、结果映射

  首先需要通过反射获取到出参对象的类,然后循环查询结果,在每一个循环中,通过出参对象类的newInstance()方法创建一个出参类型的对象,然后获取查询结果列数并循环,获取每一列的名字,通过反射获取该名字对应的字段Field,最终再通过反射设置该字段的值。

    private void handleResult(List<Object> resultList, MyMappedStatement myMappedStatement, ResultSet rs) throws Exception {
        Class<?> resultType = myMappedStatement.getResultType();
        while (rs.next()){
            Object object = resultType.newInstance();
            ResultSetMetaData rsMetaData = rs.getMetaData();
            int columnCount = rsMetaData.getColumnCount();
            for (int i = 0; i< columnCount; i++){
                String columName = rsMetaData.getColumnName(i+1);
                Field declaredField = resultType.getDeclaredField(columName);
                declaredField.setAccessible(true);
                declaredField.set(object, rs.getObject(i + 1));
            }
            resultList.add(object);
        }
    }

 五、工具类

(一)Document工具类

package com.lcl.galaxy.mybatis.frame.util;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;

import java.io.InputStream;

public class DocumentUtils {
    public static Document readInputStream(InputStream inputStream) {
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(inputStream);
            return document;
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }
}

(二)简单类工具

package com.lcl.galaxy.mybatis.frame.util;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

public class SimpleTypeRegistry {

  private static final Set<Class<?>> SIMPLE_TYPE_SET = new HashSet<>();

  static {
    SIMPLE_TYPE_SET.add(String.class);
    SIMPLE_TYPE_SET.add(Byte.class);
    SIMPLE_TYPE_SET.add(Short.class);
    SIMPLE_TYPE_SET.add(Character.class);
    SIMPLE_TYPE_SET.add(Integer.class);
    SIMPLE_TYPE_SET.add(Long.class);
    SIMPLE_TYPE_SET.add(Float.class);
    SIMPLE_TYPE_SET.add(Double.class);
    SIMPLE_TYPE_SET.add(Boolean.class);
    SIMPLE_TYPE_SET.add(Date.class);
    SIMPLE_TYPE_SET.add(Class.class);
    SIMPLE_TYPE_SET.add(BigInteger.class);
    SIMPLE_TYPE_SET.add(BigDecimal.class);
  }

  public static boolean isSimpleType(Class<?> clazz) {
    return SIMPLE_TYPE_SET.contains(clazz);
  }

}

(三)字符转处理类

package com.lcl.galaxy.mybatis.frame.util;

public interface TokenHandler {
  String handleToken(String content);
}
package com.lcl.galaxy.mybatis.frame.util;


import com.lcl.galaxy.mybatis.frame.sqlsource.MyParameterMapping;
import org.apache.ibatis.mapping.ParameterMapping;

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

public class ParameterMappingTokenHandler implements TokenHandler {
    private List<MyParameterMapping> parameterMappings = new ArrayList<>();

    // context是参数名称
    @Override
    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private MyParameterMapping buildParameterMapping(String content) {
        MyParameterMapping parameterMapping = new MyParameterMapping(content);
        return parameterMapping;
    }

    public List<MyParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public void setParameterMappings(List<MyParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }

}
package com.lcl.galaxy.mybatis.frame.util;


public class GenericTokenParser {

  private final String openToken;
  private final String closeToken;
  private final TokenHandler handler;

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   */
  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

 (四)OGNL表达式处理类

package com.lcl.galaxy.mybatis.frame.util;

import ognl.Ognl;
import ognl.OgnlContext;

import java.math.BigDecimal;

public class OgnlUtils {
    /**
     * 根据Ongl表达式,获取指定对象的参数值
     * @param expression
     * @param paramObject
     * @return
     */
    public static Object getValue(String expression, Object paramObject) {
        try {
            OgnlContext context = new OgnlContext();
            context.setRoot(paramObject);

            //mybatis中的动态标签使用的是ognl表达式
            //mybatis中的${}使用的是ognl表达式
            Object ognlExpression = Ognl.parseExpression(expression);// 构建Ognl表达式
            Object value = Ognl.getValue(ognlExpression, context, context.getRoot());// 解析表达式

            return value;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 通过Ognl表达式,去计算boolean类型的结果
     * @param expression
     * @param parameterObject
     * @return
     */
    public static boolean evaluateBoolean(String expression, Object parameterObject) {
        Object value = OgnlUtils.getValue(expression, parameterObject);
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        if (value instanceof Number) {
            return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
        }
        return value != null;
    }
}
原文地址:https://www.cnblogs.com/liconglong/p/14090955.html