在前一节中,我们通过扫描包路径注册映射器,规范化SqlSession来描述sql行为。但是我们实际mybatis中,这些细节都是从xml解析出来的,包括包路径和sql语句(含入参,返参,id等)。

思考:符合生产实际的XML是怎么解析到mybatis中使用,像前一节我们使用映射器注册机贯穿整个流程,那么mybatis中又是使用哪个核心配置去把数据贯穿的呢?

本章重点:1、本章节使用dom4j去解析XML;2、新建Configuration类,这个类在mybatis中是最重要的核心配置类,将存储映射器、sql相关的一切数据。

1、目标

在手写mybatis的过程中,我们最重要的目标导向:Mybatis 的核心逻辑怎么实现。

其实我们可以把这样一个 ORM 框架的目标,简单的描述成是为了给一个接口提供代理类,类中包括了对 Mapper 也就是 xml 文件中的 SQL 信息(类型入参出参条件)进行解析和处理,这个处理过程就是对数据库的操作以及返回对应的结果给到接口。如图

按照 ORM 核心流程的执行过程,我们本章节就需要在上一章节的基础上,继续扩展对 Mapper 文件的解析以及提取出对应的 SQL 文件。并在当前这个阶段,可以满足我们调用 DAO 接口方法的时候,可以返回 Mapper 中对应的待执行 SQL 语句。

本章先不连接数据库。

2、设计

结合上一章节我们使用了 MapperRegistry 对包路径进行扫描注册映射器,并在 DefaultSqlSession 中进行使用。那么在我们可以把这些命名空间、SQL描述、映射信息统一维护到每一个 DAO 对应的 Mapper XML 的文件以后,其实 XML 就是我们的源头了。通过对 XML 文件的解析和处理就可以完成 Mapper 映射器的注册和 SQL 管理。

  • 首先需要定义 SqlSessionFactoryBuilder 工厂建造者模式类,通过入口 IO 的方式对 XML 文件进行解析。当前我们主要以解析 SQL 部分为主,并注册映射器,串联出整个核心流程的脉络。

  • 文件解析以后会存放到 Configuration 配置类中,接下来你会看到这个配置类会被串联到整个 Mybatis 流程中,所有内容存放和读取都离不开这个类。如我们在 DefaultSqlSession 中获取 Mapper 和执行 selectOne 也同样是需要在 Configuration 配置类中进行读取操作。

3、实现

3.1 工程结构

以下是本次的工程结构,本次新增的文件有:MapperMethod(映射器方法,例如SELECT等方法)、XMLConfigBuilder(XML配置构建器,建造者模式,继承BaseBuilder-(此接口有configuration成员变量,进行传递),完成多级xml的解析工作)、Resources(对IO进行处理并生成Reader对象)、MappedStatement(sql的id、入参、返参等多种属性类)、SqlCommandType(SQL 指令类型枚举)、Configuration(配置项 会贯穿mybatis全局)SqlSessionFactoryBuilder(读取主xml的入口工厂)

以上颜色标记的类是本章节最主要的研究对象。

mybatis-03
└── src
    ├── main
    │   └── java
    │       └── cn.sunnyy.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           │   ├── xml
    │           │   │   └── XMLConfigBuilder.java
    │           │   └── BaseBuilder.java
    │           ├── io
    │           │   └── Resources.java
    │           ├── mapping
    │           │   ├── MappedStatement.java
    │           │   └── SqlCommandType.java
    │           └── session
    │               ├── defaults
    │               │   ├── DefaultSqlSession.java
    │               │   └── DefaultSqlSessionFactory.java
    │               ├── Configuration.java
    │               ├── SqlSession.java
    │               ├── SqlSessionFactory.java
    │               └── SqlSessionFactoryBuilder.java
    └── test
        ├── java
        │   └── cn.sunnyy.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml 

