05、手写MyBatis框架

一、dom4j解析XML文件

  • 为了方便我们实践各种操作,我们创建了一个新的模块:parse-xml-by-dom4j
  • 为新模块设置打包方式和配置各种依赖
  • 将之前模块中的 mybatis-congfig.xmlCarMapper.xml 文件直接拷贝到了 resource 文件夹下

🌔 1、解析核心配置文件 与 映射文件 【完整代码】

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.junit.Test;

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

/**
 * @author Bonbons
 * @version 1.0
 */
public class ParseXMLByDom4jTest {

    //解析SQL映射文件
    @Test
    public void testParseSqlMapperXML() throws Exception{

        //利用Reader获取数据流,也就是找到我们要解析的XML文件
        SAXReader reader = new SAXReader();
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("CarMapper.xml");
        Document document = reader.read(is);
        //获取namespace
        String xpath = "/mapper";
        Element mapper = (Element) document.selectSingleNode(xpath);
        String namespace = mapper.attributeValue("namespace");
        System.out.println(namespace);
        //获取当前节点下的所有子节点
        List<Element> mapperElements = mapper.elements();
        //遍历
        mapperElements.forEach(element -> {

            String id = element.attributeValue("id");
            System.out.println(id);
            //如果获取标签没有的属性,并不会报错而是返回mull
            String resultType = element.attributeValue("resultType");
            System.out.println(resultType);
            //获取标签中的sql语句[Trim的作用是去除前后空白,也有个getText方法]
            String sql = element.getTextTrim();
            System.out.println(sql);
            //利用replaceAll方法结合正则表达式,将占位符替换为 ?
            String newSql = sql.replaceAll("#\\{[0-9A-Za-z_$]*}", "?");
            System.out.println(newSql);
        });
    }
    @Test
    public void testParseMyBatisConfigXML() throws DocumentException {

        //通过SAXReader对象解析xml
        SAXReader reader = new SAXReader();
        //获取输入流
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
        //读取xml文件,返回整个xml文件的文档对象
        Document document =  reader.read(is);
        //我们利用xpath定位xml中的元素
        String xpath = "/configuration/environments";
        //文档类提供一个selectSingleNode方法去查找元素
        Element environments = (Element) document.selectSingleNode(xpath);
        //获取属性的值
        String defaultId = environments.attributeValue("default");
        System.out.println("环境id:" + defaultId);
        //获取具体的环境,我们仍然需要xpath去定位[如果此处不添加单引号就获取不到具体的属性值]
        //如果想获取环境的属性,需要采用 environment[@属性名]的方式
        xpath += "/environment[@id='"+defaultId+"']";
        Element environment = (Element) document.selectSingleNode(xpath);
//        System.out.println(environment);
        //利用element方法获取指定元素下的子节点
        Element transcationManager = environment.element("transactionManager");
        //查看事务管理器的类型
        String transactionType = transcationManager.attributeValue("type");
        System.out.println("事务管理器:" + transactionType);
        //获取dataSource结点
        Element dataSource = environment.element("dataSource");
        String dataSourceType = dataSource.attributeValue("type");
        System.out.println("数据源:" + dataSourceType);
        //获取dataSource下的所有子节点
        List<Element> propertyElements = dataSource.elements();
        //遍历
        propertyElements.forEach(e -> {

            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            System.out.println(name + ":" + value);
        });

        //解析mapper
        //利用xpath获取路径
        xpath = "//mapper";
        List<Node> mappers = document.selectNodes(xpath);
        //遍历mappers
        mappers.forEach(e -> {

            //将原型强转为Element类型
            Element mapperElement = (Element) e;
            //获取resource属性
            String resource = mapperElement.attributeValue("resource");
            System.out.println(resource);
        });

    }
}

🌔 2、解析核心配置文件的详细步骤

(1)展示一下我的 mybatis-config.xml 文件

