精简版 MyBatis

2 阅读14分钟

整体概述

精简版 MyBatis 手写实现,复刻 MyBatis 核心执行流程,抛弃复杂细节(连接池、动态 SQL、Mapper 代理),把 MyBatis 拆分成三层:

  1. SqlSessionFactoryBuilder:建造者,解析 xml 配置文件;

  2. SqlSessionFactory:工厂,用来创建 SqlSession;

  3. SqlSession:执行层,封装 JDBC,执行 SQL 并且把查询结果转为 Java 对象。

    底层还是原生 JDBC,只是做了封装。

    我们自上而下逐段解析。

一、整体文件结构

resources
 ├─ mybatis-config-datasource.xml 全局配置(数据库连接信息、mapper文件路径)
 ├─ School_Mapper.xml
 └─ User_Mapper.xml
java
 └─ org.itstack.demo.design
    ├─ SqlSession.java          会话接口
    ├─ DefaultSqlSession.java   会话实现类(执行SQL)
    ├─ SqlSessionFactory.java    工厂接口
    ├─ DefaultSqlSessionFactory.java
    ├─ SqlSessionFactoryBuilder.java 解析xml,构建工厂
    ├─ Configuration.java       全局配置类
    └─ XNode.java               封装每一条select标签里面的数据(sql、resultType等)

整体调用链路: SqlSessionFactoryBuilder(读取 xml 配置) → DefaultSqlSessionFactory(工厂) → DefaultSqlSession(执行 JDBC)

辅助类: Configuration(全局配置)、XNode(封装 SQL 信息)。

1.Configuration.java(全局配置类,数据载体)

public class Configuration {
    // 数据源配置信息:driver、url、username、password
    public Map<String,String> dataSource;
    // JDBC数据库连接对象
    public Connection connection;
    // key=namespace.id,value=解析之后的SQL信息XNode
    public Map<String,XNode> mapperElement;

    // getter、setter省略
    public void setDataSource(Map<String, String> dataSource) {
        this.dataSource = dataSource;
    }
    public void setConnection(Connection connection) {
        this.connection = connection;
    }
    public void setMapperElement(Map<String, XNode> mapperElement) {
        this.mapperElement = mapperElement;
    }
}

类整体说明

  1. 该类作用:存放全局解析之后的全部数据,相当于一个数据容器;

  2. 生命周期:在SqlSessionFactoryBuilder.build()的时候被创建;之后DefaultSqlSessionFactory全程持有这个对象;

  3. 成员变量:

    • dataSource:保存 xml 里面数据库的 4 项配置;
    • connection:JDBC 连接;
    • mapperElement:存放所有 mapper 文件里面解析完成的 SQL 语句;
  4. 后续DefaultSqlSession创建实例时,就是从 Configuration 取出connectionmapperElement

方法解析

  • setDataSource:把解析出来的数据库配置存入 Configuration;
  • setConnection:传入创建好的 JDBC 连接;
  • setMapperElement:存入解析完成的所有 SQL 语句。

2.XNode.java(对应 Mybatis 里 MappedStatement)

public class XNode {
    // mapper的namespace
    private String namespace;
    // select标签中的id
    private String id;
    // 参数类型
    private String parameterType;
    // 返回值类型
    private String resultType;
    // #{...}替换成?之后的SQL语句
    private String sql;
    // key:参数序号,value:参数名称
    private Map<Integer,String> parameter;

    // getter和setter省略
    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }
    public void setId(String id) {
        this.id = id;
    }
    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }
    public void setResultType(String resultType) {
        this.resultType = resultType;
    }
    public void setSql(String sql) {
        this.sql = sql;
    }
    public void setParameter(Map<Integer, String> parameter) {
        this.parameter = parameter;
    }
    public String getSql() {
        return sql;
    }
    public String getResultType() {
        return resultType;
    }
}