XML 解析和注册类实现关系如下图:

  • SqlSessionFactoryBuilder 作为整个 Mybatis 的入口,提供建造者工厂,包装 XML 解析处理,并返回对应 SqlSessionFactory 处理类。

  • 通过解析把 XML 信息注册到 Configuration 配置类中,再通过传递 Configuration 配置类到各个逻辑处理类里,包括 DefaultSqlSession 中,这样就可以在获取映射器和执行SQL的时候,从配置类中拿到对应的内容了。

3.2 构建SqlSessionFactory建造者工厂

package cn.sunnyy.mybatis.session;

import cn.sunnyy.mybatis.builder.xml.XMLConfigBuilder;
import cn.sunnyy.mybatis.session.impl.DefaultSqlSessionFactory;

import java.io.Reader;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:31
 * 构建SqlSessionFactory的工厂
 */
public class SqlSessionFactoryBuilder {

    /**
     * 这里传入的是mybatis的主xml文件
     * @param reader
     * @return
     */
    public SqlSessionFactory build(Reader reader) {
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
        return build(xmlConfigBuilder.parse());
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}
  • SqlSessionFactoryBuilder 是作为整个 Mybatis 的入口类,通过指定解析XML的IO,引导整个流程的启动。

  • 从这个类开始新增加了 XMLConfigBuilder、Configuration 两个处理类,分别用于解析 XML 和串联整个流程的对象保存操作。

3.3 XML 解析处理

package cn.sunnyy.mybatis.builder.xml;

import cn.sunnyy.mybatis.builder.BaseBuilder;
import cn.sunnyy.mybatis.io.Resources;
import cn.sunnyy.mybatis.mapping.MappedStatement;
import cn.sunnyy.mybatis.mapping.SqlCommandType;
import cn.sunnyy.mybatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

import java.io.Reader;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:50
 * XML配置构建器,建造者模式,继承BaseBuilder
 */
public class XMLConfigBuilder extends BaseBuilder {

    private Element root;

    public XMLConfigBuilder(Reader reader) {
        // 1. 调用父类初始化Configuration
        super(new Configuration());
        // 2. dom4j 处理 xml
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new InputSource(reader));
            root = document.getRootElement();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析配置;类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
     *
     * @return Configuration
     */
    public Configuration parse() {
        try {
            // 解析映射器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    /**
     * 对传入的mybatis主xml文件解析,把多条mapper全部读取到configuration中去
     * @param mappers mybatis主xml文件的Element
     * @throws Exception Exception
     */
    private void mapperElement(Element mappers) throws Exception {
        /**
         * Element mappers 中的文件格式
         *      <mappers>
         *         <mapper resource="mapper/User_Mapper.xml"/>
         *     </mappers>
         */
        // 获取到mapper标签内容
        List<Element> mapperList = mappers.elements("mapper");
        // 遍历mapper标签
        for (Element e : mapperList) {
            // 获取到 resource 中的mapper路径
            String resource = e.attributeValue("resource");
            // 再次读取Reader--这次是子mapper的
            Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            /**
             * 子mapper的格式
             *  <mapper namespace="cn.sunnyy.mybatis.test.dao.IUserDao">
             *
             *     <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.sunnyy.mybatis.test.po.User">
             *         SELECT id, userId, userHead, createTime
             *         FROM user
             *         where id = #{id}
             *     </select>
             *
             * </mapper>
             */
            Element root = document.getRootElement();
            //命名空间 mapper的全路径
            String namespace = root.attributeValue("namespace");

            // SELECT 标签(目前先讨论只有select的情况)
            List<Element> selectNodes = root.elements("select");
            for (Element node : selectNodes) {
                // 方法名
                String id = node.attributeValue("id");
                // 入参类型
                String parameterType = node.attributeValue("parameterType");
                // 返回参数类型
                String resultType = node.attributeValue("resultType");
                // sql语句
                String sql = node.getText();

                // ? 匹配
                Map<Integer, String> parameter = new HashMap<>();
                // 正则表达式 匹配形如 #{something} 的字符串
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) {
                    // g1 将获取匹配的整个字符串(例如 #{something})
                    String g1 = matcher.group(1);
                    // g2 将获取大括号内的内容(例如 something)
                    String g2 = matcher.group(2);
                    // 参数占位列表
                    parameter.put(i, g2);
                    //  SQL 字符串中的 #{something} 替换为 ?  这通常用于准备 SQL 语句,以防止 SQL 注入攻击
                    sql = sql.replace(g1, "?");
                }

                String msId = namespace + "." + id;
                String nodeName = node.getName();
                // 匹配sql语句类型
                SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
                MappedStatement mappedStatement = new MappedStatement.Builder(
                        configuration,
                        msId,
                        sqlCommandType,
                        parameterType,
                        resultType,
                        sql,
                        parameter).build();
                // 添加解析 SQL
                configuration.addMappedStatement(mappedStatement);
            }

            // 注册Mapper映射器
            configuration.addMapper(Resources.classForName(namespace));
        }
    }
}
  • XMLConfigBuilder 核心操作在于初始化 Configuration,因为 Configuration 的使用离解析 XML 和存放是最近的操作,所以放在这里比较适合。

