『手写Mybatis』实现映射器的注册和使用

news/2024/10/4 1:20:49

前言

如何面对复杂系统的设计?

我们可以把 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 封装之后的对象*/@Overridepublic <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*/@Overridepublic <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*/@Overridepublic <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*/@Overridepublic 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);@Testpublic void test_MapperProxyFactory() {// 1. 注册 MapperMapperRegistry registry = new MapperRegistry();registry.addMappers("top.it6666.test.dao");// 2. 从 SqlSession 工厂获取 SessionSqlSessionFactory 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』,我会在公众号中分享更多的技术文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/44441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

松鼠的新家

https://www.luogu.com.cn/problem/P3258 考虑用 LCA。 注意我们不统一起点,统计终点。 最后统一然起点多一个糖果,终点较少一个。 首先处理链的情况。 然后对于一般情况:注意我们最后从下往上做差分,发现 \(s_x+=1,s_y+=1,s_p+=1,s_{pf}+=0\),符合要求。

裸函数和调用约定

一、裸函数 在正常的函数编译中,即使函数没有定义函数体的内容,编译器也依然会编译出部分汇编指令用来执行函数。但是如果定义一个裸函数 void _declspec(naked) test()编译器将不会操作这个函数,不会给其生成汇编指令(但是会在主函数中生成call和jmp指令指向这个裸函数)可…

帮猪猪修修改的代码2016年的代码记录

这是一个图片轮播的代码,但是它们的是css 动画,当时代码运行不了,我花了二天才修改,现在记录一下,凭回忆用。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>网易科技</title> <meta name="…

input的时候, 我输入一条链接可以运行,但输入两条会报错?

大家好,我是Python进阶者。 一、前言 前几天在Python交流群【Cappuccino】问了一个Python基础的问题,问题如下:再問一個沒那麼複雜的問題,請教一下,當我改成input 的時候, 我輸入一條鏈接可以運行,但輸入兩條就會報錯,請問多於一條鏈接的輸入格式是怎樣呢? 二、实现过…

Nivdia向量数据库图检索最新标杆——CAGRA

本文连接:https://wanger-sjtu.github.io/CARGA/ CAGRA 是 N社在RAFT项目中 最新的 ANN 向量索引。这是一种高性能的、 GPU 加速的、基于图的方法,尤其是针对小批量情况进行了优化,其中每次查找只包含一个或几个查询向量。 与其他像HNSW、SONG等这类基于图的方法相似,CAGRA…

[转]32th@探索C++的模板元编程:揭秘零运行时开销的高性能编程技术@20240616

C++的模板元编程是一种强大的编程技术,它能够在编译时进行计算,生成高效的代码,而且不需要任何运行时开销。这种技术被广泛应用于高性能计算、游戏开发、金融等领域,是C++程序员必须掌握的技能之一。本文将深入探讨C++模板元编程的原理和实现方式,并通过代码案例来展示其强…

Docker部署SpringBoot项目

准备 服务器安装Docker 下载docker Windows版本并登录 根据项目需要在项目根目录下创建Dockerfile文件 # 使用官方的 OpenJDK 8 作为基础镜像 FROM openjdk:8-jdk-alpine# 维护者信息 LABEL maintainer="name"# 添加一个应用程序的工作目录 WORKDIR /app# 将 JAR 文件…

spring-5-事务

参考: spring 事务失效的 11 种场景 一、事务基础 1.什么是事务 事务是指作为单个逻辑工作单元执行的一系列操作,要么全部成功执行,要么全部失败回滚到初始状态,保证数据的一致性和完整性。事务具有ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isola…