zoukankan      html  css  js  c++  java
  • 手写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;
        }
    }
  • 相关阅读:
    TortoiseSVN和VisualSVN-下载地址
    asp.net mvc输出自定义404等错误页面,非302跳转
    IIS7如何显示详细错误信息
    关于IIS7.5下的web.config 404 配置的一些问题
    MVC 错误处理1
    后台获取视图对应的字符串
    HTML5 ArrayBuffer:类型化数组 (二)
    Web 前沿——HTML5 Form Data 对象的使用(转)
    HTML5 File 对象
    HTML5 本地裁剪图片并上传至服务器(转)
  • 原文地址:https://www.cnblogs.com/liconglong/p/14090955.html
Copyright © 2011-2022 走看看