Mybatis系列1:Mybatis的架构

1.为什么使用Mybatis

如果不使用Mybatis,我们可以直接使用JDBC来操作数据库,可以使用Hibernate,也可以使用JPA或者Spring 的JdbcTemplate等ORM框架来操作数据库,这几个相对比JDBC过于繁琐,重复代码太多,难以维护,现在我们很少直接用。而Hibernate框架封装程度太高,不利于优化,也不支持动态SQL,灵活性差。
Spring JDBCTemplate比较小巧,对于数据操作相对简单固定的场景,该小巧的框架就够了。
而JPA是Hibernate的一个子集,很多兼有Hibernate的封装和Mybatis的灵活,也是用的比较多的orm框架。
Mybatis是一个半自动框架,封装程度没有Hibernate那么高,不会自动生成全部的SQL语句,但是大部分重复工作都能自动生成,因此灵活性也很高。

2.Mybatis的核心特征

Mysql的核心特征主要包括以下问题:
1.使用连接池对连接进行管理,
2.SQL和代码分离,结果集映射,.参数映射
3.动态SQL
4.缓存管理
5.插件机制
下面先分析一下前三个,后两个比较复杂,后面章节单独讨论。

2.1 连接池管理连接

ORM和连接池是独立的,不用orm也可以直接使用连接池。
连接池可以减少连接创建和释放的开销,提高处理性能。
问题:有连接池的时候如何保证web请求的无状态的?
SpringMVC响应请求时无状态的,但是到了dao层,数据库连接池是有状态的,两者怎么管理的呢
每个请求会独占一个连接池的连接,用完就归还,多个请求之间不会相互影响,这样就保证了每个请求都是无状态的。
Mybatis自带了两种Unpooled和pooled,但是一般我们会用更流程的开源连接池软件例如hikaCP或者Druid等。

2.2.SQL和代码分离,集中管理

这个主要是将SQL写在XML中,也就是经常见到的各种**Map.xml,代码通过路径查找和映射。SQL语句和配置信息都写在XML文件中,代码只负责对象的管理和业务的实现,从而实现两者的分离,但是执行时仍能交互。
为了保证准确执行,Mybatis还提供了参数集和结果集的映射方法,就是当前的入参和出参与代码中的哪个对象关联的。所以当我们自己写xml的时候,一般先将需要的Dto对象,实体对象等都先定义好。

2.3 动态SQL

由于业务传入的查询参数可能不同,这导致最终执行的SQL会有差异,为了让程序更具适用性,可以在XML映射文件里添加 if else,choose,trim 和foreach等完成条件过滤。

3. Mybatis的架构

MyBatis 的整体架构分为三层 , 分别是基础支持层 、 核心处理层和接口层,如下图所示:
Mybatis系列1:Mybatis的架构_第1张图片
下面分别看每个层次的功能。

3.1 基础支持层

基础支持层包含整个 MyBatis 的基础模块,这些模块为核心处理层的功能提供了良好的支撑。下面简单梳理各个模块的功能。
1.反射模块
Java 中的反射虽然功能强大,但对大多数开发人员来说,写出高质量的反射代码还是有一定难度的 。 MyBatis 中专门提供了反射模块,该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API,方便上层使调用,并且对反射操作进行了一系列优化,例如缓存了类的元数据,提高了反射操作的性能 。
2.类型转换模块
MyBatis 为简化配置文件提供了别名机制 , 该机制是类型转换模块的主要功能之一 。 类型转换模块的另一个功能是实现 JDBC 类型与 Java 类型之间的转换,该功能在为 SQL 语句绑定实参以及映射查询结果集时都会涉及。在为 SQL 语句绑定实参时, 会将数据由 Java 类型转换成 JDBC 类型;而在映射结果集时,会将数据由 JDBC 类型转换成 Java 类型。
3.日志模块
无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈、等问题 。 目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j 、 Log4j2, slf4j 等 。 MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是集成第三方日志框架。
4.资源加载模块
资源加载模块主要是对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件以及其他资源文件的功能 。
5.解析器模块
解析器模块的主要提供了两个功能 : 一个功能是对 XPath 进行封装,为 MyBatis 初始化时解析 mybatis-config.xml 配置文件以及映射配置文件提供支持;另一个功能是为处理动态 SQL 语句中的占位符提供支持。
6.数据源模块
数据源是实际开发中常用的组件之一。 现在开源的数据源都提供了比较丰富的功能,例如,连接池功能、检测连接状态等,选择性能优秀的数据源组件对于提升 ORM 框架乃至整个应用的性能都是非常重要的。 MyBatis 自身提供了相应的数据源实现,当然 MyBatis 也提供了与第三方数据源集成的接口,这些功能都位于数据源模块之中 。
7.事务管理
MyBatis 对数据库中的事务进行了抽象,其自身提供了相应的事务接口和简单实现。在很多场景中, MyBatis 会与 Spring 框架集成,并由 Spring 框架管理事务.
8.缓存模块
在优化系统性能时 ,优化数据库性能是非常重要的一个环节,而添加缓存则是优化数据库时最有效的手段之一。正确合理地使用缓存可以将一部分数据库请求拦截在缓存这一层,这就能够减少相当一部分数据库的压力。
MyBatis 中提供了一级缓存和二级缓存,而这两级缓存都是依赖于基础支持层中的缓存模块实现的。这里需要读者注意的是, MyBatis 中自带的这两级缓存与 MyBatis 以及整个应用是运行在同一个 川币4 中的,共享同一块堆内存。如果这两级缓存中的数据量较大, 则可能影响系统中其他功能的运行,所以当需要缓存大量数据时 ,优先考虑使用 Redis 、 Memcache 等缓存产品。
9.Binding 模块
通过前面的示例我们知道,在调用 SqISession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行时才能发现相应的异常 。 为了尽早发现这种错误, MyBatis 通过 Binding 模块将用户自定义的 Mapper 接口与映射配置文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。

