手写一个简单的MyBatis框架

本文转载于:https://www.qfcwx.top/2019/04/23/shou-xie-yi-ge-jian-dan-de-mybatis/
作者:清风

本文代码参考了:"享学课堂——手写MyBatis"的课程。如有侵权请立即与本人联系,本人将及时删除相关的代码和文献。

MyBatis核心流程的三大阶段

一、初始化阶段:读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
二、代理阶段:封装iBatis的编程模型,使用mapper接口开发的初始化工作。
三、数据读写阶段:通过SqlSession完成SQL解析,参数的映射,SQL的执行,结果的反射解析过程。

SqlSession

  1. 意味着创建会话,代表一次与数据库的连接。
  2. 是MyBatis对外提供数据访问的主要API(iBatis的编程方式)
  3. 实际上SqlSession的功能是基于Executor来实现的。

实际操作

首先创建连接数据库的配置文件db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC&useSSL=false&nullNamePatternMatchesAll=true
username=root
password=root

pom中导入相关的约束,最重要的就是dom4j,用来解析mapper.xml文件。还有MySQL的驱动。


<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.0modelVersion>

    <groupId>com.qfcwxgroupId>
    <artifactId>simple-mybatisartifactId>
    <version>1.0-SNAPSHOTversion>
    <properties>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatisartifactId>
            <version>3.4.6version>
        dependency>

        
        <dependency>
            <groupId>dom4jgroupId>
            <artifactId>dom4jartifactId>
            <version>1.6.1version>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.15version>
        dependency>
    dependencies>
project>

新建一个实体类。

package com.qfcwx.pojo;

import java.io.Serializable;

/**
 * @ClassName: User
 * @Description: TODO
 * @Date: 2019/4/18 17:36
 * @Version 1.0
 **/
public class User implements Serializable {

    private Long id;
    private String username;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

有三个属性,分别与数据库对应,然后创建mapper接口和mapper.xml文件。

public interface UserMapper {

    /**
     * //TODO 根据id查询user对象
     *
     * @param id 用户id
     * @return com.qfcwx.pojo.User
     */
    User selectUserById(Long id);

    /**
     * //TODO 查询所有用户
     *
     * @return java.util.List
     */
    List<User> selectList();
}

然后在resource文件夹新建一个mapper文件夹,建立UserMapper.xml



<mapper namespace="com.qfcwx.mapper.UserMapper">

    <select id="selectUserById" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM 
    	user
    WHERE 
    	id = ?
  select>
    
    <select id="selectList" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM 
    	user
  select>

mapper>

准备工作完成,接下来就是重头戏了。
需要创建一个与mapper.xml中标签和属性对应的实体类。其中包含(namespace、id、resultType、sql…)等。

package com.qfcwx.config;

/**
 * @ClassName: MappedStatement
 * @Description: TODO 映射mapper.xml的实体类namespace,id,resultType,sql...等,对应了一条sql语句
 * @Date: 2019/4/18 11:14
 * @Version 1.0
 **/
public class MappedStatement {

    private String namespace;
    private String sourceId;
    private String resultType;
    private String sql;

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public String getSourceId() {
        return sourceId;
    }

    public void setSourceId(String sourceId) {
        this.sourceId = sourceId;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }
}

一个MappedStatement对象对应一条sql语句。接下来。新建一个配置类。读取所有配置文件(db.properties,mapper.xml)

package com.qfcwx.config;

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

/**
 * @ClassName: Configuration
 * @Description: TODO 对应mybatis-config.xml。读取所有配置文件,包括db.properties,mapper.xml
 * @Date: 2019/4/18 11:20
 * @Version 1.0
 **/
public class Configuration {

    private String driver;
    private String url;
    private String userName;
    private String passWord;

    private Map<String, MappedStatement> statementMap = new HashMap<String, MappedStatement>();

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public Map<String, MappedStatement> getStatementMap() {
        return statementMap;
    }

    public void setStatementMap(Map<String, MappedStatement> statementMap) {
        this.statementMap = statementMap;
    }
}

接下来就进入第一个阶段,读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
SqlSession是MyBatis的关键对象,通过java操作MyBatis时,可看到,它是由SqlSessionFactory这个工厂来创建的。所以需要先完成SqlSessionFactory的相关代码。

public class SqlSessionFactory {

    private final Configuration configuration = new Configuration();

    /**
     * 记录mapper.xml存放的位置
     **/
    private static final String MAPPER_CONFIG_LOCATION = "mapper";
    /**
     * 记录数据库连接信息存放的文件
     **/
    private static final String DB_CONFIG_FILE = "db.properties";

    public SqlSessionFactory() {
        loadDBInfo();
        loadMappersInfo();
    }

    /**
     * //TODO 读取数据库配置文件信息
     *
     * @param
     * @return void
     **/
    private void loadDBInfo() {
        //加载数据库信息配置文件
        InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
        Properties properties = new Properties();
        try {
            properties.load(stream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //将数据库配置信息写入configuration对象中
        configuration.setDriver(properties.get("driver").toString());
        configuration.setUrl(properties.get("url").toString());
        configuration.setUserName(properties.get("username").toString());
        configuration.setPassWord(properties.get("password").toString());
    }

    /**
     * //TODO 获取指定文件下的所有mapper.xml文件
     *
     * @param
     * @return void
     **/
    private void loadMappersInfo() {
        URL resource = null;
        resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
        //获取指定文件夹信息
        File file = new File(resource.getFile());
        if (file.isDirectory()) {
            File[] mappers = file.listFiles();
            //遍历文件夹下所有的mapper.xml文件,解析后,注册到configuration中
            for (File mapper : mappers) {
                loadMapper(mapper);
            }
        }
    }

    /**
     * //TODO 对mapper.xml文件解析
     *
     * @param mapper
     * @return void
     **/
    private void loadMapper(File mapper) {
        //创建SAXReader对象
        SAXReader saxReader = new SAXReader();
        //通过read方法读取一个文件,转换成Document对象
        Document document = null;
        try {
            document = saxReader.read(mapper);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //获取根节点元素对象
        Element rootElement = document.getRootElement();
        //获取命名空间
        String namespace = rootElement.attribute("namespace").getData().toString();
        //获取子节点