  • 之后就是具体的 parse() 解析操作,并把解析后的信息,通过 Configuration 配置类进行存放,包括:添加解析 SQL(configuration.addMappedStatement(mappedStatement);)、注册Mapper映射器(configuration.addMapper(Resources.classForName(namespace));)

  • 解析配置整体包括:类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器,但目前我们还不需要那么多,所以只做一些必要的 SQL 解析处理。

3.4 通过配置类包装注册机和SQL语句

package cn.sunnyy.mybatis.session;

import cn.sunnyy.mybatis.binding.MapperRegistry;
import cn.sunnyy.mybatis.mapping.MappedStatement;

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

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:35
 * 配置项 会贯穿mybatis全局
 * 目前涵盖 映射注册机 和 映射语句
 */
public class Configuration {

    /**
     * 映射注册机
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    /**
     * 映射的语句,存在Map里
     */
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();

    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

    public boolean hasMapper(Class<?> type) {
        return mapperRegistry.hasMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }

    public MappedStatement getMappedStatement(String id) {
        return mappedStatements.get(id);
    }
}
  • 映射器注册机是我们上一章节实现的内容,用于注册 Mapper 映射器锁提供的操作类。

  • 另外一个 MappedStatement 是本章节新添加的 SQL 信息记录对象,包括记录:SQL类型、SQL语句、入参类型、出参类型等。

3.5 DefaultSqlSession结合配置项获取信息

package cn.sunnyy.mybatis.session.impl;

import cn.sunnyy.mybatis.mapping.MappedStatement;
import cn.sunnyy.mybatis.session.Configuration;
import cn.sunnyy.mybatis.session.SqlSession;
import com.alibaba.fastjson.JSON;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/26 22:14
 * 默认SqlSession实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }
    @Override
    public <T> T selectOne(String statement) {
        return (T) ("你的操作被代理了!" + statement);
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement mappedStatement = configuration.getMappedStatement(statement);
        return (T) ("你的操作被代理了!"
                + "\n方法:"
                + statement
                + "\n入参:"
                + JSON.toJSONString(parameter)
                + "\n待执行SQL:"
                + mappedStatement.getSql());
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }
}
  • DefaultSqlSession 相对于上一章节,把 MapperRegistry mapperRegistry 替换为 Configuration configuration,这样才能传递更丰富的信息内容,而不只是注册器操作。

  • 之后在 DefaultSqlSession#selectOne、DefaultSqlSession#getMapper 两个方法中都使用 configuration 来获取对应的信息。

  • 目前 selectOne 方法中只是把获取的信息进行打印,后续将引入 SQL 执行器进行结果查询并返回。

