从零开始手写 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 沉淀.分享.成长,让自己和他人都能有所收获!
随机推荐
- Scan Synthesis Review
Review scan replacement - 将normal DFF替换为mux gate DFF scan stitching - 将DFF连接起来 scan的作用:将测试困难的时序逻辑转变为 ...
- 使用QQ屏幕识图实现识别表格功能
1.问题 目前市场上的OCR工具对于识别表格功能均是采取了收费制度,但我们时常要进行一些表格的复制(原表格为图片) 便可以使用QQ或钉钉自带的功能来实现 2.解决 1.QQ屏幕识图 先使用屏幕识图功能 ...
- ONVIF网络摄像头(IPC)客户端开发—RTSP RTCP RTP加载H264视频流
前言: RTSP,RTCP,RTP一般是一起使用,在FFmpeg和live555这些库中,它们为了更好的适用性,所以实现起来非常复杂,直接查看FFmpeg和Live555源代码来熟悉这些协议非常吃力, ...
- [转帖]prometheus和node_exporter中的磁盘监控
https://www.ipcpu.com/2021/04/prometheus-node_exporter/ prometheus和node_exporter中的磁盘监控.md 对于磁盘问题,我们主 ...
- [转帖]事务上的等待事件 —— enq: TM - contention
执行DML期间,为防止对与DML相关的对象进行修改,执行DML的进程必须对该表获得TM锁.若在获得TM锁的过程中发生争用,则等待enq: HW - contention 事件. SQL> sel ...
- [转帖]003、体系结构之TiKV持久化
TiKV架构和作用 数据持久化 分布式一致性 MVCC 分布式事务 Coprocessor coprocessor : 协同处理器. 可以将一些SQL计算交给TiKV处理.不需要将TiKV所有数据通过 ...
- [转帖]TIDB - 使用BR工具进行数据热备份与恢复
一.BR工具 BR 全称为 Backup & Restore,是 TiDB 分布式备份恢复的命令行工具,用于对 TiDB 集群进行数据备份和恢复.BR 只支持在 TiDB v3.1 及以上版本 ...
- [转帖]LSM树详解
https://zhuanlan.zhihu.com/p/181498475 LSM树(Log-Structured-Merge-Tree)的名字往往会给初识者一个错误的印象,事实上,LSM树并不像B ...
- 【转帖】Mysql一张表可以存储多少数据
https://www.cnblogs.com/wenbochang/p/16723537.html Mysql一张表可以存储多少数据 在操作系统中,我们知道为了跟磁盘交互,内存也是分页的,一页大小4 ...
- [转帖]Nginx性能优化详解
https://developer.aliyun.com/article/886146?spm=a2c6h.24874632.expert-profile.256.7c46cfe9h5DxWK 感觉文 ...