类整体说明

  1. 类的作用:每一条<select>标签对应一个 XNode 实例;把 xml 中 select 标签里面所有信息解析之后封装到这个对象;
  2. 创建时机:SqlSessionFactoryBuilder.mapperElement()方法里面循环解析<select>标签时 new 出来;
  3. 存放内容:namespace、id、处理后的 sql 语句、resultType、参数映射;
  4. 后续DefaultSqlSession执行 SQL 的时候,通过statementId(namespace.id)取出 XNode,拿到 SQL 语句和返回类型。

方法解析

  • set 方法:SqlSessionFactoryBuilder解析 xml 之后赋值;
  • getSql ():给 DefaultSqlSession 获取预处理之后的 SQL;
  • getResultType ():获取返回实体类全限定名,反射创建对象。

3.SqlSessionFactory.java 和DefaultSqlSessionFactory.java(工厂模式)

(1)工厂接口 SqlSessionFactory.java

public interface SqlSessionFactory {
    // 创建会话对象
    SqlSession openSession();
}

(2)实现类 DefaultSqlSessionFactory.java

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    // 持有全局配置对象
    private final Configuration configuration;

    // 构造方法接收已经解析完毕的Configuration
    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        // 把连接和解析完毕的SQL集合传给DefaultSqlSession
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }
}

类整体讲解

  1. SqlSessionFactory是顶层接口,定义工厂的唯一功能:openSession()打开数据库会话;

  2. DefaultSqlSessionFactory实现这个接口:

    • 成员变量configuration:构造方法传入,工厂全程持有全局配置;
    • openSession():创建DefaultSqlSession实例;
  3. 职责:工厂只负责生产 SqlSession 对象

  4. 调用时机:开发者拿到DefaultSqlSessionFactory之后,每次做数据库查询调用openSession()

  5. 注意:这里简化版代码每次 openSession 复用同一个 Connection,生产环境 Mybatis 会从连接池获取连接。

方法解析

  1. 构造方法DefaultSqlSessionFactory(Configuration configuration)

    接收SqlSessionFactoryBuilder创建好的配置对象;工厂后续创建 SqlSession 全部依赖这个配置;

  2. openSession()

    从 configuration 取出 connection 和 mapperElement,实例化 DefaultSqlSession 并且返回给调用者。

4.SqlSession 接口 + DefaultSqlSession 实现类(门面模式,核心执行层)

(1)SqlSession.java 接口

public interface SqlSession {
    // 查询单个结果,不带参数
    <T> T selectOne(String statement);
    // 查询单个结果,携带参数
    <T> T selectOne(String statement, Object parameter);
    // 查询列表,不带参数
    <T> List<T> selectList(String statement);
    // 查询列表,携带参数
    <T> List<T> selectList(String statement, Object parameter);
    // 关闭数据库连接
    void close();
}

(2)DefaultSqlSession.java 实现类

public class DefaultSqlSession implements SqlSession {
    // JDBC连接
    private Connection connection;
    // 预解析好的SQL语句集合
    private Map<String, XNode> mapperElement;

