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

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

- MapperRegistry 提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。
- SqlSession、DefaultSqlSession 用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用 Mybatis 的 API 接口也都是从这个接口类定义的方法进行使用的。
- SqlSessionFactory 是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。
2. 映射器注册机
源码详见:cn.bugstack.mybatis.binding.MapperRegistry
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 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 时进行使用。
3. SqlSession 标准定义和实现
源码详见:cn.bugstack.mybatis.session.SqlSession
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,后续还会有相应其他方法的定义。
源码详见:cn.bugstack.mybatis.session.defaults
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入参:" + parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
- 通过 DefaultSqlSession 实现类对 SqlSession 接口进行实现。
- getMapper 方法中获取映射器对象是通过 MapperRegistry 类进行获取的,后续这部分会被配置类进行替换。
- 在 selectOne 中是一段简单的内容返回,目前还没有与数据库进行关联,这部分在我们渐进式的开发过程中逐步实现。
4. SqlSessionFactory 工厂定义和实现
源码详见:cn.bugstack.mybatis.session.SqlSessionFactory
public interface SqlSessionFactory {
/**
* 打开一个 session
* @return SqlSession
*/
SqlSession openSession();
}
- 这其实就是一个简单工厂的定义,在工厂中提供接口实现类的能力,也就是 SqlSessionFactory 工厂中提供的开启 SqlSession 的能力。
源码详见:cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
- 默认的简单工厂实现,处理开启 SqlSession 时,对 DefaultSqlSession 的创建以及传递 mapperRegistry,这样就可以在使用 SqlSession 时获取每个代理类的映射器对象了。
五、测试
1. 事先准备
在同一个包路径下,提供2个以上的 Dao 接口:
public interface ISchoolDao {
String querySchoolName(String uId);
}
public interface IUserDao {
String queryUserName(String uId);
Integer queryUserAge(String uId);
}
2. 单元测试
@Test
public void test_MapperProxyFactory() {
// 1. 注册 Mapper
MapperRegistry registry = new MapperRegistry();
registry.addMappers("cn.bugstack.mybatis.test.dao");
// 2. 从 SqlSession 工厂获取 Session
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(registry);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 4. 测试验证
String res = userDao.queryUserName("10001");
logger.info("测试结果:{}", res);
}
- 在单元测试中通过注册机扫描包路径注册映射器代理对象,并把注册机传递给 SqlSessionFactory 工厂,这样完成一个链接过程。
- 之后通过 SqlSession 获取对应 DAO 类型的实现类,并进行方法验证。
测试结果
22:43:23.254 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:你被代理了!方法:queryUserName 入参:[Ljava.lang.Object;@50cbc42f
Process finished with exit code 0
- 通过测试大家可以看到,目前我们已经在一个有 Mybatis 影子的手写 ORM 框架中,完成了代理类的注册和使用过程。
六、总结
- 首先要从设计结构上了解工厂模式对具体功能结构的封装,屏蔽过程细节,限定上下文关系,把对外的使用减少耦合。
- 从这个过程上读者伙伴也能发现,使用 SqlSessionFactory 的工厂实现类包装了 SqlSession 的标准定义实现类,并由 SqlSession 完成对映射器对象的注册和使用。
- 本章学习要注意几个重要的知识点,包括:映射器、代理类、注册机、接口标准、工厂模式、上下文。这些工程开发的技巧都是在手写 Mybatis 的过程中非常重要的部分,了解和熟悉才能更好的在自己的业务中进行使用。
带码农《手写Mybatis》进度3:实现映射器的注册和使用的更多相关文章
- 框架源码系列十二:Mybatis源码之手写Mybatis
一.需求分析 1.Mybatis是什么? 一个半自动化的orm框架(Object Relation Mapping). 2.Mybatis完成什么工作? 在面向对象编程中,我们操作的都是对象,Myba ...
- 要想精通Mybatis?从手写Mybatis框架开始吧!
1.Mybatis组成 动态SQL Config配置 Mapper配置 2.核心源码分析 Configuration源码解析 SqlSessionFactory源码解析 SqlSession源码解析 ...
- 手写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 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 手写MyBatis流程
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 面试必会之ArrayList源码分析&手写ArrayList
简介 ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高. 线程 ...
- 从零搭建Spring Boot脚手架(4):手写Mybatis通用Mapper
1. 前言 今天继续搭建我们的kono Spring Boot脚手架,上一文把国内最流行的ORM框架Mybatis也集成了进去.但是很多时候我们希望有一些开箱即用的通用Mapper来简化我们的开发.我 ...
- 手写mybatis框架-增加缓存&事务功能
前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...
随机推荐
- Java的jinfo命令使用详解
jinfo命令简介 jinfo(Java Virtual Machine Configuration Information)是JDK提供的一个可以实时查看Java虚拟机各种配置参数和系统属性的命令行 ...
- C#/VB.NET 将Html转为Excel
本文介绍通过C#和VB.NET代码展示将Html转为Excel文档的方法. dll引用 方法1 将 Spire.XLS for .NET 下载到本地,解压,安装.完成安装后,在安装路径下找到BIN文件 ...
- 魅族mx3手机 固件升级方法
1.正常手机刷机,升级固件的方法 我今天3.5升4.2.8都提示固件损坏 后来我找客服 一定要用电脑下3.8固件 然后按住开机和音量+ 进入刷机模式 然后连接电脑 电脑会给你一个900mb的磁盘 拉进 ...
- Linux巡检检查项
不定时更新...... 1)服务器 1.1 SELINUX检查(sestatus) 1.2 资源限制检查(ulimit -a) 1.3 最近登录(last) 1.4 操作系统版本(cat /etc/r ...
- Activemq特性之持久化--LevelDB特性
LevelDB持久性适配器使用LevelDB作为高性能的消息存储.它是一个基于文件的存储库,它使用了Google的LevelDB,将索引保存到包含消息的日志文件中.它经过优化,提供了比KahaDB更快 ...
- hutool包里的ObjectUtil.isNull和ObjectUtil.isEmpty的区别
大家都用过hutool包把,包路径为:cn.hutool.core.util,最近再使用的过程中一直没高明白ObjectUtil.isEmpty和ObjectUtil.isNull两者到底有那些区别, ...
- Shell:常见错误总结(一)
Blog:博客园 个人 译自BashPitfalls 本文总结了编写Shell脚本中的常见错误. for f in $(ls *.mp3) 最常犯的错之一就是编写这样的循环: for f in $(l ...
- 计算机网络:套接字(Socket)| Python socket实现服务器端与客户端通信,使用TCP socket阿里云ECS服务器与本机通信
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象.一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制.从所处的地位来讲,套接字上联应 ...
- jvm大局观之内存管理篇(二):当java中new一个对象,背后发生了什么
https://zhuanlan.zhihu.com/p/257863129?utm_source=ZHShareTargetIDMore 番茄番茄我是西瓜 那是我日夜思念深深爱着的人啊~ 已关注 ...
- 去掉一个Vector集合中重复的元素 ?
Vector newVector = new Vector(); For (int i=0;i<vector.size();i++) { Object obj = vector.get(i); ...