转载

带你一步一步手写一个简单的 Mybatis

在前两篇文章中我向你介绍了 Mybatis 的构建和执行流程,这篇文章中我会带领你一步一步手写一个简单的 Mybatis 框架。

本文主要涉及代码实现,很多要点会在代码注释中说明,请仔细阅读。

所有代码已经在 github 上托管,感兴趣的同学可以自行 fork 。看完记得点赞哦(#^.^#)

仿写框架的文章,我会尽量将源代码贴出来(很多思路已经写在了源码注释中),如果。。。。

如果还没看过我前两篇文章的请戳这里

  • 带你一步一步手撕 Mybatis 源码加手绘流程图——构建部分
  • 带你一步一步手撕 Mybatis 源码加手绘流程图——执行部分

提炼构建部分核心类

既然是仿写一个简单的,那么我们就不可能面面俱到,和分析源码一样,我们需要一步一步跟着 主线 走。所以我们首先要提炼出整个框架构建流程所涉及到的核心类,然后再去仿写。

在第一篇的构建文章中我画了一张简单的流程图,这里我直接拿来用,以便提炼我们的核心类。

带你一步一步手写一个简单的 Mybatis

在第一篇文章中我已经通过源码向大家解释了这张图的由来,这里我不再赘述。直接动手开干吧!

构建部分仿写

SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder

在第二篇文章中我画了关于 SqlSessionFactory工厂模式UML

带你一步一步手写一个简单的 Mybatis

我们可以通过这张图构建一个简单的 SqlSessionSqlSessionFactory

public interface SqlSession {
   // 目前什么都没有 不用管 具体内容应该在执行部分
}
复制代码
public interface SqlSessionFactory {
    // 创建 SqlSession
    SqlSession openSqlSession();
}
复制代码

你肯定不禁感叹,这 tnd 也太简单了。

其实就是这么简单。

当然,有了 SqlSessionSqlSessionFactory 之后还需要使用一个 SqlSessionFactoryBuilder 来构建 SqlSessionFactory 。这里使用到了 构建者模式

public class SqlSessionFactoryBuilder {
    // 通过输入流去创建 XMLConfigBuilder
    public SqlSessionFactory build(InputStream inputStream) {
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(inputStream);
        // 通过 Configuration 去构建默认SqlSession工厂
        return new DefaultSqlSessionFactory(xmlConfigBuilder.parse());
    }
}
复制代码

在上面的代码中就出现了我们上面流程图的东西了, 这个方法会传入一个 InputStream 输入流,然后我们通过这个输入流去创建一个 XmlConfigBuidler (这是一个 Configuration 的构建者),然后我们通过 XmlConfigBuilder 中的 parse() 方法构建一个 Configuration 对象,最终配置对象传入 SqlSessionFactory ,Sql会话工厂就创建成功了。

此时我们肯定有一些疑问

  1. InputStream 怎么来的?
  2. XmlConfigBuilder 如何构建 Configuration 的?

首先我来解答一下第一个问题,这其实非常简单。因为我们构建这个 配置对象 是基于配置文件的,所以输入流肯定是从 配置文件中转换过来的

public class Resource {
    // 通过文件路径获取输入流
    public static InputStream getResourceAsStream(String resource) {
        if (resource == null || "".equals(resource)) {
            return null;
        }
        return Resource.class.getClassLoader().getResourceAsStream(resource);
    }
}
复制代码

XmlConfigBuilder、XMLMapperBuilder

第一个问题解决了,那第二个呢?我们先来写一个简单的 XmlConfigBuilder (后面会补充)

public class XmlConfigBuilder {
    // 输入流
    private InputStream inputStream;
    // 构建的 configuration
    private Configuration configuration;

    public XmlConfigBuilder(InputStream inputStream) {
        this.inputStream = inputStream;
        this.configuration = new Configuration();
    }
    // 解析然后返回 Configuration
    public Configuration parse() {
        // 通过 Dom4j 解析xml
        Document document = DocumentReader.getDocument(this.inputStream);
        // 这里就是解析根标签
        parseConfiguration(document.getRootElement());
        return configuration;
    }
    // 从根标签开始解析
    private void parseConfiguration(Element rootElement) {
        // 解析 environments 子标签
        parseEnvironmentsElement(rootElement.element("environments"));
        // 解析 mappers 子标签
        parseMappersElement(rootElement.element("mappers"));
    }

    @SuppressWarnings("unchecked")
    private void parseMappersElement(Element mappers) {
        // 这里就是解析 mappers子标签的 具体流程了
        // 主要是遍历 mappers 的 mapper 子标签 
        // 然后再通过 XMLMapperBuilder 去解析对应的 mapper 映射文件
    }

    @SuppressWarnings("unchecked")
    private void parseEnvironmentsElement(Element element) {
        System.out.println(element == null);
        // 获取默认环境
        String defaultEnvironment = element.attributeValue("default");
        // 获取environment标签
        List<Element> environmentList = element.elements();
        for (Element environment : environmentList) {
            String environmentId = environment.attributeValue("id");
            if (defaultEnvironment.equals(environmentId)) {
                // 创建数据源
                createDataSource(environment);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void createDataSource(Element element) {
        Element dataSource = element.element("dataSource");
        // 获取数据源类型
        String dataSourceType = dataSource.attributeValue("type");
        List<Element> propertyElements = dataSource.elements();
        Properties properties = new Properties();
        for (Element property : propertyElements) {
            String name = property.attributeValue("name");
            String value = property.attributeValue("value");
            properties.setProperty(name, value);
        }
        DruidDataSource datasource = null;
        if ("Druid".equals(dataSourceType)) {
            datasource = new DruidDataSource();
            // 获取驱动
            datasource.setDriverClassName(properties.getProperty("driver"));
            // 数据库连接的 url
            datasource.setUrl(properties.getProperty("url"));
            // 数据库用户名
            datasource.setUsername(properties.getProperty("username"));
            // 数据库密码
            datasource.setPassword(properties.getProperty("password"));
        }
        // 设置配置对象中的数据源字段
        configuration.setDataSource(datasource);
    }
}
复制代码

这里对于 mappers 标签的解析只做了简单的中文注释,等会会再次介绍。

其实 XmlConfigBuilder 做的事很简单, 根据配置文件创建配置对象 ,这里我只做了对于 <environments><mappers> 标签的解析,因为 <environments> 标签涉及到对于 数据源的配置 ,而 <mappers> 则是对于 映射文件的配置 ,二者都是最重要的,如果仅仅需要实现一个最简单的 Mybatis,这两个也是必须的。

我们可以结合着简单配置文件的内容理解上面的代码。

<configuration>
	<!-- mybatis 数据源环境配置 -->
	<environments default="dev">
		<environment id="dev">
			<!-- 配置数据源信息 -->
			<dataSource type="Druid">
				<property name="driver" value="com.mysql.jdbc.Driver"></property>
				<property name="url"
					value="jdbc:mysql://localhost:3306/custom_mybatis"></property>
				<property name="username" value="root"></property>
				<property name="password" value="xxxxxx"></property>
			</dataSource>
		</environment>
	</environments>

	<!-- 映射文件加载 -->
	<mappers>
		<!-- resource指定映射文件的类路径 -->
		<mapper resource="mapper/UserMapper.xml"></mapper>
	</mappers>
</configuration>
复制代码

在上面我使用到了 dom4jDruid 数据源 以及 mysql 连接驱动,你也需要在 pom.xml 文件中配置相应的 jar 包。

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.20</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
</dependency>
<!-- 这里我使用了 lombok 简化代码 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>
复制代码

理解了上面的代码,我们就可以来补充在我们自定义的 XmlConfigBuilder 类中对于 <mappers> 标签的解析。其实就是我们上面流程图的右边部分。

带你一步一步手写一个简单的 Mybatis
// 上面解析的代码
private void parseMappersElement(Element mappers) {
    // 遍历 mappers 子标签
    List<Element> mapperElements = mappers.elements();
    for (Element element : mapperElements) {
        // 获取文件路径
        String resource = element.attributeValue("resource");
        // 通过路径获取流
        InputStream inputStream = Resource.getResourceAsStream(resource);
        // 创建一个 XMLMapperBuilder 去构建 关于 mapper文件的 配置对象
        XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(inputStream, configuration);
        xmlMapperBuilder.parse();
    }
}
复制代码

其实你会发现,这里的 XMLMapperBuilder 和上面 XmlConfigBuilder 的流程基本一模一样。

这个时候我们来看一下关于 XmlMapperBuilder 到底长什么样。

@Data
@AllArgsConstructor
public class XmlMapperBuilder {
    // 和 XmlConfigBuilder 差不多呀。。
    private InputStream inputStream;
    private Configuration configuration;
    // 解析
    public void parse() {
        Document document = DocumentReader.getDocument(this.inputStream);
        parseMapperElement(document.getRootElement());
    }
    @SuppressWarnings("unchecked")
    private void parseMapperElement(Element rootElement) {
        // 首先查看 namespace 
        // 在 mybatis 中在注册 MappedStatement 会先给它的id 加上 namespace 命名空间
        // 这里上面都没做 只是做个判断 如果没有则抛出异常
        String namespace = rootElement.attributeValue("namespace");
        try {
            if (namespace == null || "".equals(namespace)) {
                throw new Exception("namespace is null");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 直接解析 select 标签
        // 在 mybatis 中对应的是这个
        // buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        // 这里只解析了 select 标签 而且没有用到 xpath 语法
        parseStatementElements(rootElement.elements("select"));
    }
    // 解析所有的 select 标签
    private void parseStatementElements(List<Element> select) {
        for (Element element: select) {
            parseStatementElement(element);
        }
    }
    // 这里就是解析 select 标签的具体流程
    private void parseStatementElement(Element element) {
        /**
        * <select id="findUserById" parameterType="java.lang.Integer" resultType="test.domain.User">
	* SELECT * FROM user WHERE id = #{id} 
	* </select>
	*/
        // 获取 select 的id 这里会作为key 存储到 MappedStatement 的集合中
        String id = element.attributeValue("id");
        // 获取参数类型并解析
        String parameterType = element.attributeValue("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
        // 获取结果类型并解析
        String resultType = element.attributeValue("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        // 获取 Statement 类型
        // 这里其实对应着 JDBC 中的 Statement 类型
        // 我默认使用了 PreparedStatement 预编译类型
        String statementTypeString = element.attributeValue("statementType") == null ? "prepared"
                : element.attributeValue("statementType");

        StatementType statementType = "prepared".equals(statementTypeString) ? StatementType.PREPARED : StatementType.STATEMENT;
        // 创建 sqlSource 这里存储了 sql文本 已经整个 crud 标签的信息
        SqlSource sqlSource = createSqlSource(element);
        // 生成 MappedStatement 对象 很重要
        MappedStatement mappedStatement = new MappedStatement(configuration, id, statementType, sqlSource,
                parameterTypeClass, resultTypeClass);
        // 加入配置 其实就是加入里面的 map 中
        configuration.addMapStatement(mappedStatement);
    }

    private SqlSource createSqlSource(Element element) {
        String text = element.getTextTrim();
        return new SqlSource(text);
    }
    // 转换为 class
    private Class<?> resolveClass(String parameterType) {
        try {
            return Class.forName(parameterType);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
复制代码

MappedStatement

在上面我们已经将 XmlConfigBuilderXmlMapperBuilder 打通了,也就是 XmlMapperBuilder XmlConfigBuilder 中的很重要的一个子构建过程 。所以 XmlMapperBuilder 其实也是 Configuartion 对象的构建者,而在其中它主要构建了 MappedStatement 对象。这样 ConfigurationXmlMapperBuilder 就也打通了。

所以接下来的重头戏就是 MappedStatement 了。

@Data
@AllArgsConstructor
public class MappedStatement {
    private Configuration configuration;
    private String id;
    private StatementType statementType;
    private SqlSource sqlSource;
    private Class<?> parameterTypeClass;
    private Class<?> resultTypeClass;
}
复制代码
public enum StatementType {
    // 几种处理器类型 对应着 Statement 的类型
    STATEMENT,
    PREPARED,
    CALLABLE
}
复制代码

其实很简单。。就是将上面解析玩的东西加入到 MappedStatement 对象中。

所以整个 构建流程 就基本完成了,请你再回顾一下上面的流程图。

带你一步一步手写一个简单的 Mybatis

提炼执行部分核心类

在提炼几个重要的核心类之前,我首先将我们后面所需要写的测试代码贴上来,以便你可以串联两个知识点。

public void execute() throws Exception {
    // 指定全局配置文件的类路径
    String resource = "mybatis-config.xml";
    // 获取输入流
    InputStream inputStream = Resource.getResourceAsStream(resource);
    // 创建 Sql 会话工厂
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 通过工厂创建 Sql 会话
    SqlSession sqlSession = sqlSessionFactory.openSqlSession();
    // 通过 Sql 会话执行指定 id 语句并返回相应对象
    User user = sqlSession.selectOne("findUserById", 1);
    // 打印结果
    System.out.println(user);
}
复制代码

SqlSession

相比上面我们所写的,对于 SqlSession 的部分我们就可以补充了。

public interface SqlSession {
    // 选取一个 最终还是调用的 选取列表操作
    <T> T selectOne(String statementId, Object args);
    // 选取列表
    <T> List<T> select(String statementId, Object args);
}
复制代码

为了最大地简化框架,这里我只 简单定义了两个选取方法满足业务需求 。因为这里需要 SqlSessionSqlSessionFactory 的实现类,所以我直接贴出代码,你可以结合者上面 工厂模式 的 UML 图来理解。

@Data
@AllArgsConstructor
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;
    public SqlSession openSqlSession() {
        return new DefaultSqlSession(configuration);
    }
}
@AllArgsConstructor
public class DefaultSqlSession implements SqlSession{
    private Configuration configuration;
    // 最终还是调用的 select
    public <T> T selectOne(String statementId, Object args) {
        List<T> list = this.select(statementId, args);
        if (list != null && list.size() > 0) {
            return list.get(0);
        } else {
            return null;
        }
    }
    public <T> List<T> select(String statementId, Object args) {
        // 首先在 configuration 对象中获取对应的 MappedStatement
        MappedStatement mappedStatement = this.configuration.getMappedStatement(statementId);
        if (mappedStatement == null) {
            return null;
        }
        // 构造一个执行器
        Executor executor = new SimpleExecutor();
        // 执行获取到的 mappedStatement
        return executor.execute(mappedStatement, configuration, args);
    }
}
复制代码

其实这里就是我在第二篇文章中画的图,只不过这里没有使用到 装饰者模式 来做缓存处理。

带你一步一步手写一个简单的 Mybatis

Executor

public interface Executor {
    // 这里就定义了一个执行方法
    <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object args);
}

public class SimpleExecutor implements Executor  {
    // 查询
    public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object args) {
        List<T> list = new ArrayList<T>();
        // 获取 SqlSource
        SqlSource sqlSource = mappedStatement.getSqlSource();
        // 获取boundSql 这里面很重要
        // 做了对 配置文件中 sql文本的解析
        // 并将参数加入到了 BoundSql 中的 parameterMappingList 中
        BoundSql boundSql = sqlSource.getBoundSql(mappedStatement, configuration, args);
        // 获取 statement 类型
        StatementType statementType = mappedStatement.getStatementType();
        StatementHandler statementHandler = null;
        // 这里面只有默认的 preparedStatement
        if (statementType == StatementType.PREPARED) {
            statementHandler = new PreparedStatementHandler(configuration, args);
            PreparedStatement preparedStatement =
                    (PreparedStatement) statementHandler.getStatement(boundSql.getSql());
            // 这里面很重要 根据上面解析出来的 boundSql 中的 参数列表
            // 然后调用 jdbc 设置参数
            statementHandler.setParameter(preparedStatement, boundSql);
            // 这里调用 jdbc 的执行方法
            ResultSet resultSet = statementHandler.doExecute(preparedStatement);
            // 这里很重要 主要是做类型转换 将resultSet转换为数组
            list = handleResult(resultSet, mappedStatement);
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> handleResult(ResultSet resultSet, MappedStatement mappedStatement) {
       // 这里做类型转换。。。
    }
}
复制代码

在这里逻辑就变得非常复杂了,我先不贴如何做 结果类型转换 的代码,我们先来研究一下 Mybatis 如何将 MappedStatement 中的对象提取出来并且使用 JDBC 来与数据库交互的

写过 JDBC 代码的同学大概都知道 JDBC 的代码长这样

Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/xxxx","xxxx", "xxx");
// 定义 Sql 执行语句
String sql = "select * from xxxtable where xxx = ?";
// 预处理
preparedStatement = connection.prepareStatement(sql);
// 设置第一个参数
preparedStatement.setString(1, "xxx");
// 获取结果集
rs = preparedStatement.executeQuery();
while (resultSet.next()) {
  // 取出结果
}
复制代码

而我们在 Mybatis 中配置的是这样的。

<select id="findUserById" parameterType="java.lang.Integer"
	resultType="test.domain.User">
	SELECT * FROM user WHERE id = #{id} 
</select>
复制代码

在上一篇文章中,我们得出一个结论就是 Mybatis 的执行过程其实主要就是对 JDBC 代码的封装 ,底层是调用的 JDBC 的。所以上面的 Executor 类中的查询方法做的就是这些,我罗列出有主要的三点。

  1. 将我们在配置文件中配置的 sql 动态语句转换为 JDBC 能看懂的语句
    • 比如 SELECT * FROM user WHERE id = #{id}select * from user where id = ? 的转换。
  2. 转换的同时将我们在配置文件中配置的 parameterType 封装到一个有序集合中,然后通过处理器去调用 JDBCsetString , setInt 这类的代码
  3. 通过我们在配置文件中配置的结果类型,调用 JDBC 代码获取 ResultSet 之后通过相应的处理器来将结果集做类型转换

首先我们来实现一下第一个和第二个执行流程。答案在上面的 getBoundSql() 方法中。

@AllArgsConstructor
@Data
public class SqlSource {
    // 在配置文件中原本的 sql 文本
    private String text;
    // 很重要 执行了上面我所说的两个步骤
    public BoundSql getBoundSql(MappedStatement mappedStatement,
                                Object parameterObject) {
        // 通过我们配置的 参数类型 来构建一个 参数映射处理器
        // 里面存储了一个 参数类型 和 一个 ParameterMapping(参数映射)集合
        // 其中参数类型最终都会转入 参数映射 的集合中
        ParameterMappingHandler handler = new ParameterMappingHandler(mappedStatement.getParameterTypeClass());
        // 创建一个GenericTokenParse去解析原本的 sql 文本
        GenericTokenParse genericTokenParse = new GenericTokenParse("#{", "}", handler);
        // 解析的同时会将参数映射列表填充完整
        String sql = genericTokenParse.parse(text);
        // 构建完成
        return new BoundSql(sql, handler.getParameterMappings(), parameterObject);
    }
}

@Data
public class ParameterMappingHandler implements TokenHandler {
    // 持有了 参数映射集合
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    // 持有了参数类型
    private Class<?> parameterTypeClass;
    // 构造
    public ParameterMappingHandler(Class<?> parameterTypeClass) {
        this.parameterTypeClass = parameterTypeClass;
    }
    // 在 GenericTokenParse 中会调用,会将指定文本转换为 ?
    // 比如将 #{id} 转换为 ?
    public String handleToken(String content) {
        // 这里还构建了 parameterMapping 集合
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }
    // parameterTypeClass 最终还是会变成 参数映射集合
    private ParameterMapping buildParameterMapping(String content) {
        return new ParameterMapping(content, parameterTypeClass);
    }
}
复制代码
@AllArgsConstructor
public class GenericTokenParse {
    private String openToken;
    private String closeToken;
    // 这里持有了 TokenHandler
    private TokenHandler handler;
    // 这里就是解析流程 具体逻辑不用管
    // 你只要知道是将 与 openToken 和 closeToken 匹配的字符串转变为
    // 传入参数 text 的
    public String parse(String text) {
        if (text == null || text.length() == 0) {
            return "";
        }
        // search open token
        int start = text.indexOf(openToken, 0);
        if (start == -1) {
            return text;
        }
        char[] src = text.toCharArray();
        int offset = 0;
        final StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;
        while (start > -1) {
            if (start > 0 && src[start - 1] == '//') {
                // this open token is escaped. remove the backslash and continue.
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                // found open token. let's search close token.
                if (expression == null) {
                    expression = new StringBuilder();
                } else {
                    expression.setLength(0);
                }
                builder.append(src, offset, start - offset);
                offset = start + openToken.length();
                int end = text.indexOf(closeToken, offset);
                while (end > -1) {
                    if (end > offset && src[end - 1] == '//') {
                        // this close token is escaped. remove the backslash and continue.
                        expression.append(src, offset, end - offset - 1).append(closeToken);
                        offset = end + closeToken.length();
                        end = text.indexOf(closeToken, offset);
                    } else {
                        expression.append(src, offset, end - offset);
                        offset = end + closeToken.length();
                        break;
                    }
                }
                if (end == -1) {
                    // close token was not found.
                    builder.append(src, start, src.length - start);
                    offset = src.length;
                } else {
                  // 这里调用了 handleToken 方法 这里面做了转换
                  // 回过去看 ParameterMappingHandler 中的方法
                  builder.append(handler.handleToken(expression.toString()));
                    offset = end + closeToken.length();
                }
            }
            start = text.indexOf(openToken, offset);
        }
        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}

复制代码

整个流程如下图

带你一步一步手写一个简单的 Mybatis

StatementHandler

其实你也发现了,第二步到这里并没有完成,这里仅仅是完成了前面一半(将配置的 parameterType 封装到集合中),到现在为止还是没有调用 JDBC 的设置参数代码。

我们再回去看一下 SimpleExecutor 中的查询方法,在获取到 BoundSql 之后有一个构建 StatementHandler 的过程。

这个 StatementHandler 又是何方神圣呢?这是一个非常非常重要的类,可以这么说, 它主管了 Mybatis 调用 JDBC 的大部分流程。比如说获取 Statement 和 参数化等等

这里我们给它定义以下三个方法

public interface StatementHandler {
    // 获取 JDBC 中的 statement
    Statement getStatement(String sql);
    // 调用 JDBC 执行
    ResultSet doExecute(Statement statement);
    // 设置参数
    void setParameter(Statement statement, BoundSql boundSql);
}
复制代码

并且我们实现了一个我们业务中需要的 PreparedStatementHandler ,它主要用于 Mybatis 调用 JDBCPreparedStatement 预处理。

@AllArgsConstructor
// 很多JDBC 封装都在这里进行了
public class PreparedStatementHandler implements StatementHandler {

    private Configuration configuration;
    private Object parameterObject;
    // 这里调用了 JDBC的执行并获取 结果集
    public ResultSet doExecute(Statement statement) {
        ResultSet resultSet = null;
        try {
            resultSet = ((PreparedStatement)statement).executeQuery();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return resultSet;
    }
    // 调用 JDBC 获取 Statement
    public Statement getStatement(String sql) {
        PreparedStatement preparedStatement = null;
        DataSource dataSource = configuration.getDataSource();
        try {
            Connection connection = dataSource.getConnection();
            preparedStatement = connection.prepareStatement(sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return preparedStatement;
    }
    // 设置参数
    public void setParameter(Statement statement, BoundSql boundSql) {
        PreparedStatement preparedStatement = (PreparedStatement) statement;
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 首先获取 bounSql中的 参数映射集合然后遍历
        for (int i = 0; i < parameterMappings.size(); i++) {
            Class<?> parameterTypeClass = parameterMappings.get(i).getParameterTypeClass();
            // 获取参数集合中的参数类型 并通过参数类型去获取
            // 对应的类型处理器 调用相应的参数设置方法
            TypeHandler typeHandler = getTypeHandler(parameterTypeClass);
            if (typeHandler != null) {
                typeHandler.setParameter(i + 1, preparedStatement, parameterObject);
            }
        }
    }
    // 这里仅仅简单写了个 Integer 参数类型处理器
    private TypeHandler getTypeHandler(Class<?> parameterTypeClass) {
        if (parameterTypeClass.isAssignableFrom(Integer.class)) {
            return new IntegerTypeHandler();
        } else {
            System.out.println("暂不支持该类型");
            return null;
        }
    }
}
复制代码

TypeHandler

这里是 TypeHandlerIntegerTypeHandler 的实现

public interface TypeHandler {
    // 设置参数
    void setParameter(int index, PreparedStatement preparedStatement, Object parameterObject);
}

public class IntegerTypeHandler implements TypeHandler {
    public void setParameter(int index, PreparedStatement preparedStatement, Object parameterObject) {
        try {
            // 调用setInt
            preparedStatement.setInt(index, (Integer) parameterObject);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
复制代码
带你一步一步手写一个简单的 Mybatis

写到这里我们就已经将构建部分的核心代码写完一大半了,接下来就是 MyabtisJDBC 结果集转换的封装了。我们回过去看 SimpleExecutor 可以发现我们需要实现我们再查询方法中写的

list = handleResult(resultSet, mappedStatement);
复制代码

这里我讲处理结果的代码补充完整。

public class SimpleExecutor implements Executor  {

    // 查询
    public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object args) {
        List<T> list = new ArrayList<T>();
        // 获取 SqlSource
        SqlSource sqlSource = mappedStatement.getSqlSource();
        // 获取boundSql 这里面很重要
        // 做了对 配置文件中 sql文本的解析
        // 并将参数加入到了 BoundSql 中的 parameterMappingList 中
        BoundSql boundSql = sqlSource.getBoundSql(mappedStatement, configuration, args);
        // 获取 statement 类型
        StatementType statementType = mappedStatement.getStatementType();
        StatementHandler statementHandler = null;
        // 这里面只有默认的 preparedStatement
        if (statementType == StatementType.PREPARED) {
            statementHandler = new PreparedStatementHandler(configuration, args);
            PreparedStatement preparedStatement =
                    (PreparedStatement) statementHandler.getStatement(boundSql.getSql());
            // 这里面很重要 根据上面解析出来的 boundSql 中的 参数列表
            // 然后调用 jdbc 设置参数
            statementHandler.setParameter(preparedStatement, boundSql);
            // 这里调用 jdbc 的执行方法
            ResultSet resultSet = statementHandler.doExecute(preparedStatement);
            // 这里很重要 主要是做类型转换 将resultSet转换为数组
            list = handleResult(resultSet, mappedStatement);
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> handleResult(ResultSet resultSet, MappedStatement mappedStatement) {
        List<T> results = new ArrayList<T>();
        // 我们获取到 MappedStatement 中的 ResultType
        Class<?> resultTypeClass = mappedStatement.getResultTypeClass();
        try {
            while (resultSet.next()) {
                // 遍历结果集 并通过反射去创建对象。
                Object resultObject = resultTypeClass.newInstance();
                ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
                int columnCount = resultSetMetaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    Field field = resultTypeClass.getDeclaredField(resultSetMetaData.getColumnLabel(i));
                    field.setAccessible(true);
                    field.set(resultObject, resultSet.getObject(i));
                }
                results.add((T)resultObject);
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return results;
    }
}
复制代码

到这里我们所有的逻辑就写完了,当然在 Mybatis 中的处理比这个要复杂的多得多,如果对源码感兴趣的同学可以自己 github 上下载源码调试。

测试运行结果

我们来测试一下我的测试代码

public void execute() throws Exception {
    // 指定全局配置文件的类路径
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resource.getResourceAsStream(resource);

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSqlSession();

    User user = sqlSession.selectOne("findUserById", 1);

    System.out.println(user);
}
复制代码

执行结果

带你一步一步手写一个简单的 Mybatis

到这里,我们就完成了一个简单 Mybatis 框架。

所有代码已经在 github 上托管,感兴趣的同学可以自行 fork 。

原文  https://juejin.im/post/5dc21a36e51d455c03094abc
正文到此结束
Loading...