<?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>
    <environments default="powernodeDB">
        <environment id="powernodeDB">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
                <property name="username" value="root"/>
                <property name="password" value="111111"/>
            </dataSource>
        </environment>

        <!--添加一个新的环境-->
        <environment id="mybatisDB">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.myql.cj.hdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/shop"/>
                <property name="username" value="root"/>
                <property name="password" value="111111"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--sql映射文件创建好之后,需要将该文件路径配置到这里-->
        <mapper resource="CarMapper.xml"/>
    </mappers>
</configuration>

(2)我们在 test/java 目录下创建一个测试类:ParseXMLByDom4jTest

  • 如果我们想解析一个文件,首先就要定位到这个文件的位置:

  • 我们通过 SAXReader 的实例去读取文件的数据流

```java 
SAXReader reader = new SAXReader();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml);
Document document = reader(is);
  • 最后得到的是整个xml文件的文档实例

  • 我们需要定位到具体的表现,所以我们实现要定位到环境标签的位置:

  • 我们使用 xpath 记录标签的位置

  • 通过 Document t的 selectSingleNode 去查找标签

  • 通过标签的 attributeValue(属性) 方法,可以获取标签的属性值

```java 
String xpath = "/configuration/environments";
Element environments = (Element) document.selectSingleNode(xpath);
String default = environments.attributeValue("default");
System.out.println("环境id:" + defaultId);

*  之后与此类似,就是一层一层的解析**xml**文件中的所有标签及其属性
*  接下来我们去查看具体的环境
  • 还是需要利用 xpath 去定位标签【因为可能存在多个环境,所以我们可以通过id去指定查看哪个环境】
  • 还是需要 selectSingleNode 根据 xpath 获取标签
```java 
xpath += "/environment[@id='"+defaultId+"']";
Element environment = (Element) document.selectSingleNode(xpath);
System.out.println(environment);

*  因为之后与上述相同,且配置信息大部分都属于当前环境的子节点,所以给出说明和代码【注释在代码里面】
  • 我们使用 element(“标签名”) 获取子节点
  • 获得事务管理器的标签,并查看管理器的类型
```java 
//获取子节点  -- 事务管理器
Element transcationManager = environment.element("transactionManager");
//查看事务管理器的类型
String transactionType = transcationManager.attributeValue("type");
System.out.println("事务管理器:" + transactionType);
  • 获取 dataSource 标签
