在前两篇文章中我向你介绍了 Mybatis 的构建和执行流程,这篇文章中我会带领你一步一步手写一个简单的 Mybatis 框架。
本文主要涉及代码实现,很多要点会在代码注释中说明,请仔细阅读。
所有代码已经在 github 上托管,感兴趣的同学可以自行 fork 。看完记得点赞哦(#^.^#)
仿写框架的文章,我会尽量将源代码贴出来(很多思路已经写在了源码注释中),如果。。。。
如果还没看过我前两篇文章的请戳这里
既然是仿写一个简单的,那么我们就不可能面面俱到,和分析源码一样,我们需要一步一步跟着 主线 走。所以我们首先要提炼出整个框架构建流程所涉及到的核心类,然后再去仿写。
在第一篇的构建文章中我画了一张简单的流程图,这里我直接拿来用,以便提炼我们的核心类。
在第一篇文章中我已经通过源码向大家解释了这张图的由来,这里我不再赘述。直接动手开干吧!
在第二篇文章中我画了关于 SqlSessionFactory
的 工厂模式UML 图
我们可以通过这张图构建一个简单的 SqlSession
和 SqlSessionFactory
。
public interface SqlSession { // 目前什么都没有 不用管 具体内容应该在执行部分 } 复制代码
public interface SqlSessionFactory { // 创建 SqlSession SqlSession openSqlSession(); } 复制代码
你肯定不禁感叹,这 tnd 也太简单了。
其实就是这么简单。
当然,有了 SqlSession
和 SqlSessionFactory
之后还需要使用一个 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会话工厂就创建成功了。
此时我们肯定有一些疑问
InputStream
怎么来的? 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
(后面会补充)
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> 复制代码
在上面我使用到了 dom4j
和 Druid
数据源 以及 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; } } 复制代码
在上面我们已经将 XmlConfigBuilder
和 XmlMapperBuilder
打通了,也就是 XmlMapperBuilder
是 XmlConfigBuilder
中的很重要的一个子构建过程 。所以 XmlMapperBuilder
其实也是 Configuartion
对象的构建者,而在其中它主要构建了 MappedStatement
对象。这样 Configuration
和 XmlMapperBuilder
就也打通了。
所以接下来的重头戏就是 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
的部分我们就可以补充了。
public interface SqlSession { // 选取一个 最终还是调用的 选取列表操作 <T> T selectOne(String statementId, Object args); // 选取列表 <T> List<T> select(String statementId, Object args); } 复制代码
为了最大地简化框架,这里我只 简单定义了两个选取方法满足业务需求 。因为这里需要 SqlSession
和 SqlSessionFactory
的实现类,所以我直接贴出代码,你可以结合者上面 工厂模式 的 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); } } 复制代码
其实这里就是我在第二篇文章中画的图,只不过这里没有使用到 装饰者模式 来做缓存处理。
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
类中的查询方法做的就是这些,我罗列出有主要的三点。
sql
动态语句转换为 JDBC
能看懂的语句 。
SELECT * FROM user WHERE id = #{id}
到 select * from user where id = ?
的转换。 parameterType
封装到一个有序集合中,然后通过处理器去调用 JDBC
的 setString
, setInt
这类的代码 。 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(); } } 复制代码
整个流程如下图
其实你也发现了,第二步到这里并没有完成,这里仅仅是完成了前面一半(将配置的 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
调用 JDBC
的 PreparedStatement
预处理。
@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
和 IntegerTypeHandler
的实现
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(); } } } 复制代码
写到这里我们就已经将构建部分的核心代码写完一大半了,接下来就是 Myabtis
对 JDBC
结果集转换的封装了。我们回过去看 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 。