阅读 345

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

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

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

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

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

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

提炼构建部分核心类

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

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

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

构建部分仿写

SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder

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

我们可以通过这张图构建一个简单的 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> 标签的解析。其实就是我们上面流程图的右边部分。

// 上面解析的代码
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 对象中。

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

提炼执行部分核心类

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

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);
    }
}
复制代码

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

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

复制代码

整个流程如下图

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();
        }
    }
}
复制代码

写到这里我们就已经将构建部分的核心代码写完一大半了,接下来就是 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 框架。

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

关注下面的标签,发现更多相似文章
评论