```java 
Element dataSource = environment.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println("数据源:" + dataSourceType);
  • 上面我们使用 element(标签名) 获取当前结点的一个指定子节点,我们可以使用 elements() 获取当前结点的所有子节点
```java 
//获取dataSource下的所有子节点
List<Element> propertyElements = dataSource.elements();
//遍历
propertyElements.forEach(e -> {

  String name = e.attributeValue("name");
  String value = e.attributeValue("value");
  System.out.println(name + ":" + value);
});
*  还有一个 **mapper** 标签,但是它不是环境的子节点,所以我们呢要重新定位,其余步骤与上述内容类似

    ```java 
    //利用xpath获取路径
    xpath = "//mapper";
    List mappers = document.selectNodes(xpath);
    //遍历mappers
    mappers.forEach(e -> {

    //将原型强转为Element类型
    Element mapperElement = (Element) e;
       //获取resource属性
      String resource = mapperElement.attributeValue("resource");
      System.out.println(resource);
    });
  • 运行结果如下:【之所以输出多次CarMapper是我测试,在mapper标签里添加了多个重复资源】
    &nbsp;
    🌔 3、解析映射文件xml文件CarMapper.xml

(1)老样子,先看看 CarMapper.xml 文件中都有什么 【一条查询、一条插入语句】




    
        insert into t_cat values (null, #{

     carNum}, #{

     brand}, #{

     guidePrice}, #{

     produceTime}, #{

     carType})
    

    

(2)如果想解析这个xml文件,整体思路梳理:

  • 我们要通过数据流与SAXReader实例定位到这个文件,最后获得document文档
  • 我们要利用 xpathselectSingleNode 获取根标签,此处为 namespace 标签,我们可以通过 attributeValue 方法获取属性值
  • 我们通过 elements() 获取所有子节点,进而查看属性值
  • 我们还可以通过 getTextTrim() 查看具体的SQL语句
  • 我们还可以将SQL语句的占位符替换成JDBC的格式
  • 代码如下:详情见注释
    @Test
    public void testParseSqlMapperXML() throws Exception{

        //利用Reader获取数据流,也就是找到我们要解析的XML文件
        SAXReader reader = new SAXReader();
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("CarMapper.xml");
        Document document = reader.read(is);
        //获取namespace
        String xpath = "/mapper";
        Element mapper = (Element) document.selectSingleNode(xpath);
        String namespace = mapper.attributeValue("namespace");
        System.out.println(namespace);
        //获取当前节点下的所有子节点
        List mapperElements = mapper.elements();
        //遍历
        mapperElements.forEach(element -> {

            String id = element.attributeValue("id");
            System.out.println(id);
            //如果获取标签没有的属性,并不会报错而是返回mull
            String resultType = element.attributeValue("resultType");
            System.out.println(resultType);
            //获取标签中的sql语句[Trim的作用是去除前后空白,也有个getText方法]
            String sql = element.getTextTrim();
            System.out.println(sql);
            //利用replaceAll方法结合正则表达式,将占位符替换为 ?
            String newSql = sql.replaceAll("#\\{[0-9A-Za-z_$]*}", "?");
            System.out.println(newSql);
        });
    }

二、手写Godbatis框架

  • 并不是和mybatis一样的完整框架,只是实现了一些基本功能的删减版框架
  • 接下来我们从整体上分析一下这个框架:

1、 创建一个新的模块–godbatis
(1)确定打包方式和导入相关依赖【修改pom.xml文件】

jar
    
        
        
            org.dom4j
            dom4j
            2.1.3
        
        
        
            jaxen
            jaxen
            1.2.0
        
        
        
            junit
            junit
            4.13.2
            test
        
    

(2)为了方便从类路径加载资源,我们写一个工具类 – Resources.java

package org.god.ibatis.utiils;

import java.io.InputStream;

/**
 * godbatis 工具类
 * 从类路径下加载资源
 * @author Bonbons
 * @version 1.0
 */
public class Resources {

    /**
     * 工具类的构造器都是私有的 >> 不需要创建对象,提供的都是静态方法
     */
    public Resources(){

     }
    /**
     * 从类路径加载资源
     * @param resource 放在类路径当中的资源文件
     * @return 指向资源文件的一个输入流
     */
    public static InputStream getResourceAsStream(String resource){

        return ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
    }
}

2、 创建SqlSessionFactoryBuilder类,为其设计一个build方法,可以返回一个SqlSessionFactory的对象;

package org.god.core;

import java.io.InputStream;

/**
 * SqlSessionFactory对象构建器
 * @author 老杜
 * @version 1.0
 * @since 1.0
 */
public class SqlSessionFactoryBuilder {

    /**
     * 创建构建器对象
     */
    public SqlSessionFactoryBuilder() {

    }
    /**
     * 获取SqlSessionFactory对象
     * 该方法主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
     * @param inputStream 指向核心配置文件的输入流
     * @return SqlSessionFactory对象
     */
    public SqlSessionFactory build(InputStream inputStream){

        // 解析配置文件,创建数据源对象
        // 解析配置文件,创建事务管理器对象
        // 解析配置文件,获取所有的SQL映射对象
        // 将以上信息封装到SqlSessionFactory对象中
        // 返回
        return null;
    }
}

3、 既然我们要通过build方法返回SqlSessionFactory的实例,那么我们要知道SqlSessionFactory包含哪些属性呢?;

  • 使用了哪种事务管理器:此处我们只实现JDBC的事务管理器
  • SQL映射对象集合:为了方便,我们将SQL语句抽象出来,通过一个简单的java类,封装了SQL语句的resultType和语句内容

(1)我们思考一个问题,对于事务管理器和数据源类型都有多种,难道我们他们彼此之间没有联系吗?

实则不然,对于两种事务管理器、三种数据源类型他们各自都要遵守自己的规范,也就是我们要通过实现接口的方式来完成。【面向接口编程】
(2)我们定义一个事务管理器的接口

package org.god.ibatis.core;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * 事务管理器接口,所有的事务管理器都应该实现该接口
 * 实际上接口也只有两个:JDBC、MANAGED事务管理器
 * 就是提供事务管理的方法
 *  @author Bonbons
 * @version 1.0
 */
public interface Transaction {

    /**
     * 提交事务
     */
    void commit();
    /**
     * 回滚事务
     */
    void rollback();
    /**
     * 关闭事务
     */
    void close();

    //开启数据库连接
    void openConnection();

    //返回连接对象
    Connection getConnection();
}

(2)再定义两个类JdbcTransactionManagedTransaction分别代表两种事务管理器的实现类,因为我们以JDBC为例,所以此处只给出如何实现Jdbc的事务管理器。

package org.god.ibatis.core;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * JDBC 事务管理器
 * @author Bonbons
 * @version 1.0
 */
public class JdbcTransaction implements Transaction{

    /**
     * 数据源属性,面向接口编程
     */
    private DataSource dataSource;

    /**
     * 自动提交标志
     * true 表示自动提交
     * false 表示不采用自动提交
     */
    private boolean autoCommit;
    private Connection connection;
    //因为执行SQL语句的时候也需要连接对象,所以为外部提供获取connection的方法
    @Override
    public Connection getConnection(){

        return connection;
    }
    /**
     * 创建事务管理器对象
     * @param dataSource
     * @param autoCommit
     */
    public JdbcTransaction(DataSource dataSource, boolean autoCommit) {

        this.dataSource = dataSource;
        this.autoCommit = autoCommit;
    }

    @Override
    public void commit() {

        try {

            connection.commit();
        } catch (SQLException e) {

            e.printStackTrace();
        }
    }

    @Override
    public void rollback() {

        try {

            connection.rollback();
        } catch (SQLException e) {

            e.printStackTrace();
        }
    }

    @Override
    public void close() {

        try {

            connection.close();
        } catch (SQLException e) {

            e.printStackTrace();
        }
    }
    @Override
    public void openConnection(){

        try {

            if(connection == null){

                connection = dataSource.getConnection();
            }
        } catch (Exception e) {

            e.printStackTrace();
        }
    }
}

(2)因为事务管理器的实例化需要获取数据源,所以我们将数据源的定义在事务管理器中完成,对外部提供会话对象的获取方法

  • 与事务管理器不同,我们不需要去写一个数据源的接口,因为Javajdk为我们提供了
  • 我们自己写的数据源类只需要实现 javax.sql.dataSource 接口
  • 数据源的类型有三种: POOLED、UNPOOLED、JNDI三种类型,我么以UNPOLED为例

我们创建一个 UnPooledDataSource.java 用来实现数据源的获取

package org.god.ibatis.core;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

/**
 * 数据源实现类:UNPOOLED
 * 不使用连接池,每一次都新建Connection对象
 * @author Bonbons
 * @version 1.0
 */
public class UnPooledDataSource implements javax.sql.DataSource{

    // private String driver; 数据驱动创建一次就可以了
    private String url;
    private String username;
    private String password;

    /**
     * 创建一个数据源的对象
     * @param driver
     * @param url
     * @param username
     * @param password
     */
    public UnPooledDataSource(String driver, String url, String username, String password) {

        try {

            Class.forName(driver);
        } catch (ClassNotFoundException e) {

            throw new RuntimeException(e);
        }
        this.url = url;
        this.username = username;
        this.password = password;
    }

    @Override
    public Connection getConnection() throws SQLException {

        //每次获取连接的时候,都创建一个连接对象
        Connection connection = DriverManager.getConnection(url, username, password);
        return connection;

    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {

        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {

        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {

        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {

        return null;
    }

    @Override
    public  T unwrap(Class iface) throws SQLException {

        return null;
    }

    @Override
    public boolean isWrapperFor(Class iface) throws SQLException {

        return false;
    }
}

(3)事务管理器和数据源都有了,接下来我们就要考虑获取具体的SQL语句了

  • 我们定义一个 MapperdStatement.java 用来封装SQl标签
package org.god.ibatis.core;

/**
 * 普通pojo类,作用就是封装一个SQL标签
 * 一个MapperdStatement对象对应一个SQL标签
 * @author Bonbons
 * @version 1.0
 */
public class MapperdStatement {

    /**
     * sql 代表SQL语句
     * resultType 代表返回的结果类型,只有在select语句中不为空
     */
    private String sql;
    private String resultType;

    //有参和无参构造方法
    public MapperdStatement(){

     }

    public MapperdStatement(String sql, String resultType) {

        this.sql = sql;
        this.resultType = resultType;
    }

    public String getSql() {

        return sql;
    }

    public void setSql(String sql) {

        this.sql = sql;
    }

    public String getResultType() {

        return resultType;
    }

    public void setResultType(String resultType) {

        this.resultType = resultType;
    }

    @Override
    public String toString() {

        return "MapperdStatement{" +
                "sql='" + sql + '\'' +
                ", resultType='" + resultType + '\'' +
                '}';
    }
}

4、 此时已经完成了基本结构的定义,接下来需要完善SqlSessionFactory类和SqlSessionFactoryBuilder类;
(1)完善SqlSessionFactory类,包含具体的属性、构造方法、GetSet方法

package org.god.ibatis.core;

import java.util.Map;

/**
 * 一个数据库对应一个SqlSessionFactory对象,通过该对象可以开启会话并返回
 * 一个SqlSession的对象(一对多)
 * @author Bonbons
 * @version 1.0
 */
public class SqlSessionFactory {

    /**
     * 事务管理器
     */
    Transaction transaction;
    /**
     * 存放sql语句的Map集合
     * key是sqlId
     * value是对应SQL标签信息对象
     */
    private Map mapperdStatements;
    public SqlSessionFactory(){

     }

    public SqlSessionFactory(Transaction transaction, Map mapperdStatements) {

        this.transaction = transaction;
        this.mapperdStatements = mapperdStatements;
    }

    public Transaction getTransaction() {

        return transaction;
    }

    public void setTransaction(Transaction transaction) {

        this.transaction = transaction;
    }

    public Map getMapperdStatements() {

        return mapperdStatements;
    }

    public void setMapperdStatements(Map mapperdStatements) {

        this.mapperdStatements = mapperdStatements;
    }
}

(2)完成SqlSessionFactoryBuilder类中的build方法【重点内容】

package org.god.ibatis.core;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.god.ibatis.utiils.Resources;

import javax.sql.DataSource;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Bonbons
 * @version 1.0
 */
public class SqlSessionFactoryBuilder {

    public SqlSessionFactoryBuilder() {

    }

    /**
     * 解析godbatis-config.xml文件,来构造SqlSessionFactory独享
     *
     * @param in 指向godbatis-config.xml文件的一个输入流
     * @return SqlSessionFactory对象
     */
    //利用build方法创建SqlSessionFactory
    public SqlSessionFactory build(InputStream in) {

        SqlSessionFactory factory = null;
        try {

            //解析godbatis-config.xml文件
            //(1)获取文件 >> 转换为document文档
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //(2)获得环境集合节点
            Element environments = (Element) document.selectSingleNode("/configuration/environments");
            //(3)获得它的属性 >> 默认使用的环境id
            String defaultId = environments.attributeValue("default");
            //(4)获取具体的环境
            Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
            //(5)获取子节点 事务管理器和数据源
            Element transactionElt = (Element) environment.element("transactionManager");
            Element dataSourceElt = (Element) environment.element("dataSource");
            //(6)创建JDBC事务管理器的时候需要传入数据源
            DataSource dataSource = getDataSource(dataSourceElt);
            //获取事务管理器
            Transaction transaction = getTransaction(transactionElt, dataSource);
            //创建一个集合,保存所有的Mapper.xml的路径
            List nodes = document.selectNodes("//mapper"); // "//"代表找到所有的mapper标签,而不是定位到mappers的位置
            List sqlMapperXMLPathList = new ArrayList<>();
            //遍历nodes,获取其resource属性
            nodes.forEach(node -> {

                Element mapper = (Element) node;
                String resource = mapper.attributeValue("resource");
                sqlMapperXMLPathList.add(resource);
            });

            //获取mapperedStatement [封装的SQl语句]
            Map mappedStatements = getMappedStatements(sqlMapperXMLPathList);
            //解析完成我们返回SqlSessionFactory
            factory = new SqlSessionFactory(transaction, mappedStatements);
        } catch (Exception e) {

            e.printStackTrace();
        }
        return factory;
    }

    private Map getMappedStatements(List sqlMapperXMLPathList) {

        //结果封装 key 为SQL的id,value 为resultType和SQL语句
        Map mapperdStatements = new HashMap<>();
        //根据我们获得的mapper标签,获得每个Mapper文件的内容
        sqlMapperXMLPathList.forEach(sqlMapperXMLPath ->{

            try{

                //解析每个xml文件
                SAXReader reader = new SAXReader();
                Document document = reader.read(Resources.getResourceAsStream(sqlMapperXMLPath));
                Element mapper = (Element) document.selectSingleNode("mapper");
                String namespace = mapper.attributeValue("namespace");
                //获得所有SQL语句
                List elements = mapper.elements();
                elements.forEach(element -> {

                    String id = element.attributeValue("id");
                    //拼接namespace+id
                    String sqlId = namespace + "." +  id;
                    String resultType = element.attributeValue("resultType");
                    String sql = element.getTextTrim();
                    MapperdStatement mapperdStatement = new MapperdStatement(sql, resultType);
                    mapperdStatements.put(sqlId, mapperdStatement);

                });
            }catch (Exception e){

                e.printStackTrace();
            }
        });
        return mapperdStatements;
    }
    private Transaction getTransaction(Element transactionElt, DataSource dataSource) {

        //事务管理器的类型
        String type = transactionElt.attributeValue("type").trim().toUpperCase();
        //创建事务管理器的对象
        Transaction transaction = null;
        //选择使用哪种事务管理器
        if(Const.JDBC_TRANSACTION.equals(type)){

            transaction = new JdbcTransaction(dataSource, false);
        }else{

            transaction = new ManagedTransaction();
        }

        return transaction;
    }

    /**
     * 获取数据源对象
     *
     * @param dataSourceElt 数据源跟标签
     * @return
     */
    private DataSource getDataSource(Element dataSourceElt) {

        //利用集合存储DataSource子节点的name和value
        Map map = new HashMap<>();
        //获取DataSource节点的所有子节点
        List propertyElts = dataSourceElt.elements();
        //遍历所有子节点,记录元素值
        propertyElts.forEach(e -> {

            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            map.put(name, value);
        });
        //定义数据源的对象
        DataSource dataSource = null;
        //获取数据源类型[使用哪种连接池]
        String type = dataSourceElt.attributeValue("type").trim().toUpperCase();
        //根据不同的类型选择不同的操作[需要传入参数]
        if (Const.UN_POOLED_DATASOURCE.equals(type)) {

            dataSource = new UnPooledDataSource(map.get("driver"), map.get("url"), map.get("username"), map.get("password"));
        }
        if (Const.POOLED_DATASOURCE.equals(type)) {

            dataSource = new PooledDataSource();
        }
        if (Const.JNDI_DATASOURCE.equals(type)) {

            dataSource = new JNDIDataSource();
        }

        return dataSource;
    }
}

(3)为SqlSessionFactory类添加openSession方法,用于开启会话

public SqlSession openSession(){

        //利用事务管理器的openSession方法来开启连接,这样确保会话不为空
        transaction.openConnection();
        //创建SqlSession的对象 [此处进行了修改2]
        SqlSession sqlSession = new SqlSession(this);
        return sqlSession;

}

4、 为了实现具体的功能,我们提供插入数据和根据id查询数据的功能【为了方便,我们将所有字段都定义为字符串类型】;

  • 在SqlSession类中提供 insertselectOne 方法

(1)insert 方法的代码如下:【大部分都标注了注释】

public int insert(String sqlId, Object pojo){

        //操作影响数据库表记录的条数
        int count = 0;
        try {

            //获取连接对象
            Connection connection = factory.getTransaction().getConnection();
            //获取我们封装的SQL语句,此时是使用#{}作为占位符的
            String godbatisSql = factory.getMapperdStatements().get(sqlId).getSql();
            //将sql语句改为JDBC的格式
            String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$]*}", "?");
            PreparedStatement ps = connection.prepareStatement(sql);
            //重点在于如何动态的给占位符传值
            //为了简化操作,该框架应用的数据库表的字段都是varchar类型
            int fromIndex = 0; //记录查询的起始位置
            int index = 1; //对应第几个 ?
            while(true){

                //每个占位符#的位置
                int jingIndex = godbatisSql.indexOf("#", fromIndex);
                //已经没有#号了
                if(jingIndex < 0){

                    break;
                }
                //每个占位符右括号的位置
                int youKuoHaoIndex = godbatisSql.indexOf("}", fromIndex);
                //对应字段名
                String propertyName = godbatisSql.substring(jingIndex + 2, youKuoHaoIndex).trim();
                //更新下次查找起始位置
                fromIndex = youKuoHaoIndex + 1;
                //如何获取属性值?[分为三部分:get + 字段首字母大写 + 字段剩余部分]
                String getMethodName = "get" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
                //
                Method getMethod = pojo.getClass().getDeclaredMethod(getMethodName);
                Object propertyValue = getMethod.invoke(pojo);
                ps.setString(index, propertyValue.toString());
                //更新索引
                index++;

            }
            //执行SQL语句
            count = ps.executeUpdate();
        } catch (Exception e) {

            e.printStackTrace();
        }
        return count;
    }

(2)selectOne 的代码块如下:

public Object selectOne(String sqlId, Object param){

        Object obj = null;
        try {

            //获取连接对象
            Connection connection = factory.getTransaction().getConnection();
            //获取我们封装的SQL集合,key--sqlId、value--resultType和SQL语句
            MapperdStatement mapperdStatement = factory.getMapperdStatements().get(sqlId);
            //获取sql语句和结果集的类型
            String godbatisSql = mapperdStatement.getSql();
            String resultType = mapperdStatement.getResultType();
            //占位符替换
            String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-_$]*}", "?");
            //参数传递
            PreparedStatement ps = connection.prepareStatement(sql);;
            //此时占位符只有一个
            ps.setString(1,param.toString());
            ResultSet rs = ps.executeQuery();

            //从结果集中取数据封装成Java对象
            if(rs.next()){

                // 获取resultType的Class
                Class resultTypeClass = Class.forName(resultType);
                //调用无参构造方法创建对象
                obj = resultTypeClass.newInstance();
                //给User类的属性赋值
                ResultSetMetaData rsmd = rs.getMetaData();
                //列数
                int columnCount = rsmd.getColumnCount();
                //依次赋值
                for (int i = 0; i < columnCount; i++) {

                    //获取第i个列名
                    String propertyName = rsmd.getColumnName(i + 1);
                    //拼接Set方法名
                    String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
                    //获取set方法
                    Method setMethod = resultTypeClass.getDeclaredMethod(setMethodName, String.class);
                    //调用set方法给obj传值
                    setMethod.invoke(obj, rs.getString(propertyName));

                }
            }
        } catch (Exception e) {

            e.printStackTrace();
        }
        return obj;
}

(3)所有的代码都经过了测试,文末会提供框架的 jar


三、项目打包和测试

1、将手写的这个框架打包

  • 因为我用的是idea,所有可以一键打包
  • 右侧点击Maven >> 找到项目 >> Lifecycle >> install
    &nbsp;
  • 打包后会存储到我们的本地仓库中
    &nbsp;
    2、框架测试

(1)创建了一个新的模块 godbatis-test 用于功能测试
(2)修改 pom.xml 文件,确定打包方式和导入我们需要的依赖



    4.0.0

    com.powernode
    godbatis-test
    1.0-SNAPSHOT
    jar
    
        
            org.god.ibatis
            godbatis
            1.0-SNAPSHOT
        
        
            junit
            junit
            4.13.2
            test
        
    
    
        17
        17
        UTF-8
    


没有导入mysql驱动等,因为在原框架中有依赖关系自动就下载了,所以我们此处只额外添加了 Junit 的依赖。
&nbsp;

  • 那么我们在框架中也导入了 junit 依赖,为什么此处还需要重新导入呢?
    &nbsp;

因为 junitscopetest,在打包的时候就不会将其打包进来【test 代表作用范围为测试】

(3)添加我们的核心配置文件映射文件

  • godbatis-config.xml


    
        
            
            
                
                
                
                
            
        

        
        
            
            
                
                
                
                
            
        
    
    
        
        
    

  • UserMapper.xml



    
        insert into t_user values (#{

     id}, #{

     name}, #{

     age})
    
    

(4)编写我们的pojo类,利用普通Java类作为数据库表查询结果的返回值类型

package cn;

/**
 * @author Bonbons
 * @version 1.0
 */
public class User {

    private String id;
    private String name;
    private String age;

    public User(){

     }
    public User(String id, String name, String age) {

        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {

        return id;
    }

    public void setId(String id) {

        this.id = id;
    }

    public String getName() {

        return name;
    }

    public void setName(String name) {

        this.name = name;
    }

    public String getAge() {

        return age;
    }

    public void setAge(String age) {

        this.age = age;
    }

    @Override
    public String toString() {

        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

(5)编写测试类 UserMapperTest

package com.powernode.godbatis.test;

import cn.User;
import org.god.ibatis.core.SqlSession;
import org.god.ibatis.core.SqlSessionFactoryBuilder;
import org.god.ibatis.utiils.Resources;
import org.junit.Test;

/**
 * @author Bonbons
 * @version 1.0
 */
public class UserMapperTest {

    @Test
    public void testInsertUser(){

        //创建会话
        SqlSession sqlSession = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("godbatis-config.xml")).openSession();
        //执行SQL
        User user = new User("111", "孙悟空", "20");
        sqlSession.insert("user.insertUser", user);
        //提交事务、关闭连接
        sqlSession.commit();
        sqlSession.close();

    }
    @Test
    public void testSelectOne(){

        //创建会话
        SqlSession sqlSession = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("godbatis-config.xml")).openSession();
        //执行SQL
        Object user = sqlSession.selectOne("user.selectById", 1);
        System.out.println(user);
        //关闭连接
        sqlSession.close();
    }
}
  • 对于这两个方法我们分开进行测试【使用了junit进行单元测试】
  • 测试插入方法,原数据库中只有一条记录

&nbsp;
运行结果,孙悟空的信息成功添加进去
&nbsp;

  • 测试根据id查询数据,我们就查id为1的用户,查询结果如下

&nbsp;
点击下载godbatis的jar包(阿里云盘): godbatis-1.0-SNAPSHOT.jar