导图社区 持久层框架设计实现及MyBatis源码分析
这是一篇关于持久层框架设计实现及MyBatis源码分析的思维导图。自定义持久层框架的本质是对JDBC代码进行了封装,所以底层执行的还是JDBC代码,JDBC代码想要正常执行,两部分信息必不可少,一部分是数据库配置信息,还有一部分是SQL配置信息。
编辑于2021-06-06 22:28:262. 模块一:持久层框架设计实现及MyBatis源码分析
2.1 自定义持久层框架
JDBC操作步骤

1、获取驱动 2、加载连接 3、sql编写 4、创建preparedStatement 5、执行语句preparedStatement.executeQuery() 6、结果封装
2.1.1 JDBC回顾及问题分析
1、数据库配置 硬编码 2、数据库连接频繁创建,消耗资源 解决: 1.通过配置文件 2.利用连接池创建连接
3、SQL语句、参数设置、结果集封装, 存在硬编码 解决: 编写配置文件
4、结果集封装繁琐 解决: 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射
内省: 在类UserInfo中有属性 userName, 那我们可以通过 getUserName,setUserName来得到其值或者设置新的值。通过 getUserName/setUserName来访问 userName属性,这就是默认的规则。 Java JDK中提供了一套 API 用来访问某个属性的 getter/setter 方法,这就是内省。 1 JDK内省类库: PropertyDescriptor类: PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法: 1. getPropertyType(),获得属性的Class对象; 2. getReadMethod(),获得用于读取属性值的方法;getWriteMethod(),获得用于写入属性值的方法; 3. hashCode(),获取对象的哈希值; 4. setReadMethod(Method readMethod),设置用于读取属性值的方法; 5. setWriteMethod(Method writeMethod),设置用于写入属性值的方法。 Introspector类: 将JavaBean中的属性封装起来进行操作。在程序把一个类当做JavaBean来看,就是调用Introspector.getBeanInfo()方法,得到的BeanInfo对象封装了把这个类当做JavaBean看的结果信息,即属性的信息。 getPropertyDescriptors(),获得属性的描述,可以采用遍历BeanInfo的方法,来查找、设置类的属性。 通过这两个类的比较可以看出,都是需要获得PropertyDescriptor,只是方式不一样:前者通过创建对象直接获得,后者需要遍历,所以使用PropertyDescriptor类更加方便。 2 BeanUtils工具包: 由上述可看出,内省操作非常的繁琐,所以所以Apache开发了一套简单、易用的API来操作Bean的属性——BeanUtils工具包。 BeanUtils工具包:下载:http://commons.apache.org/beanutils/ 注意:应用的时候还需要一个logging包 http://commons.apache.org/logging/ 1.获得属性的值,例如,BeanUtils.getProperty(userInfo,"userName"),返回字符串 2.设置属性的值,例如,BeanUtils.setProperty(userInfo,"age",8),参数是字符串或基本类型自动包装。设置属性的值是字符串,获得的值也是字符串,不是基本类型。 3.BeanUtils的特点: 1). 对基本数据类型的属性的操作:在WEB开发、使用中,录入和显示时,值会被转换成字符串,但底层运算用的是基本类型,这些类型转到动作由BeanUtils自动完成。 2). 对引用数据类型的属性的操作:首先在类中必须有对象,不能是null,例如,private Date birthday=new Date();。操作的是对象的属性而不是整个对象,例如,BeanUtils.setProperty(userInfo,"birthday.time",111111); 3.PropertyUtils类和BeanUtils不同在于,运行getProperty、setProperty操作时,没有类型转换,使用属性的原有类型或者包装类。由于age属性的数据类型是int,所以方法PropertyUtils.setProperty(userInfo, "age", "8")会爆出数据类型不匹配,无法将值赋给属性。
2.1.2 自定义持久层框架思路分析
构建步骤
①使用数据库连接池初始化连接资源 ②将sql语句抽取到xml配置文件中 ③使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射
使用端
引入框架包
提供配置信息: (1)sqlMapConfig.xml 数据库连接配置 (2) mapper.xml(多个):sql语句,查询参数,结果封装 等配置
框架
解析使用端提供的配置(SqlMapConfig.xml和 xxxMapper.xml)
解析出来的配置,封装到Java对象中 (1)configuration 存储数据库连接配置 (2)多个mapedStatement:sql映射,mapper.xml解析的内容,利用map存放,key为设定的命名空间namespace
创建类SqlSessionFactoryBuilder,在build(InputStream in) 解析配置 然后利用SqlSessionFactory 生产sqlSession(工厂模式)
基于(开闭原则)创建类:SqlSessionFactory 及其实现类DefaultSqlSessionFactory 实现方法:(1) openSession(): 用于生产sqlSession
创建SqlSession接口及实现 增删改查操作:selectList(),selectOne(),update(),delete()等
Executor接口及实现SimpleExecutor 执行JDBC操作:query(Configuration configuration,MapedStatement statement,Object... params)
2.1.14 功能扩展
使用代理模式生成DAO层接口的代理实现类
2.2 MyBatis基础回顾及高级应用
2.2.1 Mybatis相关概念
2.2.1 对象/关系数据库映射(orm)
2.2.2 相关简介
半自动轻量级持久层框架
优势: Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编 码进行分离,功能边界清晰,一个专注业务,一个专注数据
2.2.2环境搭建回顾
2.2.3 MyBatis的CRUD
传统写法:编写接口及其实现类(IUserDao-->UserDaoImpl)
mybatis采用代理方式:编写接口UserMapper及其sql映射-->UserMapper.xml
Mapper 接口开发需要遵循以下规范: 1) Mapper.xml文件中的namespace与mapper接口的全限定名相同 2) Mapper接口方法名和Mapper.xml中定义的每个statement的id相同 3) Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同 4) Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
2.2.4 MyBatis的相关配置文件
SqlMapConfig.xml
1.properties属性 2.settings设置 3.typeAliases类型别名 4.typeHandlers类型处理器 5.objectFactory对象工厂 6.plugin插件 7.environments环境 7.1 environment环境变量 7.1.1 transactionManage事务管理器 7.1.2 dataSource 数据源 8.databaseIdProvider 9. mapper映射器
配置概览
相关配置及层级关系
1.properties属性(将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的properties文件)