3.6 其他辅助类

3.6.1 MapperMethod(映射器方法,例如SELECT等方法)

package cn.sunnyy.mybatis.binding;

import cn.sunnyy.mybatis.mapping.MappedStatement;
import cn.sunnyy.mybatis.mapping.SqlCommandType;
import cn.sunnyy.mybatis.session.Configuration;
import cn.sunnyy.mybatis.session.SqlSession;

import java.lang.reflect.Method;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 21:19
 * 映射器方法
 */
public class MapperMethod {

    private final SqlCommand command;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
        this.command = new SqlCommand(configuration, mapperInterface, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result = null;
        switch (command.getType()) {
            case INSERT:
                break;
            case DELETE:
                break;
            case UPDATE:
                break;
            case SELECT:
                result = sqlSession.selectOne(command.getName(), args);
                break;
            default:
                throw new RuntimeException("Unknown execution method for: " + command.getName());
        }
        return result;
    }

    /**
     * SQL 指令
     */
    public static class SqlCommand {

        private final String name;
        private final SqlCommandType type;

        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
            String statementName = mapperInterface.getName() + "." + method.getName();
            MappedStatement ms = configuration.getMappedStatement(statementName);
            name = ms.getId();
            type = ms.getSqlCommandType();
        }

        public String getName() {
            return name;
        }

        public SqlCommandType getType() {
            return type;
        }
    }
}

3.6.2 Resources(对IO进行处理并生成Reader对象)

package cn.sunnyy.mybatis.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:57
 */
public class Resources {

    /**
     * 自己封装提供的Reader方法
     * @param resource
     * @return
     * @throws IOException
     */
    public static Reader getResourceAsReader(String resource) throws IOException {
        return new InputStreamReader(getResourceAsStream(resource));
    }

    private static InputStream getResourceAsStream(String resource) throws IOException {
        ClassLoader[] classLoaders = getClassLoaders();
        for (ClassLoader classLoader : classLoaders) {
            InputStream inputStream = classLoader.getResourceAsStream(resource);
            if (null != inputStream) {
                return inputStream;
            }
        }
        throw new IOException("Could not find resource " + resource);
    }

    private static ClassLoader[] getClassLoaders() {
        return new ClassLoader[]{
                ClassLoader.getSystemClassLoader(),
                Thread.currentThread().getContextClassLoader()};
    }

    /*
     * Loads a class
     *
     * @param className - the class to fetch
     * @return The loaded class
     * @throws ClassNotFoundException If the class cannot be found (duh!)
     */
    public static Class<?> classForName(String className) throws ClassNotFoundException {
        return Class.forName(className);
    }
}

3.6.3 MappedStatement(sql的id、入参、返参等多种属性类)

package cn.sunnyy.mybatis.mapping;

import cn.sunnyy.mybatis.session.Configuration;

import java.util.Map;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:38
 * 映射语句类 存储sql相关的内容
 */
public class MappedStatement {

    private Configuration configuration;
    /**
     * sql的唯一id
     */
    private String id;

    /**
     * sql的类型
     */
    private SqlCommandType sqlCommandType;

    /**
     * sql的请求参数类型
     */
    private String parameterType;

    /**
     * sql的响应类型
     */
    private String resultType;

    /**
     * sql语句
     */
    private String sql;

    /**
     * 参数占位列表
     */
    private Map<Integer, String> parameter;

    MappedStatement() {
        // constructor disabled
    }

