从零开始手写 mybatis(一)MVP 版本
什么是 MyBatis ?
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。(这是官网解释)
MyBatis 运行原理

当框架启动时,通过configuration解析config.xml配置文件和mapper.xml映射文件,映射文件可以使用xml方式或者注解方式,然后由configuration获得sqlsessionfactory对象,再由sqlsessionfactory获得sqlsession数据库访问会话对象,通过会话对象获得对应DAO层的mapper对象,通过调用mapper对象相应方法,框架就会自动执行SQL语句从而获得结果。
手写 mybatis
其实整体流程就是这么简单,我们来一起实现一个简单版本的 mybatis。
创作目的
(1)深入学习 mybatis 的原理
一千个读者就有一千个哈姆雷特,一千个作者就有一千个莎士比亚。——老马
(2)实现属于自己的 mybatis 工具。
数据库的种类实际上有几百种,比如工作中就用到过 GreenPlum 这种相对小众的数据库,这时候 mybatis 可能就不能使用了。
感觉大可不必,符合 SQL 标准都应该统一支持下,这样更加方便实用。
实现方式
本系列目前共计 17 个迭代版本,基本完成了 mybatis 的核心特性。
耗时大概十天左右,相对实现的方式比较简单。
采用 mvp 的开发策略,逐渐添加新的特性。
本系列将对核心代码进行讲解,完整代码已经全部开源
快速体验
mysql 安装
不是本系列重点,请自行找资料。
版本:使用的是 v5.7 版本,v8.0 之后依赖的驱动包会有所不同。
sql 执行
-- auto-generated definition
create table user
(
id int auto_increment
primary key,
name varchar(100) not null,
password varchar(100) not null
);
insert into user (name, password) value ('ryo', '123456');
maven 引入
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>mybatis</artifactId>
<version>0.0.1</version>
</dependency>
配置文件
- mybatis-config-5-7.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<dataSource>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
测试代码
Config config = new XmlConfig("mybatis-config-5-7.xml");
SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);
System.out.println(user);
输出结果:
User{id=1, name='ryo', password='123456'}
是不是有种 mybatis 初恋般的感觉呢?
到这里都是引子,下面我们来讲述下一些核心实现。
代码实现
maven 依赖
这里我们需要访问 mysql,也需要解析 xml。
需要引入如下的依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
接口定义
上述的测试代码中,我们演示用到的几个核心接口如下:
- Config.java
配置接口
/**
* 配置信息
* @author binbin.hou
* @since 0.0.1
*/
public interface Config {
/**
* 获取数据源信息
* @return 数据源配置
* @since 0.0.1
*/
DataSource getDataSource();
/**
* 获取映射类信息
* @param clazz 类信息
* @return 结果
* @since 0.0.1
*/
MapperClass getMapperData(final Class clazz);
/**
* 获取映射类信息
* @param clazz 类信息
* @param methodName 方法名称
* @return 结果
* @since 0.0.1
*/
MapperMethod getMapperMethod(final Class clazz,
final String methodName);
/**
* 数据库连接信息
* @return 连接信息
* @since 0.0.1
*/
Connection getConnection();
}
- SqlSession.java
public interface SqlSession {
/**
* 查询单个
* @param mapperMethod 方法
* @param args 参数
* @param <T> 泛型
* @return 结果
* @since 0.0.1
*/
<T> T selectOne(final MapperMethod mapperMethod, Object[] args);
/**
* Retrieves a mapper.
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
* @since 0.0.1
*/
<T> T getMapper(Class<T> type);
/**
* 获取配置信息
* @return 配置
* @since 0.0.1
*/
Config getConfig();
}
- UserMapper.java
UserMapper 就是我们经常定义的 mapper
public interface UserMapper {
User selectById(final long id);
}
下面我们来看看对应的几个比较重要的实现。
xml 的配置初始化
我们的很多配置放在 config.xml 文件中,肯定是通过解析 xml 实现的。
基础属性
public class XmlConfig extends ConfigAdaptor {
/**
* 文件配置路径
*
* @since 0.0.1
*/
private final String configPath;
/**
* 配置文件信息
*
* @since 0.0.1
*/
private Element root;
/**
* 数据源信息
*
* @since 0.0.1
*/
private DataSource dataSource;
/**
* mapper 注册类
*
* @since 0.0.1
*/
private final MapperRegister mapperRegister = new MapperRegister();
public XmlConfig(String configPath) {
this.configPath = configPath;
// 配置初始化
initProperties();
// 初始化数据连接信息
initDataSource();
// mapper 信息
initMapper();
}
@Override
public DataSource getDataSource() {
return this.dataSource;
}
@Override
public Connection getConnection() {
try {
Class.forName(dataSource.driver());
return DriverManager.getConnection(dataSource.url(), dataSource.username(), dataSource.password());
} catch (ClassNotFoundException | SQLException e) {
throw new MybatisException(e);
}
}
@Override
public MapperMethod getMapperMethod(Class clazz, String methodName) {
return this.mapperRegister.getMapperMethod(clazz, methodName);
}
}
配置初始化
这里就是解析 xml 文件的 root 节点,便于后续使用:
root 节点的初始化如下:
/**
* 获取根节点
* @param path 配置路径
* @return 元素
* @since 0.0.1
*/
public static Element getRoot(final String path) {
try {
// 初始化数据库连接信息
InputStream inputStream = StreamUtil.getInputStream(path);
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
return document.getRootElement();
} catch (DocumentException e) {
throw new MybatisException(e);
}
}
初始化数据连接信息
这就是解析 xml 中对于 dataSource 的配置信息:
/**
* 初始化数据源
*
* @since 0.0.1
*/
private void initDataSource() {
// 根据配置初始化连接信息
this.dataSource = new DataSource();
Element dsElem = root.element("dataSource");
Map<String, String> map = new HashMap<>(4);
for (Object property : dsElem.elements("property")) {
Element element = (Element) property;
String name = element.attributeValue("name");
String value = element.attributeValue("value");
map.put("jdbc." + name, value);
}
dataSource.username(map.get(DataSourceConst.USERNAME))
.password(map.get(DataSourceConst.PASSWORD))
.driver(map.get(DataSourceConst.DRIVER))
.url(map.get(DataSourceConst.URL));
}
初始化 mapper
解析 xml 中的 mapper 配置。
/**
* 初始化 mapper 信息
*
* @since 0.0.1
*/
private void initMapper() {
Element mappers = root.element("mappers");
// 遍历所有需要初始化的 mapper 文件路径
for (Object item : mappers.elements("mapper")) {
Element mapper = (Element) item;
String path = mapper.attributeValue("resource");
mapperRegister.addMapper(path);
}
}
mapperRegister 就是对方法的元数据进行一些构建,比如出参,入参的类型,等等,便于后期使用。
比如我们的 UserMapper.xml 方法内容如下:
<select id = "selectById" paramType="java.lang.Long" resultType = "com.github.houbb.mybatis.domain.User">
select * from user where id = ?
</select>
sql 就是:select * from user where id = ?
方法标识:selectById
入参:Long
出参:User
创建 session
如何创建
SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
这句话实际执行的是:
@Override
public SqlSession openSession() {
return new DefaultSqlSession(config, new SimpleExecutor());
}
获取 mapper 实现
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
这里获取 mapper,实际获取的是什么呢?
实际上获取到的是一个代理。
mybatis 将我们的接口,和实际 xml 中的 sql 二者通过动态代理结合,让我们调用 xml 中的 sql 和使用接口方法一样自然。
获取代理
getMapper 实际上是一个动态代理。
@Override
@SuppressWarnings("all")
public <T> T getMapper(Class<T> clazz) {
MapperProxy proxy = new MapperProxy(clazz, this);
return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, proxy);
}
动态代理的实现
MapperProxy 的实现如下:
public class MapperProxy implements InvocationHandler {
/**
* 类信息
*
* @since 0.0.1
*/
private final Class clazz;
/**
* sql session
*
* @since 0.0.1
*/
private final SqlSession sqlSession;
public MapperProxy(Class clazz, SqlSession sqlSession) {
this.clazz = clazz;
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MapperMethod mapperMethod = this.sqlSession.getConfig()
.getMapperMethod(clazz, method.getName());
if (mapperMethod != null) {
return this.sqlSession.selectOne(mapperMethod, args);
}
return method.invoke(proxy, args);
}
}
代理了什么?
当我们执行 userMapper.selectById(1L) 时,实际执行的是什么?
实际执行的是 sqlSession.selectOne(mapperMethod, args)
代理实现
selectOne 是比较核心的内容了。
整体实现
整体如下
public <T> T query(final Config config,
MapperMethod method, Object[] args) {
try(Connection connection = config.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(method.getSql());) {
// 2. 处理参数
parameterHandle(preparedStatement, args);
// 3. 执行方法
preparedStatement.execute();
// 4. 处理结果
final Class resultType = method.getResultType();
ResultSet resultSet = preparedStatement.getResultSet();
ResultHandler resultHandler = new ResultHandler(resultType);
Object result = resultHandler.buildResult(resultSet);
return (T) result;
} catch (SQLException ex) {
throw new MybatisException(ex);
}
}
我们获取到 xml 中的 sql,然后构建 jdbc 中大家比较熟悉的 PreparedStatement。
然后对出参和入参进行处理,最后返回结果。
入参设置
public void setParams(final Object[] objects) {
try {
for(int i = 0; i < objects.length; i++) {
Object value = objects[i];
// 目标类型,这个后期可以根据 jdbcType 获取
// jdbc 下标从1开始
statement.setObject(i+1, value);
}
} catch (SQLException throwables) {
throw new MybatisException(throwables);
}
}
针对我们非常简单的例子:
select * from user where id = ?
那就是直接把入参中的 1L 设置到占位符 ? 即可。
出参处理
这里主要用到反射,将查询结果和 javaBean 做一一映射。
/**
* 构建结果
* @param resultSet 结果集合
* @return 结果
* @since 0.0.1
*/
public Object buildResult(final ResultSet resultSet) {
try {
// 基本类型,非 java 对象,直接返回即可。
// 可以进行抽象
Object instance = resultType.newInstance();
// 结果大小的判断
// 为空直接返回,大于1则报错
if(resultSet.next()) {
List<Field> fieldList = ClassUtil.getAllFieldList(resultType);
for(Field field : fieldList) {
Object value = getResult(field, resultSet);
ReflectFieldUtil.setValue(field, instance, value);
}
// 返回设置值后的结果
return instance;
}
return null;
} catch (InstantiationException | IllegalAccessException | SQLException e) {
throw new MybatisException(e);
}
}
到这里,一个简易版的 myabtis 就可以跑起来了。
当然这里还有很多的不足之处,我们后续都会一一优化。
完整代码地址
为了便于学习,完整版本代码以开源:
从零开始手写 mybatis(一)MVP 版本的更多相关文章
- 手写MyBatis ORM框架实践
一.实现手写Mybatis三个难点 1.接口既然不能被实例化?那么我们是怎么实现能够调用的? 2.参数如何和sql绑定 3.返回结果 下面是Mybatis接口 二.Demo实现 1.创建Maven工程 ...
- java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端
通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...
- 要想精通Mybatis?从手写Mybatis框架开始吧!
1.Mybatis组成 动态SQL Config配置 Mapper配置 2.核心源码分析 Configuration源码解析 SqlSessionFactory源码解析 SqlSession源码解析 ...
- 手写mybatis框架笔记
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 手写MyBatis流程
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- java 从零开始手写 RPC (03) 如何实现客户端调用服务端?
说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...
- java 从零开始手写 RPC (04) -序列化
序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...
- java 从零开始手写 RPC (07)-timeout 超时处理
<过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...
- 手写mybatis框架-增加缓存&事务功能
前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...
- 带码农《手写Mybatis》进度3:实现映射器的注册和使用
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获!
随机推荐
- ABP微服务系列学习-搭建自己的微服务结构(二)
在解决方案根目录添加common.props,这个文件的作用是可以配置项目文件全局的一些属性,如忽略警告,全局PackageReference,语言版本等. <Project> <P ...
- 【TouchGFX 】使用 CubeMX 创建 TouchGFX 工程时 LCD 显示为雪花屏
经几个晚上折腾,修改大量的LTDC时钟.时序,FMC时序等,结果还是一样,耐心与好使的工程仔细对比,发现是时钟源配置问题,真是冤,聊以此以示纪念 实质上是没有分清有源和无源晶振 无源晶振又被叫做 谐振 ...
- Android Studio 的 Gradle 面板没有 Task
问题描述:Android Studio Gradle 窗口没有显示 task 列表的问题,如下图所示: 网上找了好久都没有找到原因,最后自己摸索,找了解决方法. 解决方法:依次点击:File -> ...
- [转帖]Unicode与utf的前世今生
https://www.cnblogs.com/naodong/p/12742987.html 历史上存在两个独立的尝试创立单一字符集的组织,即 国际标准化组织(ISO)于1984年创建的通用字符集( ...
- [转帖]SIMD指令集 SSE/AVX
SIMD指令集 SSE/AVX 概述 参考手册 Intel Intrinsics Guide Tommesani.com Docs Intel 64 and IA-32 Architectures S ...
- [转帖]一次SSL握手异常,我发现JDK还有发行版区别
https://www.cnblogs.com/codelogs/p/16633704.html 原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 最近,我们一个 ...
- [转帖]/proc文件简介
https://www.jianshu.com/p/2610241770be 简介 /proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间.它以文件系统的方式为访问系统内核数据的操作提 ...
- [转帖]exportfs命令
https://www.cnblogs.com/xzlive/p/9766388.html exportfs命令:功能说明 :NFS共享管理语法格式exportfs [必要参数][选择参数][目录]功 ...
- 学习: Linux的 date 命令
date 命令非常好用 多用 date --h 还是非常好的 获取 今天是今年的第多少天 最简单的办法 就是 date +%j 以后需要多学习 多利用 linux的帮助才可以呢. Usage: dat ...
- 最小的 $x$ 满足 $L\le x\bmod P\le R$
设 \(G(L, R, D, P)\) 为 \(y P+L \leq x D \leq y P+R\) ,满足 \(1 \leq L \leq R<P, D<P\) ,其中 \(x\) 的 ...