值得读者注意的是,开发人员无须编写自定义 Mapper 接口的实现, MyBatis 会自动为其创建动态代理对象 。在有些场景中,自定义 Mapper 接口可以完全代替映射配置文件,但有的映射规则和 SQL 语句的定义还是写在映射配置文件中比较方便,例如动态 SQL语句的定义 。

3.2 核心处理层

我们来分析 MyBatis 的核心处理层。在核心处理层中实现了 MyBatis 的核心处理流程,其中包括 MyBatis 的初始化以及完成一次数据库操作的涉及的全部流程 。 下面简单描述各个模块的功能,后面会详细分析核心处理层的实现原理 。
1.配置解析
在 MyBatis 初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration 对象中 。 例如定义的<resultMap>节点(即 ResultSet 的映射规则)会被解析成 ResultMap 对象:<result>节点(即属性映射)会被解析成ResultMapping 对象 。 之后,利用该 Configuration 对象创建 Sq!SessionFactory 对象 。待 MyBatis 初始化之后,开发人员可以通过初始化得到 Sq!SessionFactory 创建Sq!Session 对象并完成数据库操作。
2.SOL 解析与 scripting 模块
拼凑 SQL 语句是一件烦琐且易出错的过程,为了将开发人员从这项枯燥无趣的工作中解脱出来, MyBatis 实现动态 SQL 语句的功能,提供了多种动态 SQL 语句对应的节点,例如,<where>节点、<if>节点、<foreach>节点等。通过这些节点的组合使用, 开发人员可以写出几乎满足所有需求的动态 SQL 语句。
MyBatis 中的 scripting 模块会根据用户传入的实参,解析映射文件中定义的动态 SQL节点,并形成数据库可执行的 SQL 语句 。之后会处理 SQL 语句中的占位符,绑定用户传入的实参。
3.SOL 执行
SQL 语句的执行涉及多个组件 ,其中比较重要的是 Executor 、 StatementHandler 、ParameterHandler 和 R巳sultSetHandler 。 Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作 ,它会将数据库相关操作委托给 StatementHandler 完成。StatementHandler 首先通过 ParameterHandler 完成 SQL 语句的实参绑定,然后通过java.sql.Statement 对象执行 SQL 语句并得到结果集,最后通过 ResultSetHandler 完成结果集的映射,得到结果对象并返回 。下图展示了 MyBatis 执行一条 SQL 语句的大致过程。
Mybatis系列1:Mybatis的架构_第2张图片
4.插件
Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBati s 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。

3.3 接口层

接口层相对简单,其核心是 SqlSession 接口,该接口中定义了 MyBatis 暴露给应用程序调用的 API,也就是上层应用与 MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作 。 SqlSession 接口及其具体实现将在后面单独介绍。

4. Mybatis的工作流程

如下图所示:
Mybatis系列1:Mybatis的架构_第3张图片
mybatis的工作流程

  1. 加载配置文件并初始化
    加载配置文件,配置文件由主配置congfig.xml 以及用户定义的mapper.xml 文件。
    加载注解,即主配置中内容解析封装到configuration,以及用户使用的mapper注解或mapperscan,将其mapper信息封装成mappedstatement存储到内存中

  2. 接收调用请求
    接受用户调用mybatis提供的api,将参数和sql传递数据处理层

  3. 处理请求
    处理流程如下:

根据sql id寻找对应的MappedStatement对象
根据入参解析MappedStatement对象,得到要执行的sql和传入参数
获取数据库连接,执行sql,并处理结果
根据MappedStatement对象中配置的输出结果进行转换
释放数据库连接资源
4. 返回处理结果
将封装好的结果返回出去

5. Mybatis的重要问题

5.1 XML映射文件与Dao接口是对应的,其工作原理是什么

问题:通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个、、、标签,都会被解析为一个MapperStatement 对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id 为findStudentById 的 MapperStatement。Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

5.2 Mybatis 是如何进行分页的?分页插件的原理是什么?

Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

5.3 Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。

5.4 Mybatis 的一级、二级缓存

1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

你可能感兴趣的:(JavaEE)