    /**
     * 建造者
     */
    public static class Builder {

        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration,
                       String id,
                       SqlCommandType sqlCommandType,
                       String parameterType,
                       String resultType,
                       String sql,
                       Map<Integer, String> parameter) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlCommandType = sqlCommandType;
            mappedStatement.parameterType = parameterType;
            mappedStatement.resultType = resultType;
            mappedStatement.sql = sql;
            mappedStatement.parameter = parameter;
        }

        public MappedStatement build() {
            assert mappedStatement.configuration != null;
            assert mappedStatement.id != null;
            return mappedStatement;
        }

    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    public String getId() {
        return id;
    }

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

    public SqlCommandType getSqlCommandType() {
        return sqlCommandType;
    }

    public void setSqlCommandType(SqlCommandType sqlCommandType) {
        this.sqlCommandType = sqlCommandType;
    }

    public String getParameterType() {
        return parameterType;
    }

    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }

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

    public Map<Integer, String> getParameter() {
        return parameter;
    }

    public void setParameter(Map<Integer, String> parameter) {
        this.parameter = parameter;
    }
}

3.6.4 SqlCommandType(SQL 指令类型枚举)

package cn.sunnyy.mybatis.mapping;

/**
 * @author sunzhen
 * @version 1.0
 * @date 2024/8/28 20:40
 * SQL 指令类型
 */
public enum SqlCommandType{

    /**
     * 未知
     */
    UNKNOWN,
    /**
     * 插入
     */
    INSERT,
    /**
     * 更新
     */
    UPDATE,
    /**
     * 删除
     */
    DELETE,
    /**
     * 查找
     */
    SELECT;

}

4、测试步骤

4.1 准备

提供 DAO 接口和对应的 Mapper xml 配置

package cn.sunnyy.mybatis.test.dao;

public interface IUserDao {

    String queryUserInfoById(String uId);

}
<?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="cn.sunnyy.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.sunnyy.mybatis.test.po.User">
        SELECT id, userId, userHead, createTime
        FROM user
        where id = #{id}
    </select>

</mapper>

主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>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>

</configuration>

4.2 单元测试

package cn.sunnyy.mybatis.test;

import cn.sunnyy.mybatis.io.Resources;
import cn.sunnyy.mybatis.session.SqlSession;
import cn.sunnyy.mybatis.session.SqlSessionFactory;
import cn.sunnyy.mybatis.session.SqlSessionFactoryBuilder;
import cn.sunnyy.mybatis.test.dao.IUserDao;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.Reader;

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_SqlSessionFactory() throws IOException {
        // 1. 从SqlSessionFactory中获取SqlSession
        Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 2. 获取映射器对象
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);

        // 3. 测试验证
        String res = userDao.queryUserInfoById("10001");
        logger.info("测试结果:{}", res);
    }

}
  • 目前的使用方式就和 Mybatis 非常像了,通过加载 xml 配置文件,交给 SqlSessionFactoryBuilder 进行构建解析,并获取 SqlSessionFactory 工厂。这样就可以顺利的开启 Session 以及完成后续的操作。

测试结果:

21:13:16.970 [main] INFO  cn.sunnyy.mybatis.test.ApiTest - 测试结果:你的操作被代理了!
方法:cn.sunnyy.mybatis.test.dao.IUserDao.queryUserInfoById
入参:["10001"]
待执行SQL:
        SELECT id, userId, userHead, createTime
        FROM user
        where id = ?
    

Process finished with exit code 0
  • 从测试结果我们可以看到,目前的代理操作已经可以把我们从 XML 中解析的 SQL 信息进行打印了,后续我们将结合这部分的处理继续完成数据库的操作。

5、总结

  • 了解 ORM 处理的核心流程,知晓目前我们所处在的步骤和要完成的内容,只有非常清楚的知道这个代理、封装、解析和返回结果的过程才能更好的完成整个框架的实现。

  • SqlSessionFactoryBuilder 的引入包装了整个执行过程,包括:XML 文件的解析、Configuration 配置类的处理,让 DefaultSqlSession 可以更加灵活的拿到对应的信息,获取 Mapper 和 SQL 语句。

  • 另外从整个工程搭建的过程中,可以看到有很多工厂模式、建造者模式、代理模式的使用,也有很多设计原则的运用,这些技巧都可以让整个工程变得易于维护和易于迭代。