『手写Mybatis』实现映射器的注册和使用
前言
如何面对复杂系统的设计?
我们可以把 Spring、MyBatis、Dubbo 这样的大型框架或者一些公司内部的较核心的项目,都可以称为复杂的系统。
这样的工程也不在是初学编程手里的玩具项目,没有所谓的 CRUD,更多时候要面对的都是对系统分层的结构设计和聚合逻辑功能的实现,再通过层层转换进行实现和调用。
这对于很多刚上道的小码农来说,会感觉非常难受,不知道要从哪下手,但又想着可以一口吃个胖子。
其实这是不现实的,因为这些复杂系统中的框架中有太多的内容你还没用和了解和熟悉,越是硬搞越难受,信心越受打击。
其实对于解决这类复杂的项目问题,核心在于要将分支问题点缩小,突出主干链路,具体的手段包括:分治、抽象和知识。
运用设计模式和设计原则等相关知识,把问题空间合理切割为若干子问题,问题越小也就越容易理解和处理。
就像你可以把很多内容做成单个独立的案例一样,最终在进行聚合使用。
目标
在上一章节我们初步的了解了怎么给一个接口类生成对应的映射器代理,并在代理中完成一些用户对接口方法的调用处理。
虽然我们已经看到了一个核心逻辑的处理方式,但在使用上还是有些刀耕火种的,包括:需要编码告知 MapperProxyFactory 要对哪个接口进行代理,以及自己编写一个假的 SqlSession 处理实际调用接口时的返回结果。
那么结合这两块问题点,我们本章节要对映射器的注册提供注册机处理,满足用户可以在使用的时候提供一个包的路径即可完成扫描和注册。
与此同时需要对 SqlSession 进行规范化处理,让它可以把我们的映射器代理和方法调用进行包装,建立一个生命周期模型结构,便于后续的内容的添加。
设计
鉴于我们希望把整个工程包下关于数据库操作的 DAO 接口与 Mapper 映射器关联起来,那么就需要包装一个可以扫描包路径的完成映射的注册器类。
当然我们还要把上一章节中简化的 SqlSession 进行完善,由 SqlSession 定义数据库处理接口和获取 Mapper 对象的操作,并把它交给映射器代理类进行使用。这一部分是对上一章节内容的完善。
有了 SqlSession 以后,你可以把它理解成一种功能服务,有了功能服务以后还需要给这个功能服务提供一个工厂,来对外统一提供这类服务。比如我们在 MyBatis 中非常常见的操作,开启一个 SqlSession。整个设计图如下:

- 以包装接口提供映射器代理类为目标,补全映射器注册机 MapperRegistry,自动扫描包下接口并把每个接口类映射的代理类全部存入映射器代理的 HashMap 缓存中。
- 而 SqlSession、SqlSessionFactory 是在此注册映射器代理的上层使用标准定义和对外服务提供的封装,便于用户使用。我们把使用方当成用户 经过这样的封装就可以以更加方便的方式供我们后续在框架上继续扩展功能了,也希望大家可以在学习的过程中对这样的设计结构有一些思考,它可以帮助你解决一些业务功能开发过程中的领域服务包装。
实现
工程结构
step-02
├───src
│ ├───main
│ │ ├───java
│ │ │ └───top
│ │ │ └───it6666
│ │ │ └───mybatis
│ │ │ ├───binding
│ │ │ └───session
│ │ │ └───defaults
│ │ └───resources
│ └───test
│ └───java
│ └───top
│ └───it6666
│ └───test
│ └───dao
工程源码:https://github.com/BNTang/Java-All/tree/main/mybatis-source-code/step-02
映射器标准定义实现关系,如下图:

