在前两篇文章中我向你介绍了 Mybatis 的构建和执行流程,这篇文章中我会带领你一步一步手写一个简单的 Mybatis 框架。
本文主要涉及代码实现,很多要点会在代码注释中说明,请仔细阅读。
所有代码已经在github上托管,感兴趣的同学可以自行 fork 。看完记得点赞哦(#^.^#)
仿写框架的文章,我会尽量将源代码贴出来(很多思路已经写在了源码注释中),如果。。。。
如果还没看过我前两篇文章的请戳这里
提炼构建部分核心类
既然是仿写一个简单的,那么我们就不可能面面俱到,和分析源码一样,我们需要一步一步跟着主线走。所以我们首先要提炼出整个框架构建流程所涉及到的核心类,然后再去仿写。
在第一篇的构建文章中我画了一张简单的流程图,这里我直接拿来用,以便提炼我们的核心类。
在第一篇文章中我已经通过源码向大家解释了这张图的由来,这里我不再赘述。直接动手开干吧!
构建部分仿写
SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder
在第二篇文章中我画了关于 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会话工厂就创建成功了。
此时我们肯定有一些疑问
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>
在上面我使用到了
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;
}
}
MappedStatement
在上面我们已经将 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
相比上面我们所写的,对于 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);
}
}
其实这里就是我在第二篇文章中画的图,只不过这里没有使用到 装饰者模式 来做缓存处理。
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
封装到一个有序集合中,然后通过处理器去调用 JDBC
的 setString
,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
调用 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
这里是 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 。