# mybatis-mini **Repository Path**: harvey4j/mybatis-mini ## Basic Information - **Project Name**: mybatis-mini - **Description**: 参考mybatis源码,逐步构建mini版mybatis。主要基于xml配置版本~Harvey4j - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 1 - **Created**: 2022-11-30 - **Last Updated**: 2024-01-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: MyBatis, source, Java ## README # harvey-mybatis-mini ## 介绍 参考mybatis源码,构建自己的mini版mybatis。~Harvey4j mybatis-mini分为两个部分,一部分是最终的xml版本,mybatis-mini的框架依赖包,外部项目若需要引入此包只需要引入harvey-mybatis-mini的依赖即可。另一部分是按流程分析源码编写mybatis-mini的步骤,总共分为18步,主要基于xml版本的mybatis编写,之所以选择逐步渐进式的分析并手写源码,而不是一步到位,这样能让自己更加深刻的理解mybatis框架构建的原理,理解其设计模式的使用,由浅入深,锻炼自己的思维。本项目mybatis-mini涵盖了mybatis的几乎所有核心功能点,主要用于个人对源码的钻研和学习,由于本人能力有限,难免细节上会出现错误,欢迎提出问题,我会虚心纠正。 之后版本会继续整合spring,发布harvey-mybatis-spring-mini,mybatis-spring的设计也十分微妙,它通过spring的生命周期管理,sqlSessionFactoryBean实现InitializingBean接口,在afterPropertiesSet()方法中完成相关配置的解析。利用sqlSessionTemplate替换之前的DefaultSqlSession,利用SpringManagedTransactionFactory替换之前的JdbcTransactionFactory,sqlSessionTemplate通过对SqlSession的代理完成与spring事务的整合。 ## 项目模块 | 包名 | 简介 | |---|---| | annotations | 注解相关包,如@Select/@Insert | | bingding | 资源绑定相关包,核心包,主要包含mapper接口代理绑定 | | builder | 资源构建解析相关包,主要负责xml解析相关工作 | | cache | 缓存相关包,主要支撑一级缓存和二级缓存的功能 | | datasource | 数据源相关包,主要负责池化和非池化数据源的支撑 | | executor | sql执行器相关包,主要包含了mybatis中的四大组件,负责sql的执行,参数处理,sql预编译,结果集处理等工作 | | io | 文件流io相关包,工具类主要负责文件流解析 | | mapping | 资源映射相关包,主要包含了一些资源的映射对象,供全局调用 | | parsing | sql解析相关包,主要负责解析sql中的参数等 | | plugin | 插件机制相关包,是mybatis-mini插件机制的支撑,包括组件拦截器与插件增强功能 | | reflection | 反射工具类,主要提供元对象反射解析工具 | | scripting | 脚本工具类,主要提供sql脚本解析功能,根据指定的方式去解析sql | | sqlsession | session会话相关包,主要包含全局配置类configuration以及sqlSession相关的实现 | | transaction | 事务相关包,提供事务相关功能 | | type | typeHandler类型相关包,主要保存typeHandler相关的类型处理器 | ## 使用说明 1. 下载本项目源码到本地环境 2. 进入源码目录,这里只需通过maven构建工具将模块module -> harvey-mybatis-mini,mvn install到本地仓库中。 3. 之后在需要引入harvey-mybatis-mini的项目的pom文件中添加以下依赖即可 ``` com.mini.mybatis harvey-mybatis-mini 1.2-SNAPSHOT ``` 4. 在resource目录下添加mybatis-config.xml配置文件,下面是示例 ``` ``` 5. 在resources目录下添加mapper文件夹,添加mapper.xml文件 6. 添加与mapper.xml对应的mapper接口 7. 编写测试类,测试是否可行 ``` @SpringBootTest class MybatisMiniHuhuTestApplicationTests { private static SqlSession sqlSession; @BeforeAll public static void init() throws IOException { // 首先通过配置文件获取sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml")); // 获取sqlSession sqlSession = sqlSessionFactory.openSession(); } @Test public void test() throws IOException { // 获取指定Mapper的代理 TeamMapper teamMapper = sqlSession.getMapper(TeamMapper.class); // 执行代理对象的查询方法 Team team = teamMapper.selectByTeamId(1006); System.out.println(team); } } ``` 8. 其它扩展机制的使用如插件拦截机制,一二级缓存的使用,动态sql等功能,这里不做详细介绍,可以下载本mybatis-mini源码自己验证 ## 源码流程分析总结 下面主要介绍一下mybatis-mini源码中这18步都分别做了什么以及自己完成每步的一些思考。 ### mybatis-mini-step01 **思考:** 当我们想开发一个框架的时候,我们会从哪里入手?我们肯定想去屏蔽并封装一些底层的操作,尽可能的简化用户的使用。正如mybatis源码中,我们只需要对mapper接口的某个方法调用,如 User user = userMapper.selectByUid(10001L),它就能从数据库中查询uid为10001的用户信息。诸如此类的操作都是通过什么实现的?没错,就是利用代理。我们可以通过对userMapper创建代理,对userMapper进行增强,把我们的增强逻辑都写到代理的拦截逻辑中。 **内容:** step01主要实现mybatis中简易的MapperProxy和MapperProxyFactory,它们是mybatis框架的核心。MapperProxy中保存着sqlSession和被代理的mapper接口mapperInterface,后续的对数据库的操作都通过sqlSession去执行,mybatis-spring中也是通过创建mapper代理这一步将这里的sqlSession替换成了SqlSessionTemplate。 ### mybatis-mini-step02 **思考:** 在step01中,实现了对单个mapper接口的创建代理操作,如果我们需要批量创建代理,并且需要保存到缓存中,供后续调用的时候直接获取对应的mapper代理,我们应该怎么做?我们需要引入包扫描功能,通过HashMap缓存mapper代理。 **内容:** step02中引入MapperRegistry,通过HashMap缓存MapperProxyFactory,当用到某个mapper接口的时候再去MapperRegistry中根据类型获取对应的MapperProxyFactory,并返回当前mapper的代理,这里引用的hutool工具包的ClassScanner,对mapper接口的包路径进行包扫描,批量addMapper到MapperRegistry。同时提供SqlSession顶层接口以及默认实现DefaultSqlSession,以及SqlSessionFactory。 ### mybatis-mini-step03 **思考:** 在前两个步骤我们对mapper接口创建代理并保存到了缓存中,那怎么和我们在mapper.xml中的sql的对应上呢?当我们调用mapper接口中方法的时候,它底层又是怎么执行sql并返回数据的呢?我们首先需要预先解析mapper.xml文件,将xml中的信息保存到某个对象中,通过xml文件中的namespace与mapper接口的类全限定名对应,当执行到mapper接口对应方法的时候,根据namespace + sqlId唯一对应一条sql,进而连接数据库查询。 **内容:** step03中通过SqlSessionFactoryBuilder对mapper.xml进行解析并返回SqlSessionFactory,当前只对mapper标签进行简单的解析,只解析select标签,保存在MappedStatement中(mybatis会运用大量的 **建造者模式** 来构建这些类似的对象),最后注册进全局配置类Configuration中,在解析mapper.xml后根据namespace去注册mapper接口到mapperRegistry的konwMappers中。 ### mybatis-mini-step04 **思考:** 前面步骤中我们完成了mapper代理的创建与映射,mapper.xml的解析与封装。我们需要执行sql并获取数据库链接,我们就要在配置文件中添加dataSource数据源和事务的配置,在解析xml配置文件的时候去解析这些配置,然后在我们执行sql之前从数据源中获取数据库链接。 **内容:** step04中需要在配置文件中引入environments标签,这里需要配置transactionManager以及dataSource,然后在SqlSessionFactoryBuilder解析xml配置文件的时候去解析这些配置,mybatis中这里采用TypeAliasRegistry别名注册器,在Configuration中提前注册好了默认的事务工厂和数据源。包括我们的基本数据类型如int、long、double等都已经在TypeAliasRegistry中初始化配置好。注意,mybatis默认的事务工厂是JdbcTransactionFactory。在mybatis-spring中需要被SpringManagedTransactionFactory所替代,从而将事务交于spring管理。 ### mybatis-mini-step05 **思考:** 我们在step04中添加了数据源配置,从而我们可以从数据源中获取数据库链接执行sql返回结果,那如果一旦请求量激增,我们的性能就会急剧下降,甚至会影响到上游的服务,mybatis其实考虑到了这一点,为我们提供了数据源的池化实现PooledDataSource和无池化实现UnpooledDataSource,可以通过配置数据库连接池配置控制最大活跃连接或空闲连接数,当然我们也可以引入第三方的数据库连接池,其实数据库连接池的实现可以理解为 **享元模式** 。 **内容:** 在myBatis数据源DataSource的实现中分为池化实现PooledDataSource和无池化实现UnpooledDataSource,其中池化实现PooledDataSource是对无池化实现UnpooledDataSource的扩展处理,把创建出来的链接保存在内容中,分为活跃连接盒空闲链接,根据不同的情况选择使用。其中在池化链接中,PooledConnection是对链接的代理操作,对调用关闭方法的链接进行回收处理,并通过notifyAll通知其它正在等待链接的线程,去争抢链接。 ### mybatis-mini-step06 **思考:** 前面步骤中在执行sql的时候,全部耦合在了DefaultSqlSession中,这样很不利于mybatis框架的扩展,我们需要进一步解耦这些步骤,包括对配置文件的解析、数据源的调用、sql的执行、结果的封装等。mybatis讲这些需要解耦的步骤和职责下发到了mybatis四大组件中,如Executor、StatementHandler、parameterHandler、resultSetHandler,各司其职,从而实现了解耦,又易于扩展。我们需要借鉴这种思想,很适合引入到我们平时的业务开发中。 **内容:** 在通过SqlSessionFactoryBuilder对xml文件解析返回SqlSessionFactory之后,我们从SqlSessionFactory.openSession返回sqlSession的时候我们需要将Executor执行器封装到SqlSession中,之后执行sql通过Executor执行器执行,我们这里先通过SimpleExecutor做Executor的简单实现,后续引入缓存机制通过 **装饰器模式** 包装成CachingExecutor返回。然后通过StatementHandler处理sql,resultSetHandler处理结果集,进而返回结果。从这些四大组件的源码中可以看出,mybatis中大量使用 **模板方法模式** 和 **策略模式** ,重要关键的步骤都交给子类去实现,根据不同的场景选取不同的子类实现。 ### mybatis-mini-step07 **思考:** 在step04中,我们引入数据源的时候,通过读取xml中的配置,获取对应的数据源。在读取数据源配置这一步有没有发现,我们是采用的硬编码的方式获取的,这样看似不会有什么问题,但这无法适配所有的属性读取场景,就比如有的地方密码字段叫password,有的地方又叫pw的简写,这种情况很明显不行,所以有没有一种方法可以适配所有的场景?有,那就是通过反射机制。 **内容:** 参考mybatis源码,源码中在处理数据源配置这块的时候运用的是自己实现的元对象反射工具类MetaObject,它内部是通过传入元对象,通过反射解析元对象中的属性,方法,构造函数等,并提供统一的set,get方法,对元对象的属性进行操作,非常方便。mybatis对该元对象反射工具类MetaObject的处理比较繁琐,所以需要更加细心的研究并反复阅读源码,才能看清它的实现~ ### mybatis-mini-step08 **思考:** 在前面步骤中,我们对mapper.xml的解析都是在一个方法中完成的,这有点像我们平时开发业务的时候,把所有的业务堆到一个方法处理,这样会显得我们的代码很臃肿不易维护,我们需要进一步优化这个步骤,不把所有的解析都在一个循环中处理,将职责分配下去。 **内容:** 参考mybatis源码,引入XMLMapperBuilder,和XMLStatementBuilder,分别处理映射构造器和语句构造器,他们分成不同的职责进行解析,并且引入脚本语言驱动器,默认实现是XMLLanguageDriver,它用来具体操作静态SQL和动态SQL语句节点的解析。主要对mapper.xml的解析做流程优化。 ### mybatis-mini-step09 **思考:** 思考一个问题,在参数处理流程中,我们在PreparedStatement进行预处理sql执行参数设置的时候,如果sql语句中不同参数的类型不同,我们应该调用哪种typeHandler来处理参数呢?比如Long类型和String类型的参数。我们可以采用 **策略模式** 来处理,而不用每次去判断参数类型进而处理对应的参数。 **内容:** step09采用策略模式选择不同的参数处理器设置参数,并在解析sql的流程中将这些参数保存为parameterMappings。parameterMappings中封装了参数的名称,以及typeHandler等信息,最后通过合适的typeHandler来进行参数设置。 ### mybatis-mini-step10 **思考:** 前面我们对mapper.xml解析的步骤以及sql执行的流程进行了解耦,但我们会发现我们在处理结果集的时候,我们因为不清楚ResultType统一返回的Object对象,所以我们需要在前面解析并封装MappedStatement的时候封装resultType,resultMap供后续结果封装的时候使用,这样就可以根据类型找到typeHandler的具体实现类,这样就可以避免了大量if-else的判断从而在O(1)的时间复杂度内定位到对应的类型处理器。 **内容:** 参考mybatis源码,这里在解析mapper.xml的时候,引入MapperBuilderAssistant映射器助手类。方便我们对参数的统一包装处理,并且,在封装返回参数类型的时候,这里把ResultType也封装为ResultMap进行处理,这样就统一了一个标准进行封装,做到适配的效果。 ### mybatis-mini-step11 **思考:** 截止到step10,我们实现的mybatis-mini框架已经能满足对一个DAO方法的查询操作,以及处理对应的参数和返回结果。如果我们的框架只有一个select相关的操作,那完全无法满足我们的日常需求,所以我们要继续扩展insert/update/delete等操作。 **内容:** 在SqlSession中定义新的insert/update/delete方法,在解析mapper.xml的时候,利用XMLMapperBuilder新增解析这些标签并保存为MappedStatement,后续调用流程中就通过MapperMethod根据CommandType去调用DefaultSqlSession中的这些方法。 ### mybatis-mini-step12 **思考:** 参考mybatis源码,在configuration.addMapper(type)方法内部,在注册mapper代理工厂的同时,会有一步去解析注解版sql的步骤,例如@Select/@Insert等注解。在我们日常开发场景,xml方式用的是最多的,且更易于维护,但是注解版的方式在一些简单的场景下也是非常方便的。 **内容:** 通过MapperAnnotationBuilder在configuration.addMapper(type)内部添加一步去解析注解版sql,利用与解析xmlsql相同的流程,封装为MappedStatement,之后在sql执行和获取的流程是几乎一样的。 ### mybatis-mini-step13 **思考:** 在前面的步骤中,我们在处理结果映射的时候没有去注意字段驼峰命名的处理,导致我们如果数据库表中的字段名和我们映射pojo中的属性名不一致的时候会无法完成映射,所以我们需要在mapper.xml中额外配置resultMap标签,在解析xml的时候去解析并保存,这样在处理结果集映射的时候就可以根据resultMap中配置的映射规则去匹配映射字段。 **内容:** step13步中在XMLMapperBuilder的configurationElement配置元素解析的方法中,新增加了resultMap元素的解析,因为一个mapper.xml可能配置多个resultMap标签,所以这里是去循环解析,每个resultMap标签保存的是resultMappings集合。注意在处理结果映射的时候,会借助之前的MetaObject反射工具,其实在mybatis框架的很多地方都用到了这个工具,仔细体会。 ### mybatis-mini-step14 **思考:** 目前,我们执行insert/update/delete语句的时候返回的都是数据库表影响的行数。如果我们想在insert语句执行之后返回插入的主键id,我们应该怎么实现?在insert语句中添加selectKey标签,那就又引入一个问题,selectKey标签也相当于一次查询,insert和selectKey不是同一个数据库链接,那将失去事务的特性,也就无法实现此功能。所以这两个sql需要在同一个数据库链接中处理。 **内容:** 在mapper.xml解析流程中添加selectKey标签的解析。解析完成后也和解析其它类型的标签一样,保存为MappedStatement映射器语句并存放到Configuration中,这样后面执行sql的时候可以直接从configuration中获取。另外对于键值的处理,我们需要单独包装KeyGenerator,完成sql的调用和封装。注意,在执行到insertsql中的selectKey的时候,会通过KeyExecutor再去执行query方法,这里去获取链接的时候,需要保证两次是同一个数据库链接,否则会失效。 ### mybatis-mini-step15 **思考:** 在我们的实际业务中,有很多场景都需要根据条件来判断字段是否存在,从而决定是否拼接sql。目前我们框架中的静态sql已经无法满足我们的需求,我们需要通过XML的脚本构建起扩充对动态SQL的处理。 **内容:** 我们需要扩展SqlSource的构建策略,提供新的实现类DynamicSqlSource用于解析动态sql。在sqlSouce中判断当前解析的sql是否包含我们的动态标签的信息,包括trim/where/set/foreach/if/choose/when/otherwise等。解析过程循环拆解标签结点。因为在我们的动态sql语句中,包含了text文本,trim拼装和if判断等。所以我们要提供不同的处理策略,所以我们需要扩展StaticTestSqlNode,新增TextSqlNode、TrimSqlNode、IfSqlNode这些实现类。参考mybatis源码,解析动态sql的整体流程较为复杂,需要更加细心的研究。 ### mybatis-mini-step16 **思考:** 一个优秀的框架,它肯定会预留优秀的扩展机制,这些扩展机制的原理绝大部分都是通过代理所实现,正如myabtis预留的插件机制,它本质上就是拦截器,从而生成代理,具体的拦截逻辑交给我们使用者根据具体业务场景实现,其实这种代理的运用,不止在mybatis中,在其它的如spring,dubbo等框架也都大量的运用代理去实现可插拔的功能。可见代理模式的强大之处。 **内容:** 参考mybatis源码,之前我们关注的mybatis四大组件Executor、StatementHandler、parameterHandler、resultSetHandler。mybatis都提供了interceptorChain.pluginAll方法,对着四大组件进行拦截增强,通过配置@Intercepts注解配置我们的自定义插件,根据注解属性是否拦截并创建代理,当执行到我们拦截的组件中的方法的时候就会进入拦截逻辑并执行。 ### mybatis-mini-step17 **思考:** 如果在一次SqlSession的数据库会话中,如果我们前后重复执行了完全相同的查询sql,如果不采取一些优化手段,每一次查询都与数据库交互,将非常浪费数据库资源,所以我们应该考虑是否需要引入缓存机制,相同查询只需要第一次与数据库交互,而后续只需要从缓存中获取即可。所以我们要引入mybatis的一级缓存机制。 **内容:** 一级缓存为又叫本地缓存LocalCache,在sqlSessionFactory.openSession的时候,创建Executor的时候,会创建一级缓存的实现PerpetualCache,在我们执行sql的时候,会首先为我们的缓存生成缓存CacheKey,这里CacheKey的生成规则为以mappedStatementId + offset + limit + SQL + queryParams + environment生成对应的hashCode作为key。之后在查询sql之前先从一级缓存中获取,如果不存在会走数据库查询,之后再放回一级缓存。整个流程比较清晰。 ### mybatis-mini-step18 **思考:** step17中一级缓存是sqlSession级别的,那么在多个线程中的多个sqlSession是无法运用到一级缓存的,我们需要引入更高级别的缓存,就是namespace级别的缓存,多个线程调用同一个mapper中的方法,只要namespace相同,都可以运用到这个缓存,这也是mybatis实现的二级缓存机制。但是在某些场景会有个问题,如果是分布式环境,在不同的实例上,二级缓存是很难命中的,这也是为什么二级缓存默认为关闭状态的原因。 **内容:** 二级缓存的实现是建立在以及缓存的基础上的,区别在于,二级缓存需要在mapper.xml中配置cache标签,并需要在环境配置中设置cached为true,打开二级缓存。同样在Executor实例化的时候,会大量使用 **装饰器模式** 对SimpleExecutor进行层层包装,最后返回CachingExecutor,一级缓存和二级缓存用的是同一个缓存Key,二级缓存是在sqlSession执行commit和close的时候讲缓存数据刷进二级缓存。