2.settings设置
3.typeAliases类型别名(为Java 类型设置一个短的名字)

常见别名
4.typeHandlers类型处理器
5.objectFactory对象工厂
6.plugin插件
7.environments环境
7.1 environment环境变量
7.1 environment环境变量

7.1.1 transactionManage事务管理器
类型一:JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作 用域。
类型二: MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生 命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因 此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。
7.1.2 dataSource 数据源
类型1:•UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
类型2:•POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
类型3:•JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置 数据源,然后放置一个 JNDI 上下文的引用。
8.databaseIdProvider
9. mapper映射器(加载映射配置文件)
加载方式
•使用相对于类路径的资源引用,例如: <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
•使用完全限定资源定位符(URL),例如: <mapper url="file:///var/mappers/AuthorMapper.xml"/>
•使用映射器接口实现类的完全限定类名,例如: <mapper class="org.mybatis.builder.AuthorMapper"/>
•将包内的映射器接口实现全部注册为映射器,例如: <package name="org.mybatis.builder"/>
映射配置文件mapper.xml
动态SQL
动态sql语句概述: Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动 态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。
条件判断<if test="condition"></if>
根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查 询,如果username 不为空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰 到。
循环拼接<foreach collection="遍历的集合" open="前部 如 id in(" close="后部 如:)" item="取出元素名 如id" separator="分隔符 如:,">#{取出元素名 如:id}</foreach>

SQL片段抽取

<include id="selectUser">
2.2.5复杂映射开发
一对一
1 创建Order和User实体

2 创建OrderMapper接口

3 配置OrderMapper.xml

一对多
1 创建Order和User实体
2 创建OrderMapper接口

3 配置OrderMapper.xml

多对多
1 创建Order和User实体

2 创建OrderMapper接口

3 配置OrderMapper.xml

2.2.6MyBatis注解开发
常见注解
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
注解实现复杂映射开发

一对一
1创建Order和User实体

2创建OrderMapper接口

3使用注解配置Mapper

一对多
1创建Order和User实体

2创建UserMappe接口

3使用注解配置Mapper

多对多
1创建Role和User实体

2创建UserMappe接口

3使用注解配置Mapper

2.2.7MyBatis缓存
缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁的与数据库 进行交互,进而提高响应速度
一级缓存
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数 据结构(HashMap)用于存储缓存数据。不同的sqlSession 之间的缓存数据区域(HashMap)是互相不影响的
 1、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据 库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。 2、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级 缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 3、 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直 接从 缓存中获取用户信息
缓存原理
 流程走到Perpetualcache中的clear()方法之后,会调用其cache.clear()方法,那 么这个 cache是什么东西呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是 本 地存放的一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何 时创建 的呢? 你觉得最有可能创建缓存的地方是哪里呢?我觉得是Executor,为什么这么认为?因为Executor是 执 行器,用来执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很 有可 能在Executor中,看了一圈发现Executor中有一个createCacheKey方法,这个方法很像是创 建缓存的 方法啊,跟进去看看,你发现createCacheKey方法是由BaseExecutor执行的,代码如下 创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个 update方法最终由updateList的list来把五个值存进去 
