mybatis相关知识讲解
1. mybatis相关概念
1.1 对象/关系数据库映射(ORM)
ORM全称Object/Relation Mapping:表示对象–关系映射的缩写。
ORM完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员既可以利用面向对象程序设计语言的简单易用性,又可以利用关系数据库的技术优势。
ORM把关系数据库包装成面向对象的模型。
ORM框架是面向对象设计语言与关系数据库发展不同步时的中间解决方案。采用ORM框架后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象,而ORM框架则将这些面向对象的操作转换成底层SQL操作。ORM框架实现的效果:把对持久化对象的保存、修改、删除 等操作,转换为对数据库的操作。
1.2 mybatis简介
MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解来配置和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)为数据库中的记录。
1.3 mybatis的优势
Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编码进行分离,功能边界清晰,一个专注业务,一个专注数据。
分析图示如下:
1.4 MyBatis的映射文件概述
1.5 MyBatis入门核心配置文件SqlMapConfig.xml
配置文件层级关系
- configuration 配置
- properties 属性
- setting 设置
- typeAllases 类型别名
- typeHandler 类型处理器
- objectFactory 对象工厂
- plugins 插件
- environments 环境
- environment 环境变量
- transactionManager 事务管理器
- dataSource 数据源
- environment 环境变量
- databaseIdProvider 数据库厂商识别
- mappers 映射器
1.5.1 MyBatis常用配置解析
1.5.1.1 environments标签
数据库环境配置,支持多环境配置
事务管理器(transactionManager)类型有两种:
- JDBC: 这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
- MANAGED: 这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。
数据源(dataSource)类型有三种:
- UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
- POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
- JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。
1.5.1.2 mappers标签
该标签的作用是加载映射的,加载方式如下:
•使用相对于类路径的资源引用,例如:
<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"/>
1.5.1.3 Properties标签
实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的properties文件
1.5.1.4 typeAliases标签
类型别名是为Java 类型设置一个短的名字。原来的类型名称配置如下:
<select id="findAll" resultType="com.lagou.domain.User">
select * from user
</select>
配置typeAliases,为com.lagou.domain.User定义别名为user,修改后配置如下:
<typeAliases>
<typeAlias type="com.lagou.domain.User" alias="user"></typeAlias>
</typeAliases>
<select id="findAll" resultType="user">
select * from user
</select>
上面我们是自定义的别名,mybatis框架已经为我们设置好的一些常用的类型的别名:
别名 | 数据类型 |
---|---|
string | String |
long | Long |
int | Integer |
double | Double |
boolean | Boolean |
… | … |
1.6 MyBatis映射配置文件mapper.xml及动态sql
动态sql语句概述:
Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。
常用的动态标签有:if,choose (when, otherwise),trim (where, set),foreach
如下:
1.6.1 where,if
// 在 id如果不为空时可以根据id查询,如果username 不同空时还要加入用户名作为条件
// 如果只存在id一个条件,则where标签可以自动去除是“AND”或“OR”开头的sql中的“AND”或“OR”关键字
<select id="findByCondition" parameterType="user" resultType="user">
select * from User
<where>
<if test="id!=0">
and id=#{id}
</if>
<if test="username!=null">
and username=#{username}
</if>
</where>
</select>
1.6.2 choose (when, otherwise)
// choose(when,otherwise)标签相当于switch(case,default) ,如下例,
// 若title 不为空,执行when标签里的代码;否则,默认执行otherwise标签里面的代码。
<select id="queryBy" resultType="Blog">
SELECT * FROM BLOG WHERE 1=1
<choose>
<when test="title != null">
AND title like #{title}
</when>
<otherwise>
AND id= 1
</otherwise>
</choose>
</select>
1.6.3 trim (where, set)
// 如果 where 元素没有按正常套路出牌,我们还是可以通过自定义 trim 元素来定制sql,实现where标签的效果
<select id="queryBy" resultType="com.scme.pojo.User" parameterType="com.scme.pojo.User">
select * from user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username!=null and password!=null">
and username=#{username} and password=#{password}
</if>
</trim>
<!-- 效果同上
<where>
<if test="username!=null and password!=null">
and username=#{username} and password=#{password}
</if>
</where> -->
</select>
// set标签功能和where标签差不多,set标签代替了sql中set关键字,set标签可以自动去除sql中的多余的“,”
<update id="updateUser" parameterType="com.scme.pojo.User">
update user
<set>
<if test="username!=null">
username=#{username}
</if>
</set>
<where>
<if test="id!=null">
id=#{id}
</if>
</where>
</update>
1.6.4 foreach
// 循环执行sql的拼接操作: select * from User where id in (?,?)
<select id="findByIds" parameterType="list" resultType="user">
select * from User
<where>
<foreach collection="list" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
foreach标签的属性含义如下:
标签用于遍历集合,它的属性:
•collection:代表要遍历的集合元素,注意编写时不要写#{}
•open:代表语句的开始部分
•close:代表结束部分
•item:代表遍历集合的每个元素,生成的变量名
•sperator:代表分隔符
1.7 MyBatis相应API介绍
1.7.1 SqlSession工厂构建器SqlSessionFactoryBuilder
常用API:SqlSessionFactory build(InputStream inputStream)
通过加载mybatis的核心文件输入流的形式构建一个SqlSessionFactory对象
String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
其中,Resources 工具类,这个类在 org.apache.ibatis.io 包中。Resources 类可以帮助从类路径下,文件系统或一个 web URL 中加载资源文件。
1.7.2 SqlSessio n工厂对象 SqlSessionFactory
SqlSessionFactory 有对个方法创建 SqlSession 实例。通常有以下2个:
- openSession() : 会默认开启一个事务,但事务不会自动提交,也就意味着需要手动提交该事务,更新操作数据才会持久化到数据库中
- openSession(boolean autoCommit) : 参数为是否自动提交,如果设置为true,那么不需要手动提交事务
1.7.3 SqlSession会话对象
SqlSession 实例在 Mybatis 中是非常强大的一个类。在这里可以看到所有执行语句,提交或回滚,获取映射器的方法。
- 执行语句的主要方法有:
<T> T selectOne(String statement, Object parameter) <E> List<E> selectList(String statement, Object parameter) int insert(String statement, Object parameter) int update(String statement, Object parameter) int delete(String statement, Object parameter)
- 操作事务的方法主要有:
void commit() void rollback()
1.8 MyBatis的Dao层实现
采用Mybatis的代理开发方式实现Dao层的开发,这种方式是我们企业开发的主流。
Mapper接口开发方法只需要程序编写Mapper接口(相当于Dao接口),由Mybatis框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上班Dao接口实现类方法。
Mapper接口开发需要遵循以下规范:
- Mapper.xml文件的namespace与mapper接口的全限定类名相同
- Mapper接口方法名和Mapper.xml中定义的每个statement的id相同
- Mapper接口方法输入的参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
- Mapper接口方法输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
结构如图:
2. Mybatis缓存
2.1 一级缓存
mysql一级缓存为SqlSession级别的,原理如图:
我们可以查看代码中一级缓存的sql执行情况:
-
在一个sqlSession中,对User表根据id进行两次查询,查看他们发出sql语句的情况
@Test public void test1(){ //根据 sqlSessionFactory 产生 session SqlSession sqlSession = sessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //第一次查询,发出sql语句,并将查询出来的结果放进缓存中 User u1 = userMapper.selectUserByUserId(1); System.out.println(u1); //第二次查询,由于是同一个sqlSession,会在缓存中查询结果 //如果有,则直接从缓存中取出来,不和数据库进行交互 User u2 = userMapper.selectUserByUserId(1); System.out.println(u2); sqlSession.close(); }
控制台打印情况:
-
同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。
@Test public void test2(){ //根据 sqlSessionFactory 产生 session SqlSession sqlSession = sessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //第一次查询,发出sql语句,并将查询的结果放入缓存中 User u1 = userMapper.selectUserByUserId( 1 ); System.out.println(u1); //第二步进行了一次更新操作,sqlSession.commit() u1.setSex("女"); userMapper.updateUserByUserId(u1); sqlSession.commit(); //第二次查询,由于是同一个sqlSession.commit(),会清空缓存信息 //则此次查询也会发出sql语句 User u2 = userMapper.selectUserByUserId(1); System.out.println(u2); sqlSession.close(); }
控制台打印情况:
-
总结
a、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
b、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
c、 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直 接从缓存中获取用户信息
2.1.1 一级缓存原理探究
-
一级缓存是什么
提到一级缓存绕不开SqlSession,所以我们直接从SqlSession入手,看看有没有创建缓存或者与缓存有关的属性或者方法:
研究了上述方法,只有有clearCache()和缓存沾点关系,我们可以从这个方法入手,分析源码。
根据对源码的分析,我们可以得到如下流程图:
再深入分析,流程走到Perpetualcache中的clear()方法之后,会调用其cache.clear()方法,点进去会发现,cache其实就是是private Map cache = new HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear()。
也就是说,缓存其实就是本地存放的一个map对象,每一个SqISession都会存放一个map对象的引用 -
一级缓存的cache是什么时候创建的
得最有可能创建缓存的地方是Executor,因为Executor是执行器,用来执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很有可能在Executor中。
看了一圈发现Executor中有一个createCacheKey方法,跟进去看看,你发现createCacheKey方法是由BaseExecutor执行的,代码如下:CacheKey cacheKey = new CacheKey(); //MappedStatement 的 id // id就是Sql语句的所在位置包名+类名+ SQL名称 cacheKey.update(ms.getId()); // offset 就是 0 cacheKey.update(rowBounds.getOffset()); // limit 就是 Integer.MAXVALUE cacheKey.update(rowBounds.getLimit()); //具体的SQL语句 cacheKey.update(boundSql.getSql()); //后面是update 了 sql中带的参数 cacheKey.update(value); ... if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); }
创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个update方法最终由updateList的list来把五个值存进去,对照上面的代码和下面的图示,应该能理解这五个值都是什么了:
这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis-config.xml中的标签,见如下:<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments>
-
一级缓存的工作流程是怎样的
经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存。我们来看一下这个缓存到底用在哪了,我们跟踪到query方法如下:Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); //创建缓存 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } @SuppressWarnings("unchecked") Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ... list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //这个主要是处理存储过程用的。 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key,boundSql); } ... } // queryFromDatabase 方法 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。localcache对象的put方法最终交给Map进行存放
private Map<Object, Object> cache = new HashMap<Object, Object>(); @Override public void putObject(Object key, Object value) { cache.put(key, value); }
2.2 二级缓存
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也 就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中
二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存,若二级缓存未命中,再去查询一级缓存,一级缓存没有,再查询数据库。
缓存查询顺序: 二级缓存------》 一级缓存------》数据库
与一级缓存不同,二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache,相同Mapper中的MappedStatement共用一个Cache,一级缓存则是和 SqlSession 绑定。
如何使用二级缓存
-
开启二级缓存
和一级缓存默认开启不一样,二级缓存需要我们手动开启。
首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:<!--开启二级缓存--> <settings> <setting name="cacheEnabled" value="true"/> </settings>
其次,在UserMapper.xml文件中开启缓存:
<!--开启二级缓存--> <cache></cache>
我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。
我们可以看到二级缓存底层还是HashMap结构:public class PerpetualCache implements Cache { private final String id; private MapcObject, Object> cache = new HashMapC); public PerpetualCache(St ring id) { this.id = id; } }
开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口
public class User implements Serializable( //用户ID private int id; //用户姓名 private String username; //用户性别 private String sex; }
-
测试二级缓存:
a. 测试二级缓存和sqlSession无关:@Test public void testTwoCache(){ //根据 sqlSessionFactory 产生 session SqlSession sqlSession1 = sessionFactory.openSession(); SqlSession sqlSession2 = sessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class ); //第一次查询,发出sql语句,并将查询的结果放入缓存中 User u1 = userMapper1.selectUserByUserId(1); System.out.println(u1); sqlSession1.close(); //第一次查询完后关闭 sqlSession //第二次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句 User u2 = userMapper2.selectUserByUserId(1); System.out.println(u2); sqlSession2.close(); }
b. 测试执行commit()操作,二级缓存数据清空
@Test public void testTwoCache(){ //根据 sqlSessionFactory 产生 session SqlSession sqlSession1 = sessionFactory.openSession(); SqlSession sqlSession2 = sessionFactory.openSession(); SqlSession sqlSession3 = sessionFactory.openSession(); String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ; UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class ); UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class ); //第一次查询,发出sql语句,并将查询的结果放入缓存中 User u1 = userMapperl.selectUserByUserId( 1 ); System.out.println(u1); sqlSessionl .close(); //第一次查询完后关闭sqlSession //执行更新操作,commit() u1.setUsername( "aaa" ); userMapper3.updateUserByUserId(u1); sqlSession3.commit(); //第二次查询,由于上次更新操作,缓存数据已经清空(防止数据脏读),这里必须再次发出sql语 User u2 = userMapper2.selectUserByUserId( 1 ); System.out.println(u2); sqlSession2.close(); }
控制台打印情况:
-
useCache和flushCache
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出 sql去查询,默认情况是true,即该sql使用二级缓存<select id="selectUserByUserId" useCache="false" resultType="com.lagou.pojo.User" parameterType="int"> select * from user where id=#{id} </select>
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数据库中获取。
在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存如果不执行刷新缓存会出现脏读。
设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。<select id="selectUserByUserId" flushCache="true" useCache="false" resultType="com.lagou.pojo.User" parameterType="int"> select * from user where id=#{id} </select>
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可
3 Mybatis插件
3.1 插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能
3.2 Mybatis插件介绍
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等方法);
3.3 Mybatis插件原理
在四大对象创建的时候:
- 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
- 获取到所有的Interceptor (拦截器)(插件需要实现的接口),调用interceptor.plugin(target),返回target包装后的对象
- 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
拦截:
插件具体是如何拦截并附加额外的功能的,我们可以以ParameterHandler来说:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是Mybatis初始化的时候创建的。调用拦截器链中的拦截器一次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象
如果我们想要拦截Executor的query方法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
除此之外,我们还需要将插件配置到sqlMapConfig.xm l中:
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
</plugin>
</plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执行
4. Mybatis架构原理
4.1 架构设计
我们把Mybatis的功能架构分为3层:
- API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接到调用请求就好调用数据处理层来完成具体的数据处理。
Mybatis和数据库的交互有2种方式:1). 使用传统的Mybatis提供API 2). 使用Mapper代理方式
- 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果的映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
- 数据基础层:负责最基础的功能支持,包括连接管理、事务管理、配置加载、缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件,为上层的数据处理层提供最基础的支撑
4.2 主要构件及其相互关系
构件 | 描述 |
---|---|
SqlSession | 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 |
Executor | MyBatis执行器,是MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护 |
StatementHandler | 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数、将Statement结果集转换成List集合。 |
ParameterHandler | 负责对用户传递的参数转换成JDBC Statement所需要的参数 |
ResultSetHandler | 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合 |
TypeHandler | 负责java数据类型和jdbc数据类型之间的映射和转换 |
MappedStatement | MappedStatement维护了一条<select |
SqlSource | 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回 |
BoundSql | 表示动态生成的SQL语句以及相应的参数信息 |
4.3 总体流程
- 加载配置并初始化
触发条件:加载配置文件
配置来源于2个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),一个是java代码中的注解,将主配置文件内容解析封装到Configuration、将SQL的配置信息加载成一个mappedstatement对象,存储在内存中 - 接收调用请求
触发条件: 调用mybatis提供的API
传入参数: SQL的ID和传入参数对象
处理过程: 将请求传递给下层的请求处理层进行处理 - 处理操作请求
触发条件: API接口层传递请求过来
传入参数: SQL的ID和传入参数对象
处理过程:(A). 根据SQL的ID查找对应的MappedStatement对象。 (B). 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。 (C). 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。 (D). 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。 (E). 释放连接资源。
- 返回处理结果
将最终的处理结果返回。
5. Mybatis中用到的设计模式
虽然我们都知道有3类23种设计模式,但是大多停留在概念层面,Mybatis源码中使用了大量的设计模式,观察设计模式在其中的应用,能够更深入的理解设计模式
Mybati s至少用到了以下的设计模式的使用:
模式 | mybatis 体现 |
---|---|
Builder 模式 | 例如SqlSessionFactoryBuilder、Environment |
工厂方法模式 | 例如SqlSessionFactory、TransactionFactory、LogFactory |
单例模式 | 例如 ErrorContext 和 LogFactory |
代理模式 | Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理。还有executor.loader包使用了 cglib或者javassist达到延迟加载的效果 |
组合模式 | 例如SqlNode和各个子类ChooseSqlNode等 |
模板方法模式 | 例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的子类例如IntegerTypeHandler |
适配器模式 | 例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现 |
装饰者模式 | 例如Cache包中的cache.decorators子包中等各个装饰者的实现 |
迭代器模式 | 例如迭代器模式PropertyTokenizer |
接下来对Builder构建者模式、工厂模式、代理模式进行解读,先介绍模式自身的知识,然后解读在Mybatis中怎样应用了该模式:
5.1 Builder构建者模式
Builder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分,直白来说,就是使用多个简单的对象一步一步构建成一个复杂的对象
例子:使用构建者设计模式来生产computer
主要步骤:
1). 将需要构建的目标类分成多个部件(电脑可以分为主机、显示器、键盘、音箱等部件);
2). 创建构建类;
3). 依次创建部件;
4). 将部件组装成目标对象
- 定义computer
package com.lagou.dao; import org.apache.ibatis.binding.BindingException; import org.apache.ibatis.session.SqlSession; import java.util.Optional; public class Computer { private String displayer; private String mainUnit; private String mouse; private String keyboard; public String getDisplayer() { return displayer; } public void setDisplayer(String displayer) { this.displayer = displayer; } // ...........所有get、set方法................. @Override public String toString() { return "Computer{" + "displayer='" + displayer + '\'' + ", mainUnit='" + mainUnit + '\'' + ", mouse='" + mouse + '\'' + ", keyboard='" + keyboard + '\'' + '}'; } }
- 构建者类 ComputerBuilder
public static class ComputerBuilder { private ComputerBuilder target = new ComputerBuilder(); public Builder installDisplayer(String displayer) { target.setDisplayer(displayer); return this; } public Builder installMainUnit(String mainUnit) { target.setMainUnit(mainUnit); return this; } public Builder installMouse(String mouse) { target.setMouse(mouse); return this; } public Builder installKeybord(String keyboard) { target.setKeyboard(keyboard); return this; } public ComputerBuilder build() { return target; } }
- 调用创建组件,组装目标对象
public static void main(String[]args){ ComputerBuilder computerBuilder=new ComputerBuilder(); // 创建组件 computerBuilder.installDisplayer("显万器"); computerBuilder.installMainUnit("主机"); computerBuilder.installKeybord("键盘"); computerBuilder.installMouse("鼠标"); // 组装成目标对象 Computer computer=computerBuilder.Builder(); System.out.println(computer); }
Mybatis中Builder构建者模式的体现:
- SqlSessionFactory 的构建过程:
Mybatis的初始化工作非常复杂,不是只用一个构造函数就能搞定的。所以使用了建造者模式,使用了大量的Builder,进行分层构造,核心对象Configuration使用了 XmlConfigBuilder来进行构造。
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xml 和所有的 *Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
其中 XMLConfigBuilder 在构建 Configuration 对象时,也会调用 XMLMapperBuilder 用于读取*Mapper 文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析<properties />标签 propertiesElement(root.evalNode("properties")); // 解析 <settings /> 标签 Properties settings = settingsAsProperties(root.evalNode("settings")); //加载自定义的VFS实现类 loadCustomVfs(settings); // 解析 <typeAliases /> 标签 typeAliasesElement(root.evalNode("typeAliases")); //解析<plugins />标签 pluginElement(root.evalNode("plugins")); // 解析 <objectFactory /> 标签 objectFactoryElement(root.evaINode("obj ectFactory")); // 解析 <objectWrapper Factory /> 标签 obj ectWrappe rFacto ryElement(root.evalNode("objectWrapperFactory")); // 解析 <reflectorFactory /> 标签 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 赋值 <settings /> 到 Configuration 属性 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 解析 <environments /> 标签 environmentsElement(root.evalNode("environments")); // 解析 <databaseIdProvider /> 标签 databaseldProviderElement(root.evalNode("databaseldProvider")); } }
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了 Builder模式来解决。//解析<mappers />标签 mapperElement(root.evalNode("mappers"));
SqlSessionFactoryBuilder类根据不同的输入参数来构建SqlSessionFactory这个工厂对象
5.2 工厂模式
在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于创建型模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
例子:生产电脑
假设有一个电脑的代工生产商,它目前已经可以代工生产联想电脑了,随着业务的拓展,这个代工生产商还要生产惠普的电脑,我们就需要用一个单独的类来专门生产电脑,这就用到了简单工厂模式。
下面我们来实现简单工厂模式:
- 创建抽象产品类
我们创建一个电脑的抽象产品类,他有一个抽象方法用于启动电脑:public abstract class Computer { //产品的抽象方法,由具体的产品类去实现 public abstract void start(); }
- 创建具体产品类
接着我们创建各个品牌的电脑,他们都继承了他们的父类Computer,并实现了父类的start方法:public class LenovoComputer extends Computer{ @Override public void start() { System.out.println("联想电脑启动"); } }
public class HpComputer extends Computer{ @Override public void start() { System.out.println("惠普电脑启动"); } }
- 创建工厂类
接下来创建一个工厂类,它提供了一个静态方法法createComputer用来生产电脑。你只需要传入你想生 产的电脑的品牌,它就会实例化相应品牌的电脑对象:import org.junit.runner.Computer; public class ComputerFactory { public static Computer createComputer(String type){ Computer mComputer=null; switch (type) { case "lenovo": mComputer=new LenovoComputer(); break; case "hp": mComputer=new HpComputer(); break; } return mComputer; } }
- 客户端调用工厂类
客户端调用工厂类,传入“hp”生产出惠普电脑并调用该电脑对象的start方法:public class CreatComputer { public static void main(String[]args){ ComputerFactory.createComputer("hp").start(); } }
Mybatis中工厂模式的体现
- SqlSessionFactory
Mybatis中执行Sql语句、获取Mappers、管理事务的核心接口SqlSession的创建过程使用到了工厂模式。
有一个 SqlSessionFactory 来负责 SqlSession 的创建:
可以看到,该Factory的openSession ()方法重载了很多个,分别支持autoCommit、Executor、Transaction等参数的输入,来构建核心的SqlSession对象。
在DefaultSqlSessionFactory的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品:
private SqlSession openSessionFromDataSource(ExecutorType execType,
TransactionIsolationLevel level,boolean autoCommit){
Transaction tx=null;
try{
final Environment environment=configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx=transactionFactory.newTransaction(environment.getDataSource(),level,autoCommit);
//根据参数创建制定类型的Executor
final Executor executor=configuration.newExecutor(tx,execType);
//返回的是 DefaultSqlSession
return new DefaultSqlSession(configuration,executor,autoCommit);
}catch(Exception e){
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: "+ e,e);
}finally{
ErrorContext.instance().reset();
}
}
这是一个openSession调用的底层方法,该方法先从configuration读取对应的环境配置,然后初始化TransactionFactory 获得一个 Transaction 对象,然后通过 Transaction 获取一个 Executor 对象,最后通过configuration、Executor、是否autoCommit三个参数构建了 SqlSession
5.3 代理模式
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy,它是一种对象结构型模式,代理模式分为静态代理和动态代理,我们来介绍动态代理
举例:
- 创建一个抽象类,Person接口,使其拥有一个没有返回值的doSomething方法
// 抽象类 人 public interface Person { void doSomething(); }
- 创建一个名为Bob的Person接口的实现类,使其实现doSomething方法
// 创建一个名为Bob的人的实现类 public class Bob implements Person { public void doSomething() { System.out.println("Bob doing something!"); } }
- 创建JDK动态代理类,使其实现InvocationHandler接口。拥有一个名为target的变量,并创建getTa rget获取代理对象方法
/** * JDK动态代理 * 需实现 InvocationHandler 接口 */ public class JDKDynamicProxy implements InvocationHandler { //被代理的对象 Person target; // JDKDynamicProxy 构造函数 public JDKDynamicProxy(Person person) { this.target = person; } //获取代理对象 public Person getTarget() { return (Person)Proxy.newProxylnstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } //动态代理invoke方法 public Person invoke(Object proxy, Method method, Object[] args) throws Throwable { //被代理方法前执行 System.out.println("JDKDynamicProxy do something before!"); //执行被代理的方法 Person result = (Person) method.invoke(target, args); //被代理方法后执行 System.out.println("JDKDynamicProxy do something after!"); return result; } }
- 创建JDK动态代理测试类J DKDynamicTest
// JDK动态代理测试 public class JDKDynamicTest { public static void main(String[] args) { System.out.println("不使用代理类,调用doSomething方法。"); //不使用代理类 Person person = new Bob(); // 调用 doSomething 方法 person.doSomething(); System.out.println("分割线-----------"); System.out.println("使用代理类,调用doSomething方法。"); //获取代理类 Person proxyPerson = new JDKDynamicProxy(new Bob()).getTarget(); // 调用 doSomething 方法 proxyPerson.doSomething(); } }
Mybatis中代理模式的体现
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由Mybati s后台帮我们完成具体SQL的执行。
当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又
会调用 mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new
ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession,
mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
在这里,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用TnewInstance(MapperProxy mapperProxy)生成代理对象然后返回。而查看MapperProxy的代码,可以看到如下内容:
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
非常典型的,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成 SQL 的执行和返回