带码农《手写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 ...
随机推荐
- Linux 中如何使用 Htop 监控工具?【网络安全】
镜像下载.域名解析.时间同步请点击阿里云开源镜像站 一.Htop 界面展示 "Htop 是一个用于 Linux/Unix 系统的交互式实时进程监控应用程序,也是 top 命令的替代品,它是所 ...
- 内置方法 __new__ __del__
1.__new__ 构造方法 实例化对象是先执行__new__方法,但是类中没有__new__方法,所以先到父类object类中的new方法,开辟一个属于对象的空间,然后再执行init方法 设计模式: ...
- Java基础——方法的调用
Java基础--方法的调用 总结: 1. 在同一个类中-- 对于静态方法,其他的静态和非静态方法都可以直接通过"方法名"或者"类名.方法名"调用它. 对 ...
- 配置Openfiler做ISCS实验
在VMware workstation中安装openfiler(linux底层) 系统安装 选择redhat6,64bit 分配30G 2. 3.. 4.5. ...
- CSS入门笔记
CSS @author:伏月廿柒 Cascading Style Sheet 层叠级联样式表 CSS:表现(美化) 字体,颜色,边距,高度,宽度,背景图片,网页定位,网页浮动-- CSS发展史 CSS ...
- 数据结构 - 顺序表 C++ 实现
顺序表 此处实现的顺序表为**第一个位置为 data[0] **的顺序表 顺序表的定义为 const int MAX = 50; typedef int ElemType; typedef struc ...
- 【freertos】005-启动调度器分析
前言 本节主要讲解启动调度器. 这些都是与硬件相关,所以会分两条线走:posix和cortex m3. 原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/160 ...
- Java常见的垃圾收集器有哪些?
守拙者_6a98关注 2020.04.11 22:06:31字数 2,135阅读 394 实际上,垃圾收集器( GC , Garbage Collector )是和具体 JVM 实现紧密相关的,不同厂 ...
- kafka 为什么那么快?
Cache Filesystem Cache PageCache缓存 顺序写 由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要快. Zero-copy 零拷⻉技术减少拷贝 ...
- Mybatis框架基础入门(三)--Mapper动态代理方式开发
使用MyBatis开发Dao,通常有两个方法,即原始Dao开发方法和Mapper动态代理开发方法. 原始Dao开发方法需要程序员编写Dao接口和Dao实现类,此方式开发Dao,存在以下问题: Dao方 ...