二级缓存
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession 可以共用二级缓存,二级缓存是跨SqlSession的
 二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去 缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也 就 是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相 同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域 中
开启配置及使用
1.和一级缓存默认开启不一样,二级缓存需要我们手动开启 首先在全局配置文件sqlMapConfig.xml文件中加入如下代码: <!--开启二级缓存--> <settings> <setting name="cacheEnabled" value="true"/> </settings>
2在UserMapper.xml文件中开启缓存 <!--开启二级缓存--> <cache></cache>
我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是 mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口 来自定义缓存。
3 开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操 作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取 这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口
4 useCache和flushCache
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓 存 的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出 sql 去查询,默认情况是true,即该sql使用二级缓存
 这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数 据 库中获取。 在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如 果不执行刷新缓存会出现脏读。 设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不 会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。  一般执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏 读。所以我们不用设置,默认即可
二级缓存整合redis
使用场景
分布式缓存

整合
实现cache接口开发,实现自己的缓存逻辑
1. 导入pom <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
2.配置文件 Mapper.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.lagou.mapper.IUserMapper"> <cache type="org.mybatis.caches.redis.RedisCache" /> <select id="findAll" resultType="com.lagou.pojo.User" useCache="true"> select * from user </select>
3.redis.properties redis.host=localhost redis.port=6379 redis.connectionTimeout=5000 redis.password= redis.database=0
源码分析:
1. 实现
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用 RedisCache的带有String参数的构造方法,即RedisCache(String id);
2 创建 RedisConfig 对象
在RedisCache的构造方法中, 调用了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。 RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的 属性:  并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用 RedisConfig类创建完成edisPool; 在RedisCache中实现了一个简单的模板方法,用来操作Redis: 
3 Cache中最重要的两个方法:putObject和getObject
 mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash 的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash 的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象的 序列化和反序列化;