    // 构造方法接收连接和解析完成的SQL集合
    public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
        this.connection = connection;
        this.mapperElement = mapperElement;
    }

    @Override
    public <T> T selectOne(String statement) {
        try {
            // 通过statementId获取XNode(SQL信息)
            XNode xNode = mapperElement.get(statement);
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            // 调用方法把ResultSet转为Java对象集合
            List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            return objects.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public <T> List<T> selectList(String statement) {
        XNode xNode = mapperElement.get(statement);
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // 将ResultSet结果集利用反射转换成Java实体对象列表
    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> list = new ArrayList<>();
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 遍历每一行查询结果
            while (resultSet.next()) {
                // 反射创建对象实例
                T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    // 根据数据库字段拼接setter方法名,例如userName -> setUserName()
                    String setMethod = "set" + columnName.substring(0,1).toUpperCase() + columnName.substring(1);
                    Method method;
                    // 判断日期类型做适配
                    if (value instanceof Timestamp) {
                        method = clazz.getMethod(setMethod, Date.class);
                    } else {
                        method = clazz.getMethod(setMethod, value.getClass());
                    }
                    // 反射执行set方法给对象赋值
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    @Override
    public void close() {
        if (null == connection) return;
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // 带参数的selectOne、selectList方法这里省略
    @Override
    public <T> T selectOne(String statement, Object parameter) {return null;}
    @Override
    public <T> List<T> selectList(String statement, Object parameter) {return null;}
}

类整体讲解

  1. SqlSession定义了数据库操作对外暴露的全部方法:查询单个、查询列表、关闭连接;

  2. DefaultSqlSession是真正干活的类,成员变量:

    • connection:JDBC 连接;由 DefaultSqlSessionFactory 传递进来;
    • mapperElement:提前解析完毕的 SQL 语句 Map;
  3. 设计模式:外观模式(门面模式)

    底层的ConnectionPreparedStatementResultSet全部被封装在类内部;外部调用者只需要调用selectOneselectList,完全看不到底层 JDBC 繁杂代码;

  4. 核心功能两件事:

    • 执行 SQL 语句;
    • 通过反射把数据库返回的ResultSet自动封装成 Java 对象(ORM 思想);
  5. 线程问题:DefaultSqlSession持有 Connection,因此它是线程不安全的,每次查询都新建 SqlSession。

逐个方法详细解析

  1. 构造方法:
public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement)

接收工厂传递过来的数据库连接和预解析完成的 SQL 语句集合;之后本类所有方法都依赖这两个成员变量。

  1. selectOne(String statement)

    • 参数statement:就是namespace.id,例如com.youkeda.mapper.UserMapper.selectUser
    • 第一步:从mapperElement取出 XNode 对象,拿到处理完成的 SQL 语句;
    • 第二步:创建PreparedStatement执行 SQL 得到ResultSet
    • 第三步:调用resultSet2Obj方法把结果集转为 Java 对象集合;
    • 返回列表里面第一个元素。
  2. selectList(String statement)

    和 selectOne 逻辑几乎一致,只是返回全部列表,不会取第一条。

  3. private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz)(重中之重)

    • 参数 1:数据库返回的结果集;参数 2:要封装的实体类 Class;

    • ResultSetMetaData:获取查询出来的每一列字段名称;

    • clazz.newInstance():反射创建 DO 实体对象;

    • 根据数据库列名拼接 setter 方法:列名userName拼接成setUserName()

    • method.invoke(obj,value):反射调用 set 方法给对象赋值;

    • 一行数据库记录对应一个 Java 对象,最后装入 List 返回;

    这就是 MyBatis 实现自动把数据库记录转为 Java 对象底层原理。

  4. close()

    关闭 JDBC 数据库连接,释放资源。

5.SqlSessionFactoryBuilder.java(建造者模式,整个程序入口)

public class SqlSessionFactoryBuilder {
    // 入口方法,接收配置文件Reader,返回SqlSessionFactory工厂
    public DefaultSqlSessionFactory build(Reader reader) {
        SAXReader saxReader = new SAXReader();
        try {
            // 设置本地DTD解析器,断网环境也能解析xml文件
            saxReader.setEntityResolver(new XMLMapperEntityResolver());
            // 读取mybatis‑config‑datasource.xml文档
            Document document = saxReader.read(new InputSource(reader));
            // 解析xml全部内容,生成Configuration对象
            Configuration configuration = parseConfiguration(document.getRootElement());
            // 创建并返回工厂对象
            return new DefaultSqlSessionFactory(configuration);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 解析xml根节点,组装Configuration
    private Configuration parseConfiguration(Element root) {
        Configuration configuration = new Configuration();
        // 1.解析dataSource标签拿到数据库配置
        configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
        // 2.根据配置创建JDBC连接
        configuration.setConnection(connection(configuration.dataSource));
        // 3.解析所有mapper文件,得到SQL语句集合
        configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
        return configuration;
    }

    // 获取dataSource里面的driver、url、username、password
    private Map<String, String> dataSource(List<Element> list) {
        Map<String, String> dataSource = new HashMap<>(4);
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            dataSource.put(name, value);
        }
        return dataSource;
    }

    // 根据数据库配置创建Connection
    private Connection connection(Map<String, String> dataSource) {
        try {
            Class.forName(dataSource.get("driver"));
            return DriverManager.getConnection(dataSource.get("url"),
                    dataSource.get("username"), dataSource.get("password"));
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 解析每一个Mapper.xml文件,把#{参数}替换成?,封装成XNode存入Map
    private Map<String, XNode> mapperElement(List<Element> list) {
        Map<String, XNode> map = new HashMap<>();
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String resource = e.attributeValue("resource");
            try {
                // 读取mapper文件
                Reader reader = Resources.getResourceAsReader(resource);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(new InputSource(reader));
                Element root = document.getRootElement();
                // 获取mapper的namespace
                String namespace = root.attributeValue("namespace");
                // 获取所有select标签
                List<Element> selectNodes = root.selectNodes("select");
                for (Element node : selectNodes) {
                    String id = node.attributeValue("id");
                    String parameterType = node.attributeValue("parameterType");
                    String resultType = node.attributeValue("resultType");
                    String sql = node.getText();
                    // 使用正则表达式把#{xxx}替换成JDBC的?
                    Pattern pattern = Pattern.compile("(#\{(.*?)})");
                    Matcher matcher = pattern.matcher(sql);
                    Map<Integer, String> parameter = new HashMap<>();
                    for (int i = 1; matcher.find(); i++) {
                        String g1 = matcher.group(1);
                        String g2 = matcher.group(2);
                        parameter.put(i, g2);
                        sql = sql.replace(g1, "?");
                    }
                    // 封装XNode对象
                    XNode xNode = new XNode();
                    xNode.setNamespace(namespace);
                    xNode.setId(id);
                    xNode.setParameterType(parameterType);
                    xNode.setResultType(resultType);
                    xNode.setSql(sql);
                    xNode.setParameter(parameter);
                    // key=namespace.id
                    map.put(namespace + "." + id, xNode);
                }
            } catch (IOException | DocumentException e) {
                e.printStackTrace();
            }
        }
        return map;
    }
}

类整体讲解

  1. 这个类使用建造者模式,负责整个 Mybatis 框架初始化工作;

  2. 整个程序入口就是build()方法;开发者传入配置文件mybatis‑config‑datasource.xml的 Reader;

  3. 本类内部做三件大事:

    ① 解析全局配置文件,拿到数据库连接信息;

    ② 解析每一个 Mapper.xml 文件,把#{}占位符替换为 JDBC 里面的?;将 SQL 语句封装进 XNode 存入 Map;

    ③ 组装 Configuration 对象,创建DefaultSqlSessionFactory工厂返回;

  4. 职责划分:所有 xml 解析工作全部集中在这一个类里面,后续 Factory 和 SqlSession 只管执行 SQL,不用再解析 xml;解析和运行阶段彻底分开。

逐个方法详细解析

  1. build(Reader reader):入口方法

    • saxReader.setEntityResolver(new XMLMapperEntityResolver())

      mybatis 配置文件头部引用 dtd 约束文件;如果不配置,程序启动会联网下载 dtd;设置之后读取内置本地 dtd 文件,断网环境也可以解析 xml;

    • document = saxReader.read():dom4j 读取 xml 文档;

    • parseConfiguration():解析 xml 里面所有标签内容;

    • 最后 new DefaultSqlSessionFactory(configuration)返回工厂。

  2. parseConfiguration(Element root):组装 Configuration

    依次调用下面三个私有方法:

    • dataSource():获取 driver、url、username、password;

    • connection():创建 JDBC 连接;

    • mapperElement():解析所有 mapper 文件,处理 SQL 语句;

      把解析出来的数据全部放到 Configuration 里面。

  3. dataSource(List<Element> list)

    遍历<dataSource>标签里面的子标签,取出 name 和 value,存入 Map 返回。

  4. connection(Map<String, String> dataSource)

    • Class.forName(dataSource.get("driver"))加载数据库驱动;
    • DriverManager.getConnection()创建 JDBC 连接对象。
  5. mapperElement(List<Element> list)(最核心方法)

    1. 循环读取<mapper resource="User_Mapper.xml">

    2. 获取 mapper 标签中的namespace属性;

    3. 获取<select>标签里面 id、parameterType、resultType、原生 sql;

    4. 正则表达式匹配#{里面的参数},把#{userId}替换成?;并且记录参数顺序;

    5. 实例化 XNode 对象保存处理之后的全部信息;

    6. namespace+"."+id作为 key,XNode 作为 value 存入 Map;

    解析之后 SqlSession 就可以通过 statementId 快速找到对应的 SQL 语句。

完整调用流程,串联全部类

// 1.读取配置文件
Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
// 2.建造者解析xml,生成SqlSessionFactory
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
DefaultSqlSessionFactory factory = builder.build(reader);
// 3.工厂创建SqlSession会话对象
SqlSession sqlSession = factory.openSession();
// 4.执行数据库查询
User user = sqlSession.selectOne("com.youkeda.mapper.UserMapper.selectUser");
// 5.关闭连接
sqlSession.close();
  1. SqlSessionFactoryBuilder.build():解析 xml 生成 Configuration,创建工厂;
  2. factory.openSession():工厂创建DefaultSqlSession
  3. sqlSession.selectOne():内部执行 JDBC,反射封装 Java 对象;
  4. close()关闭连接。

用到的设计模式总结

  1. 建造者模式: SqlSessionFactoryBuilder负责解析 xml 组装复杂 Configuration 对象;
  2. 工厂模式: SqlSessionFactory生产 SqlSession 会话实例;
  3. 外观模式: DefaultSqlSession封装底层 JDBC 复杂 API,对外提供简单查询方法;
  4. 反射机制: resultSet2Obj()实现 ORM 映射,数据库记录自动转为 Java 对象。

到这我其实是有一个疑问的就是为什么非要经过 Configuration 和 Factory 中转,为什么不直接 new DefaultSqlSession (connection,mapperElement)?

设想的简化写法:

//build里面直接new DefaultSqlSession(connection,mapperElement);返回给调用者

真实代码是:

configuration → DefaultSqlSessionFactory → openSession() → new DefaultSqlSession(connection,mapperElement)
  1. 职责拆分(最重要):解析阶段和运行阶段分开

    • SqlSessionFactoryBuilder:只做一次性解析 xml 工作,解析完它的生命周期就结束;

    • DefaultSqlSessionFactory长期存活,全局单例,全程持有configuration;以后每次调用openSession()创建新的 SqlSession;

      如果跳过 Factory,直接返回 SqlSession:

    • SqlSession 里面包含 Connection,SqlSession 用完就要关闭;一旦关闭连接就失效;后续再查询没有办法再次创建新连接;

    • Factory 长期存活,每次openSession()都可以生成全新的 SqlSession;原生 Mybatis 里面 Factory 是全局单例,SqlSession 每次用完销毁。

  2. 后续扩展考虑

    以后扩展可以写openSession(boolean autoCommit)、可以选择开启事务;工厂统一控制创建会话逻辑;

  3. 配置统一管理

    Configuration里面不光只有 connection 和 mapperElement,后续还可以存放分页配置、日志配置、插件配置;全部放在 Configuration 统一管理,而不是零散传递参数。

一句话大白话:Builder 只干一次性解析工作;Factory 长期存在负责生产会话对象;职责分开,后期更好扩展。

二、接下来可以测试验证一下

1.MySQL 执行建表语句

新建数据库 itstack_demo_design,执行下面 SQL:

CREATE DATABASE IF NOT EXISTS itstack_demo_design;
USE itstack_demo_design;

CREATE TABLE user (
 id bigint NOT NULL AUTO_INCREMENT,
 name varchar(32),
 age int(4),
 address varchar(128),
 entryTime datetime,
 remark varchar(64),
 createTime datetime,
 updateTime datetime,
 status int(4) DEFAULT '0',
 dateTime varchar(64),
 PRIMARY KEY (id),
 INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values
(1, '水水', 18, '吉林省榆树市黑林镇尹家村5组', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309'),
(2, '豆豆', 18, '辽宁省大连市清河湾司马道407路', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null),
(3, '花花', 19, '辽宁省大连市清河湾司马道407路', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');

CREATE TABLE school (
 id bigint NOT NULL AUTO_INCREMENT,
 name varchar(64),
 address varchar(256),
 createTime datetime,
 updateTime datetime,
 PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into school (id, name, address, createTime, updateTime) values
(1,'北京大学', '北京市海淀区颐和园路5号', '2019-10-18 13:35:57', '2019-10-18 13:35:57'),
(2,'南开大学', '中国天津市南开区卫津路94号', '2019-10-18 13:35:57', '2019-10-18 13:35:57');

2.创建 PO 实体类

User.java

public class User {
    private Long id;
    private String name;
    private Integer age;
    private Date createTime;
    private Date updateTime;

    // ... get/set
}

School.java

public class School {
    private Long id;
    private String name;
    private String address;
    private Date createTime;
    private Date updateTime;

    // ... get/set
}

3.Dao 接口(仅用来提供 namespace,简化版 Mybatis 并不会执行接口里面方法)

public interface IUserDao {
    User queryUserInfoById(Long id);
}
public interface ISchoolDao {
    School querySchoolInfoById(Long id);
}

4.目录配置文件

(1)mybatis‑config‑datasource.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="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>
</configuration>

(2)mapper/User_Mapper.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="org.itstack.demo.design.dao.IUserDao">
    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime FROM user where id = #{id}
    </select>
    <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime FROM user where age = #{age}
    </select>
</mapper>

(3)mapper/School_Mapper.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="org.itstack.demo.design.dao.ISchoolDao">
    <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
        SELECT id, name, address, createTime, updateTime FROM school where id = #{id}
    </select>
</mapper>

5.测试类 ApiTest.java

public class ApiTest {
    private static final Logger logger = LoggerFactory.getLogger(ApiTest.class);

    // 测试根据id查询单个用户
    public static void testQueryUserById() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            // 1.读取配置文件
            reader = Resources.getResourceAsReader(resource);
            // 2.建造者解析xml,生成工厂(启动阶段:只执行这一次解析xml)
            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory factory = builder.build(reader);
            // 3.工厂创建会话
            SqlSession sqlSession = factory.openSession();
            try {
                // statementId = namespace.id
                User user = sqlSession.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById");
                logger.info("查询单个用户结果:{}", JSON.toJSONString(user));
            } finally {
                // 关闭连接释放资源
                sqlSession.close();
                reader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 查询学校信息
    public static void testQuerySchoolById() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory factory = builder.build(reader);
            SqlSession sqlSession = factory.openSession();
            try {
                School school = sqlSession.selectOne("org.itstack.demo.design.dao.ISchoolDao.querySchoolInfoById");
                logger.info("查询学校结果:{}", JSON.toJSONString(school));
            } finally {
                sqlSession.close();
                reader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        testQueryUserById();
        testQuerySchoolById();
    }
}

6.运行之后输出结果示例

INFO org.itstack.demo.design.ApiTest - 查询单个用户结果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
INFO org.itstack.demo.design.ApiTest - 查询学校结果:{"address":"北京市海淀区颐和园路5号","createTime":1571387757000,"id":1,"name":"北京大学","updateTime":1571387757000}

现在全部完成了后面感觉可以

  • 完善selectOne(String statement,Object parameter)方法:实现给?赋值,对应 Mybatis 的ParameterHandler
  • 实现 JDK 动态代理,摆脱手写字符串statementId,可以直接调用 iUserDao.queryUserInfoById(1L)
  • 引入 HikariCP 连接池替换现在唯一的 Connection;
  • 实现事务功能;
  • 实现 Mybatis 拦截器插件。