- MapperRegistry:提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。
- SqlSession、DefaultSqlSession:用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用 Mybatis 的 API 接口也都是从这个接口类定义的方法进行使用的。
- SqlSessionFactory:是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。
SqlSession 标准定义和实现
在 top.it6666.mybatis.session:编写 SqlSession 接口,代码如下:
/**
* @author BNTang
* @version 1.0
* @description SqlSession 标准定义和实现
* @since 2024/6/16 星期日
**/
public interface SqlSession {
/**
* Retrieve a single row mapped from the statement key
* 根据指定的SqlID获取一条记录的封装对象
*
* @param <T> the returned object type 封装之后的对象类型
* @param statement sqlID
* @return Mapped object 封装之后的对象
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
*
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
/**
* Retrieves a mapper.
* 得到映射器,这个巧妙的使用了泛型,使得类型安全
*
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
}
- 在 SqlSession 中定义用来执行 SQL、获取映射器对象以及后续管理事务操作的标准接口。
- 目前这个接口中对于数据库的操作仅仅只提供了 selectOne,后续还会有相应其他方法的定义。
在 top.it6666.mybatis.session.defaults:编写 DefaultSqlSession 实现类,代码如下:
/**
* @author BNTang
* @version 1.0
* @description SqlSession 的默认实现
* @since 2024/6/16 星期日
**/
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;
public DefaultSqlSession(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
/**
* Retrieve a single row mapped from the statement key
* 根据指定的SqlID获取一条记录的封装对象
*
* @param statement sqlID
* @return Mapped object 封装之后的对象
*/
@Override
public <T> T selectOne(String statement) {
return (T) ("你的操作被代理了!" + statement);
}
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
*
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你的操作被代理了!" + "方法:" + statement + " 入参:" + parameter);
}
/**
* Retrieves a mapper.
* 得到映射器,这个巧妙的使用了泛型,使得类型安全
*
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
- 通过 DefaultSqlSession 实现类对 SqlSession 接口进行实现。
- getMapper 方法中获取映射器对象是通过 MapperRegistry 类进行获取的,后续这部分会被配置类进行替换。
- 在 selectOne 中是一段简单的内容返回,目前还没有与数据库进行关联,这部分在我们渐进式的开发过程中逐步实现。
SqlSessionFactory 工厂定义和实现
在 top.it6666.mybatis.session:编写 SqlSessionFactory 接口,代码如下:
/**
* @author BNTang
* @version 1.0
* @description SqlSessionFactory 工厂定义和实现
* @since 2024/6/16 星期日
**/
public interface SqlSessionFactory {
/**
* 打开一个 session
*
* @return SqlSession
*/
SqlSession openSession();
}
- 这其实就是一个简单工厂的定义,在工厂中提供接口实现类的能力,也就是 SqlSessionFactory 工厂中提供的开启 SqlSession 的能力。
在 top.it6666.mybatis.session.defaults:编写 DefaultSqlSessionFactory 实现类,代码如下:
/**
* @author BNTang
* @version 1.0
* @description SqlSessionFactory 工厂定义和实现
* @since 2024/6/16 星期日
**/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
/**
* 打开一个 session
*
* @return SqlSession
*/
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
- 默认的简单工厂实现,处理开启 SqlSession 时,对 DefaultSqlSession 的创建以及传递 mapperRegistry,这样就可以在使用 SqlSession 时获取每个代理类的映射器对象了。
映射器注册机
在这段代码的实现中,需要用到包扫描的功能,所以我引入了 Hutool 工具包,用于扫描包路径下的所有类,先添加 pom 依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.0</version>
</dependency>
在进行实现之前需要将之前的 MapperProxyFactory newInstance 方法接收参数进行改改之前是 Map<String, String> sqlSession 现在改为 SqlSession sqlSession,这样就 MapperProxy 中的 SqlSession 也需要进行更改在 MapperProxy 中就可以通过本次传递的 SqlSession 对象进行操作了。
然后在 top.it6666.mybatis.binding:MapperRegistry 类中实现包扫描功能,代码如下:
/**
* @author BNTang
* @version 1.0
* @description 映射器注册机
* @since 2024/6/16 星期日
**/
public class MapperRegistry {
/**
* 将已添加的映射器代理加入到 HashMap
*/
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
}
}
public <T> void addMapper(Class<T> type) {
// Mapper 必须是接口才会注册
if (type.isInterface()) {
if (hasMapper(type)) {
// 如果重复添加了,报错
throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
}
// 注册映射器代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
- MapperRegistry:映射器注册类的核心主要在于提供了 ClassScanner.scanPackage 扫描包路径,调用 addMapper 方法,给接口类创建 MapperProxyFactory 映射器代理类,并写入到 knownMappers 的 HashMap 缓存中。
- 另外就是这个类也提供了对应的 getMapper 获取映射器代理类的方法,其实这步就包装了我们上一章节手动操作实例化的过程,更加方便在 DefaultSqlSession 中获取 Mapper 时进行使用。
测试
在同一个包路径下,提供2个以上的 Dao 接口:

/**
* @author BNTang
* @version 1.0
* @description 学校接口
* @since 2024/6/16 星期日
**/
public interface ISchoolDao {
String querySchoolName(String uId);
String querySchoolName();
}
/**
* 用户接口
*/
public interface IUserDao {
String queryUserName(String uId);
Integer queryUserAge(String uId);
}
单元测试
/**
* @author BNTang
* @version 1.0
* @description 测试类
* @since 2024/4/16 星期二
**/
public class ApiTest {
private final Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_MapperProxyFactory() {
// 1. 注册 Mapper
MapperRegistry registry = new MapperRegistry();
registry.addMappers("top.it6666.test.dao");
// 2. 从 SqlSession 工厂获取 Session
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(registry);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3. 获取映射器对象
ISchoolDao iSchoolDao = sqlSession.getMapper(ISchoolDao.class);
// 4. 测试验证
String res = iSchoolDao.querySchoolName("neo");
logger.info("测试结果:{}", res);
}
}
- 在单元测试中通过注册机扫描包路径注册映射器代理对象,并把注册机传递给 SqlSessionFactory 工厂,这样完成一个链接过程。
- 之后通过 SqlSession 获取对应 DAO 类型的实现类,并进行方法验证。
测试结果
19:48:09.984 [main] INFO top.it6666.test.ApiTest - 测试结果:你的操作被代理了!方法:querySchoolName 入参:[Ljava.lang.Object;@5fe5c6f
- 通过测试大家可以看到,目前我们已经在一个有 MyBatis 影子的手写 ORM 框架中,完成了代理类的注册和使用过程。
总结
- 首先要从设计结构上了解工厂模式对具体功能结构的封装,屏蔽过程细节,限定上下文关系,把对外的使用减少耦合。
- 从这个过程上读者伙伴也能发现,使用 SqlSessionFactory 的工厂实现类包装了 SqlSession 的标准定义实现类,并由 SqlSession 完成对映射器对象的注册和使用。
- 本章学习要注意几个重要的知识点,包括:映射器、代理类、注册机、接口标准、工厂模式、上下文。
- 这些工程开发的技巧都是在手写 MyBatis 的过程中非常重要的部分,了解和熟悉才能更好的在自己的业务中进行使用。
结束语
着急和快,是最大的障碍!慢下来,慢下来,只有慢下来,你才能看到更全的信息,才能学到更扎实的技术。而那些满足你快的短篇内容虽然有时候更抓眼球,但也容易把人在技术学习上带偏,总想着越快越好。
如果您觉得文章对您有所帮助,欢迎您点赞、评论、转发,也欢迎您关注我的公众号『BNTang』,我会在公众号中分享更多的技术文章。
『手写Mybatis』实现映射器的注册和使用的更多相关文章
- 带码农《手写Mybatis》进度3:实现映射器的注册和使用
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获!
- MyBatis实战之映射器
映射器是MyBatis最强大的工具,也是我们使用MyBatis时用得最多的工具,因此熟练掌握它十分必要.MyBatis是针对映射器构造的SQL构建的轻量级框架,并且通过配置生成对应的JavaBean返 ...
- 要想精通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映射文件 ...
- 手写MyBatis ORM框架实践
一.实现手写Mybatis三个难点 1.接口既然不能被实例化?那么我们是怎么实现能够调用的? 2.参数如何和sql绑定 3.返回结果 下面是Mybatis接口 二.Demo实现 1.创建Maven工程 ...
- 《手写Mybatis》第4章:Mapper XML的解析和注册使用
作者:小傅哥 系列:https://bugstack.cn/md/spring/develop-mybatis/2022-03-20-%E7%AC%AC1%E7%AB%A0%EF%BC%9A%E5%B ...
- 手写mybatis框架-增加缓存&事务功能
前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...
- 《手写Mybatis》第5章:数据源的解析、创建和使用
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 管你吃几碗粉,有流量就行! 现在我们每天所接收的信息量越来越多,但很多的个人却没有多 ...
- 手写mybatis框架
前言 很久没有更新mybatis的源码解析了,因为最近在将自己所理解的mybatis思想转为实践. 在学习mybatis的源码过程中,根据mybatis的思想自己构建了一个ORM框架 .整个代码都是自 ...
随机推荐
- Vineyard 加入 CNCF Sandbox,将继续瞄准云原生大数据分析领域
简介: Vineyard 是一个专为云原生环境下大数据分析场景中端到端工作流提供内存数据共享的分布式引擎,我们很高兴宣布 Vineyard 在 2021 年 4 月 27 日被云原生基金会(CNCF) ...
- Flink + Iceberg + 对象存储,构建数据湖方案
简介: 上海站 Flink Meetup 分享内容,如何基于Flink.对象存储.Iceberg 来构建数据湖生态. 本文整理自 Dell 科技集团高级软件研发经理孙伟在 4 月 17 日 上海站 ...
- [GPT] 网页中某些dom内容是通过 js 数据异步渲染的,nodejs 怎么获取网页解析这些数据
要处理使用JavaScript异步渲染内容的网页,您可以在 JavaScript 蜘蛛中使用 Puppeter 或 Playwright 等无头浏览器来获取网页,然后与动态渲染的内容进行交互. 下 ...
- [FE] Chrome Extension 五步曲
1. Create the manifest.jsonOnly three fields is needed. { "name": "Getting Started Ex ...
- WPF 通过 RawInput 获取触摸消息
触摸在 Windows 下属于比较特殊的输入,不同于键盘和鼠标,键盘和鼠标可以通过全局 Hook 的方式获取到鼠标和键盘的输入消息.而触摸则没有直接的 Hook 的方法.如果期望自己的应用,可以在没有 ...
- WPF 推荐一个剪贴板内容查看工具
本文来安利大家一个好用的 Windows 剪贴板的内容查看工具 这是在 GitHub 上完全免费开源的应用,由 walterlv 开发的应用,详细请看 https://github.com/walte ...
- dotnet 使用 XWT 构建跨平台客户端 入门篇
本文告诉大家如何入门开始开发一个基于 mono 组织开源的 XWT 跨平台客户端 UI 框架的应用,本文的 xwt 是在 GitHub 上完全开源的,基于 MIT 协议的,底层采用 GTK# 的 UI ...
- 设置Mysql数据库允许远程连接
Mysql数据库用户权限设置 1.进入容器 docker exec -it mysql_test /bin/bash 注意:由于我是通过docker安装的数据库,所以在操作之前需要进入容器,直接安装在 ...
- 特权同学笔记-《边练边学》-在QP里调用modelsim的步骤
在QP里调用Modelsim需要先设置仿真参数和工具路径. 在QP调用modelsim的步骤 1. 在QP里建立工程,代码,分析综合:2. 添加testbench代码,processing-start ...
- Multisim仿真验证之二极管的特性参数
二极管的特性 正向 R1 10% 20% 30% 50% 70% 90% Vd/mV 299 543 583 608 627 658 Id/mA 0.01 0.1 0.6 1.4 2.8 7.2 rd ...