Mybatis插件
作用场景
我们可基于MyBati s插件机制实现分页、分表,监控等功能
介绍
Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件 (Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩 展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦 截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态 代理实现的,换句话说,MyBatis中的四大对象都是代理对象
MyBatis所允许拦截的方法
执行器Executor (update、query、commit、rollback等方法);
SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
参数处理器ParameterHandler (getParameterObject、setParameters方法);
结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
Mybatis插件原理
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返
回 target 包装后的对象
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可 以
为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
拦截 实现举例,以ParameterHandler为例
内部插槽实现代码
 interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链 中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis 中的四大对象。返回的target是被重重代理后的对象
定义插件
拦截Executor的query方法, 
需将插件配置到sqlMapConfig.xm l中。
 MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待 准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创 建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后, MyBatis会通过JDK动态代理为实例生成代理类
2.5 Mybatis-Plus
介绍
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配 置,完美解决主键问题
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大 的 CRUD 操作
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同 于普通 List 查询
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、 Postgre、SQLServer 等多种数据库
内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢 查询
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误 操作
架构

2.4 设计模式
概览
子主题
Builder构建者模式
Builder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表 示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范 围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加 复杂的对象的构建,甚至只会构建产品的一个部分,直白来说,就是使用多个简单的对象一步一步构建 成一个复杂的对象
主要步骤:
1、将需要构建的目标类分成多个部件(电脑可以分为主机、显示器、键盘、音箱等部件);
2、 创建构建类;
3、 依次创建部件;
4、 将部件组装成目标对象
mybatis的应用
SqlSessionFactoryBuilder
环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的 MybatisMapConfig.xml 和所有的 *Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration 对 象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
工厂模式
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于创 建型模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建 其他类的实例,被创建的实例通常都具有共同的父类
步骤
1. 创建抽象产品类
2. 创建具体产品类
3. 创建工厂类
mybatis的应用
SqlSessionFactory
负责 SqlSession 的创建
Mybatis中执行Sql语句、获取Mappers、管理事务的核心接口SqlSession的创建过程使用到了工厂模 式。
代理模式
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式 的 英文叫做Proxy,它是一种对象结构型模式,
分类
静态代理
动态代理
步骤
创建一个抽象类,Person接口,使其拥有一个没有返回值的doSomething方法。
创建一个名为Bob的Person接口的实现类,使其实现doSomething方法
创建JDK动态代理类,使其实现InvocationHandler接口。拥有一个名为target的变量,并创建 getTa rget获取代理对象方法
Mybatis中实现
我们只需要编写Mapper.java接口,不需要实现, 当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又 会调用 mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理:
2.3 MyBatis源码解析
架构设计原理
架构

分层
(1) API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库。接口层一接收到 调用请求就会调用数据处理层来完成具体的数据处理。
MyBatis和数据库的交互有两种方式: a. 使用传统的MyBati s提供的API ; b. 使用Mapper代理的方式
(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根 据调用的请求完成一次数据库操作。
(3) 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是 共 用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑
主要构件及其相互关系
总体流程
(1) 加载配置并初始化
触发条件:加载配置文件 配置来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),—个是java代码中的 注 解,将主配置文件内容解析封装到Configuration,将sql的配置信息加载成为一个mappedstatement 对 象,存储在内存之中
(2) 接收调用请求
触发条件:调用Mybatis提供的API 传入参数:为SQL的ID和传入参数对象 处理过程:将请求传递给下层的请求处理层进行处理。
(3) 处理操作请求
触发条件:API接口层传递请求过来 传入参数:为SQL的ID和传入参数对象 处理过程: (A) 根据SQL的ID查找对应的MappedStatement对象。 (B) 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。 (C) 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。 (D) 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理 结果。 (E) 释放连接资源
(4) 返回处理结果
将最终的处理结果返回。
源码解析
初始化
将MyBatis的配置信息全部加载到内存中
起点:资源文件呢流读入 Inputstream inputstream = Resources.getResourceAsStream("mybatisconfig.xml"); //这一行代码正是初始化工作的开始。 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
configuration对象的初始化 MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用 org.apache.ibatis.session.Configuratio n 实例来维护
环境配置:框架执行和数据源配置
MappedStatement
SQL执行
SqlSession
是MyBatis中用于和数据库交互的顶层类,通常将它与ThreadLocal绑定,一个会话使用一 个 SqlSession,并且在使用完毕后需要close
重要成员
configuration
Executor执行器
Executor
常用实现类
BatchExecutor (重用语句并执行批量更新)
ReuseExecutor (重用预处理语句 prepared statements)
SimpleExecutor (普通的执行器,默认)
作用
(1、根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;
(2、为查询创建缓存,以提高性能
(3、创建JDBC的Statement连接对象,传递给*StatementHandler*对象,返回List查询结果。
执行步骤
获得 sqlSession
打开连接 SqlSession openSession()
executor.query()
//根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示 BoundSql boundSql = ms.getBoundSql(parameter);
//为本次查询创建缓存的Key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//执行数据库查询 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
//如果缓存中没有本次查找的值,那么从数据库中查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
//从数据库查询 //查询的方法 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
//将查询结果放入缓存 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); }
StatementHandler对象
主要工作
对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使用的是SQL语句字符串会包含若干个?占位符,我们其后再对占位符进行设值。StatementHandler通过parameterize(statement)方法对 S tatement 进行设值;
StatementHandler 通过 List query(Statement statement, ResultHandler resultHandler)方法来完成执行Statement,和将Statement对象返回的resultSet封装成List;
parameterize(statement)方法
参数设值
//使用ParameterHandler对象来完成对Statement的设值 parameterHandler.setParameters((PreparedStatement) statement);

ResultSetHandler 的 handleResultSets(Statement)

代理方式Mapper解析
当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删 改查 标签 封装成MappedStatement对象,存入mappedStatements中。(上文介绍了)当 判断解析到接口时,会 建此接口对应的MapperProxyFactory对象,存入HashMap中,key =接口的字节码对象,value =此接 口对应的MapperProxyFactory对象。
getmapper()
//从 MapperRegistry 中的 HashMap 中拿 MapperProxyFactory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFact
//通过动态代理工厂生成示例。 return mapperProxyFactory.newInstance(sqlSession);
//创建了 JDK动态代理的Handler类 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
mapperProxy -invoke()

execute方法
//判断mapper中的方法类型,最终调用的还是SqlSession中的方法 switch
case INSERT: { //转换参数 Object param = method.convertArgsToSqlCommandParam(args); //执行INSERT操作 // 转换 rowCount result = rowCountResult(sqlSession.insert(command.getName(), param)); break; }
case UPDATE: { //转换参数 Object param = method.convertArgsToSqlCommandParam(args);// 转换 rowCount result = rowCountResult(sqlSession.update(command.getName(), param)); break; }
case DELETE: { //转换参数 Object param = method.convertArgsToSqlCommandParam(args); // 转换 rowCount result = rowCountResult(sqlSession.delete(command.getName(), param)); break; }
case SELECT: //无返回,并且有ResultHandler方法参数,则将查询的结果,提交给 ResultHandler 进行 处理 if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; //执行查询,返回列表 } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); //执行查询,返回Map } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); //执行查询,返回Cursor } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); //执行查询,返回单个对象 } else { //转换参数 Object param = method.convertArgsToSqlCommandParam(args); //查询单条 result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break;
返回结果为null,并且返回类型为基本类型,则抛出BindingException异常
二级缓存
MyBatis二级缓存只适用于不常进行增、删、改的数据,比如国家行政区省市区街道数据。一但数据变 更,MyBatis会清空缓存。因此二级缓存不适用于经常进行更新的数据。
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的 装饰器
二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
二级缓存具有丰富的缓存策略。
二级缓存可由多个装饰器,与基础缓存组合而成
二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache完成。
延迟加载源码剖析:
什么是延迟加载?
在开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我 们所说的延迟加载
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
优点
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表 速度要快。
缺点
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时 间,所以可能造成用户等待时间变长,造成用户体验下降。
在多表中:
一对多,多对多:通常情况下采用延迟加载
一对一(多对一):通常情况下采用立即加载
注意:
延迟加载是基于嵌套查询来实现的
实现
局部延迟加载
在association和collection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策 略。
<!-- 开启一对多 延迟加载 --> <resultMap id="userMap" type="user"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="password" property="password"></result> <result column="birthday" property="birthday"></result> <!-- fetchType="lazy" 懒加载策略 fetchType="eager" 立即加载策略 -->全局延迟加载 在Mybatis的核心配置文件中可以使用setting标签修改全局的加载策略。 注意 7.。 延迟加载原理实现 它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。当调用代理对象的延迟加载属 性的 getting 方法时,进入拦截器方法。比如调用 a.getB().getName() 方法,进入拦截器的 invoke(...) 方法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对 象的 SQL ,把 B 查询上来,然后调用 a.setB(b) 方法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理 总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。 <collection property="orderList" ofType="order" column="id" select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy"> </collection> </resultMap> <select id="findAll" resultMap="userMap"> SELECT * FROM `user` </select>
全局延迟加载
在Mybatis的核心配置文件中可以使用setting标签修改全局的加载策略
<settings> <!--开启全局延迟加载功能--> <setting name="lazyLoadingEnabled" value="true"/> </settings>
注意
关闭一对一 延迟加载
<resultMap id="orderMap" type="order"> <id column="id" property="id"></id> <result column="ordertime" property="ordertime"></result> <result column="total" property="total"></result> <!-- fetchType="lazy" 懒加载策略 fetchType="eager" 立即加载策略 --> <association property="user" column="uid" javaType="user" select="com.lagou.dao.UserMapper.findById" fetchType="eager"> </association> </resultMap> <select id="findAll" resultMap="orderMap"> SELECT * from orders </select>
原理实现
使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。
当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法。
比如调用 a.getB().getName() 方法,进入拦截器的 invoke(...) 方法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对 象的 SQL ,把 B 查询上来,然后调用 a.setB(b) 方法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。
总结:
延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。
延迟加载主要使用技术
Javassist,Cglib
源码
Setting 配置加载:
Configuration
/** aggressiveLazyLoading: * 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考 lazyLoadTriggerMethods). * 默认为true * */ protected boolean aggressiveLazyLoading; /** * 延迟加载触发方法 */ protected Set<String> lazyLoadTriggerMethods = new HashSet<String> (Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" })); /** 是否开启延迟加载 */ protected boolean lazyLoadingEnabled = false; /** * 默认使用Javassist代理工厂 * @param proxyFactory */ public void setProxyFactory(ProxyFactory proxyFactory) { if (proxyFactory == null) { proxyFactory = new JavassistProxyFactory(); } this.proxyFactory = proxyFactory; }
延迟加载代理对象创建
Mybatis的查询结果是由ResultSetHandler接口的handleResultSets()方法处理的。
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { this.useConstructorMappings = false; // reset previous mapping result final List constructorArgTypes = new ArrayList(); final List Object constructorArgs = new ArrayList<Object>(); //#mark 创建返回的结果映射的真实对象 Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix); if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {默认采用javassistProxy进行代理对象的创建 JavasisstProxyFactory实现 final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { // 判断属性有没配置嵌套查询,如果有就创建代理对象 if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { //#mark 创建延迟加载代理对象 resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); break; } } } this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result return resultObject;
JavasisstProxyFactory实现