【零基础入门MyBatis系列】第五篇——手写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 的实例去读取文件的数据流
    SAXReader reader = new SAXReader();
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml);
    Document document = reader(is);
    
    • 最后得到的是整个xml文件的文档实例
  • 我们需要定位到具体的表现,所以我们实现要定位到环境标签的位置:

    • 我们使用 xpath 记录标签的位置
    • 通过 Document t的 selectSingleNode 去查找标签
    • 通过标签的 attributeValue(属性) 方法,可以获取标签的属性值
    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 获取标签
    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);
    
    • 上面我们使用 element(标签名) 获取当前结点的一个指定子节点,我们可以使用 elements() 获取当前结点的所有子节点
    //获取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);
    });
    
  • 运行结果如下:【之所以输出多次CarMapper是我测试,在mapper标签里添加了多个重复资源】
    在这里插入图片描述
    🌔 3、解析映射文件xml文件CarMapper.xml

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.CarMapperTest">
    <insert id="insertCar">
        insert into t_cat values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType})
    </insert>

    <select id="selectCarByCarNum" resultType="com.powernode.mybatis.pojo.Car">
        select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where car_num = #{carNum}
    </select>
</mapper>

(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<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);
        });
    }

二、手写Godbatis框架

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

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

<packaging>jar</packaging>
    <dependencies>
        <!--dom4j依赖-->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!--jaxen依赖-->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.2.0</version>
        </dependency>
        <!--junit依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

(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> T unwrap(Class<T> 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<String, MapperdStatement> mapperdStatements;
    public SqlSessionFactory(){}

    public SqlSessionFactory(Transaction transaction, Map<String, MapperdStatement> mapperdStatements) {
        this.transaction = transaction;
        this.mapperdStatements = mapperdStatements;
    }

    public Transaction getTransaction() {
        return transaction;
    }

    public void setTransaction(Transaction transaction) {
        this.transaction = transaction;
    }

    public Map<String, MapperdStatement> getMapperdStatements() {
        return mapperdStatements;
    }

    public void setMapperdStatements(Map<String, MapperdStatement> 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<Node> nodes = document.selectNodes("//mapper"); // "//"代表找到所有的mapper标签,而不是定位到mappers的位置
            List<String> sqlMapperXMLPathList = new ArrayList<>();
            //遍历nodes,获取其resource属性
            nodes.forEach(node -> {
                Element mapper = (Element) node;
                String resource = mapper.attributeValue("resource");
                sqlMapperXMLPathList.add(resource);
            });

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

    private Map<String, MapperdStatement> getMappedStatements(List<String> sqlMapperXMLPathList) {
        //结果封装 key 为SQL的id,value 为resultType和SQL语句
        Map<String, MapperdStatement> 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<Element> 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<String, String> map = new HashMap<>();
        //获取DataSource节点的所有子节点
        List<Element> 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
    在这里插入图片描述
  • 打包后会存储到我们的本地仓库中
    在这里插入图片描述
    2、框架测试

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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.powernode</groupId>
    <artifactId>godbatis-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.god.ibatis</groupId>
            <artifactId>godbatis</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

没有导入mysql驱动等,因为在原框架中有依赖关系自动就下载了,所以我们此处只额外添加了 Junit 的依赖。
在这里插入图片描述

  • 那么我们在框架中也导入了 junit 依赖,为什么此处还需要重新导入呢?
    在这里插入图片描述

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

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

  • godbatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">
                <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="UserMapper.xml"/>
    </mappers>
</configuration>
  • UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
    <insert id="insertUser">
        insert into t_user values (#{id}, #{name}, #{age})
    </insert>
    <select id="selectById" resultType="cn.User">
        select id, name, age from t_user where id = #{id}
    </select>
</mapper>

(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进行单元测试】
  • 测试插入方法,原数据库中只有一条记录

在这里插入图片描述
运行结果,孙悟空的信息成功添加进去
在这里插入图片描述

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

在这里插入图片描述
点击下载godbatis的jar包(阿里云盘): godbatis-1.0-SNAPSHOT.jar

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bow.贾斯汀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值