一、Mybatis相关概念
1.对象/关系数据库映射(ORM)
ORM全称Object/Relation Mapping:表示对象-关系映射的缩写
ORM完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员既可以利用面向
对象程序设计语言的简单易用性,又可以利用关系数据库的技术优势。ORM把关系数据库包装成面向对
象的模型。ORM框架是面向对象设计语言与关系数据库发展不同步时的中间解决方案。采用ORM框架
后,应用程序不再直接访问底层数据库,而是以面向对象的放松来操作持久化对象,而ORM框架则将这
些面向对象的操作转换成底层SQL操作。ORM框架实现的效果:把对持久化对象的保存、修改、删除 等
操作,转换为对数据库的操作
2.Mybatis简介
MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架,它支持定制化SQL、存储过程以及高级映
射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的
XML或注解来配置和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)
为数据库中的记录。
3.Mybatis历史
原是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移到了google code,随着开发
团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11月迁移到Github。iBATIS一词来源于
“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access
Objects(DAO)
4.Mybatis优势
Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编
码进行分离,功能边界清晰,一个专注业务,一个专注数据。
分析图示如下:
二、Mybatis基本应用
1.MyBatis的映射文件概述
2.MyBatis核心配置文件层级关系
3.MyBatis常用配置解析
Environments标签:
事务管理器(transactionManager)类型有两种:
•JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
•MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生
命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因
此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。
其中,数据源(dataSource)类型有三种:
•UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
•POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
•JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置
数据源,然后放置一个 JNDI 上下文的引用。
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"/>
Mybatis相应API介绍:
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 中加载资源文件。
2、SqlSession工厂对象SqlSessionFactory
SqlSessionFactory 有多个个方法创建SqlSession 实例。常用的有如下两个:
openSession() 会默认开启一个事务,但事务不会自动提交,也就意味着需要手动提交该事务,更新操作数据
才会持久化到数据库中
openSession(boolean autoCommit)参数为是否自动提交,如果设置为true,那么不需要手动提交事务。
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()
Mybatis的Dao层实现
1、传统开发方式
编写UserDao接口,编写UserDaoImpl实现
2、代理开发方式
采用 Mybatis 的代理开发方式实现 DAO 层的开发,这种方式是我们后面进入企业的主流。
Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口
定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。
Mapper 接口开发需要遵循以下规范:
1) Mapper.xml文件中的namespace与mapper接口的全限定名相同
2) Mapper接口方法名和Mapper.xml中定义的每个statement的id相同
3) Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
4) Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
三、Mybatis配置文件深入
Properties标签
实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的
properties文件
typeAliases标签
类型别名是为Java 类型设置一个短的名字。原来的类型名称配置如下(附件1)
配置typeAliases,为com.lagou.domain.User定义别名为user(附件2)
上面我们是自定义的别名,mybatis框架已经为我们设置好的一些常用的类型的别名(附件3)
映射配置文件mapper.xml
动态sql语句
1、动态sql语句概述:
Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动
态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。
2、动态 SQL 之
我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查
询,如果username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰
到。
<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>
3、动态 SQL 之
循环执行sql的拼接操作,例如:SELECT * FROM USER WHERE id IN (1,2,5)。
<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:代表分隔符
4、SQL片段抽取
Sql 中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的
<!--抽取sql片段简化编写-->
<sql id="selectUser" select * from User</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"></include>
where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
四、Mybatis复杂映射开发
1、一对多查询
1、一对多查询的模型:
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户
一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单
2、 一对多查询的语句
对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;
3、配置UserMapper.xml
<mapper namespace="com.lagou.mapper.UserMapper">
<resultMap id="userMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="orderList" ofType="com.lagou.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
</mapper>
4、实体
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪一个客户
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前用户具备哪些订单
private List<Order> orderList;
}
2、多对多查询
1、多对多查询的模型:
用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用
多对多查询的需求:查询用户同时查询出该用户的所有角色
2、多对多查询的语句
对应的sql语句:select u.,r.,r.id rid from user u left join user_role ur on u.id=ur.user_id
inner join role r on ur.role_id=r.id;
3、实体
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前用户具备哪些订单
private List<Order> orderList;
//代表当前用户具备哪些角色
private List<Role> roleList;
}
public class Role {
private int id;
private String rolename;
}
4、配置UserMapper.xml
<resultMap id="userRoleMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="roleList" ofType="com.lagou.domain.Role">
<result column="rid" property="id"></result>
<result column="rolename" property="rolename"></result>
</collection>
</resultMap>
<select id="findAllUserAndRole" resultMap="userRoleMap">
select u.*,r.*,r.id rid from user u left join user_role ur on u.id=ur.user_id inner join role r on ur.role_id=r.id
</select>
3、知识小结
MyBatis多表配置方式:
一对多配置:使用+做配置
多对多配置:使用+做配置
五、Mybatis缓存
1、一级缓存
在一个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();
}
查看控制台打印情况:
2、总结
1、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据
库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
2、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级
缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
3、 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直 接从
缓存中获取用户信息
3、一级缓存原理探究与源码分析
一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?相信你现在应该会有
这几个疑问。
大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开SqlSession,所以索性我们 就
直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法
调研了一圈,发现上述所有方法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方 法入手吧,分析源码
时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,你才 会对这个类有更深的认识,分析了
一圈,你可能会得到如下这个流程图
再深入分析,流程走到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执行的,代码如下
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、二级缓存
1、二级缓存原理
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去
缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也 就
是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相
同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域 中
2、开启二级缓存
和一级缓存默认开启不一样,二级缓存需要我们手动开启
首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:
<!--开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在UserMapper.xml文件中开启缓存
<!--开启二级缓存-->
<cache></cache>
我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是
mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口 来
自定义缓存。
public class PerpetualCache implements Cache {
private final String id;
private MapcObject, Object> cache = new HashMapC);
public PerpetualCache(St ring id) { this.id = id; }
我们可以看到二级缓存底层还是HashMap结构
public class User implements Serializable(
//用户ID
private int id;
//用户姓名
private String username;
//用户性别
private String sex;
}
开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操
作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取
这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口
测试:
1、测试二级缓存和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();
}
可以看出上面两个不同的sqlSession,第一个关闭了,第二次查询依然不发出sql查询语句
2、测试执行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(); }
查看控制台情况:
3、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表示刷新缓存,这样可以避免数据库脏读。
所以我们不用设置,默认即可
4、二级缓存整合redis
上面我们介绍了 mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么
什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了 1服务器,查询后的缓 存就会
放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个 缓存,
如下图所示:
为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数
据都往它那里存,取缓存数据也从它那里取,如下图所示:
如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中, 然后无论
有多少台服务器,我们都能从缓存中获取数据。
这里我们介绍mybatis与redis的整合。
刚刚提到过,mybatis提供了一个eache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。
mybati s本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。
redis分布式缓存就可以,mybatis提供了一个针对cache接口的redis实现类,该类存在mybatis-redis包 中
实现:
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
5、源码分析:
RedisCache和大家普遍实现Mybatis的缓存方案大同小异,无非是实现Cache接口,并使用jedis操作缓
存;不过该项目在设计细节上有一些区别;
public final class RedisCache implements Cache {
public RedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require anID"); } this.id = id; RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration(); pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName()); }
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用
RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,
调用了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。
RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的
属性:
public class RedisConfig extends JedisPoolConfig {
private String host = Protocol.DEFAULT_HOST;
private int port = Protocol.DEFAULT_PORT;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private String password; private int database = Protocol.DEFAULT_DATABASE;
private String clientName;
RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:
public RedisConfig parseConfiguration(ClassLoader classLoader)
{
Properties config = new Properties();
InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
if (input != null) { try {config.load(input); } catch (IOException e)
{ throw new
RuntimeException( "An error occurred while reading classpath property '" +
redisPropertiesFilename + "', see nested exceptions", e);
} finally {
try {
input.close();
} catch (IOException e) {
// close quietly
} } } RedisConfig jedisConfig = new RedisConfig();
setConfigProperties(config, jedisConfig);
return jedisConfig;
}
核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件:
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=
并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用
RedisConfig类创建完成edisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:
private Object execute(RedisCallback callback) {
Jedis jedis = pool.getResource();
try {
return callback.doWithRedis(jedis);
} finally {
jedis.close();
} }
模板接口为RedisCallback,这个接口中就只需要实现了一个doWithRedis方法而已:
public interface RedisCallback { Object doWithRedis(Jedis jedis); }
接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis
储存数据的格式:
@Override public void putObject(final Object key, final Object value)
{ execute(new RedisCallback() {
@Override public Object doWithRedis(Jedis jedis) {
jedis.hset(id.toString().getBytes(), key.toString().getBytes(),
SerializeUtil.serialize(value));
return null;
} }); }
@Override
public Object getObject(final Object key)
{ return execute(new RedisCallback() {
@Override
public Object doWithRedis(Jedis jedis) {
return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes())); }});
}
可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash
的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash
的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,
负责对象的序列化和反序列化;
六、Mybatis插件
1、插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见
的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工
作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务无
关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能
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、Mybatis插件原理
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返 回 target
包装后的对象
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;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相关方法被调用前执
行。
以上就是MyBatis插件机制的基本原理
4、源码分析
执行插件逻辑:
Plugin实现了 InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会 对
所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
// -Plugin public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try {
/**获取被拦截方法列表,比如: * signatureMap.get(Executor.class), 可能返回 [query, update, commit] */
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//检测方法列表是否包含被拦截的方法
if (methods != null && methods.contains(method)) {
//执行插件逻辑
return interceptor.intercept(new Invocation(target, method, args));
//执行被拦截的方法
return method.invoke(target, args);
} catch(Exception e){ } }
invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截方法是否配置在插件的
@Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept中,该
方法的参数类型为Invocationo Invocation主要用于存储目标类,方法以及方法参数列表。下面简单看
一下该类的定义
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object targetf Method method, Object[] args) {
this.target = target;
this.method = method;
//省略部分代码
public Object proceed() throws InvocationTargetException, IllegalAccessException { //调用被拦截的方法 >> —
七、Mybatis架构原理
1、架构设计
我们把Mybatis的功能架构分为三层:
(1) API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库。接口层一接收到
调用请求就会调用数据处理层来完成具体的数据处理。
MyBatis和数据库的交互有两种方式:
a. 使用传统的MyBati s提供的API ;
b. 使用Mapper代理的方式
(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根
据调用的请求完成一次数据库操作。
(3) 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是
共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑
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语句以及相应的参数信息 |
3、总体流程
触发条件:加载配置文件
配置来源于两个地方,一个是配置文件(主配置文件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源码剖析
1、传统方式源码剖析:
源码剖析-初始化:
Inputstream inputstream = Resources.getResourceAsStream("mybatis- config.xml");
//这一行代码正是初始化工作的开始。
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
进入源码分析:
// 1.我们最初调用的build
public SqlSessionFactory build (InputStream inputStream){
//调用了重载方法
return build(inputStream, null, null); }
// 2.调用的重载方法
public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties){ try {
// XMLConfigBuilder是专门解析mybatis的配置文件的类
XMLConfigBuilder parser = new XMLConfigBuilder(inputstream, environment, properties);
//这里又调用了一个重载方法。parser.parse()的返回值是Configuration对象
return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e)
}
MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用
org.apache.ibatis.session.Configuratio n 实例来维护
下面进入对配置文件解析部分:
首先对Configuration对象进行介绍:
Configuration对象的结构和xml配置文件的对象几乎相同。 回顾一下xml中的配置标签有哪些: properties (属性),settings (设置),typeAliases (类型别名),typeHandlers (类型处理 器),objectFactory (对象工厂),mappers (映射器)等 Configuration也有对应的对象属性来封装它 们也就是说,初始化配置文件信息的本质就是创建Configuration对象,将解析的xml数据封装到 Configuration内部属性中
/*** 解析 XML 成 Configuration 对象。 */
public Configuration parse () {
//若已解析,抛出BuilderException异常
if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once.");}
//标记已解析
parsed = true;
// 解析 XML configuration 节点
parseConfiguration(parser.evalNode("/configuration")); return configuration; }
/***解析XML */
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.evalNode("objectFactory"));
// 解析 <objectWrapperFactory /> 标签
objectWrapperFactoryElement(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"));
// 解析 <typeHandlers /> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
//解析<mappers />标签
mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration.Cause:" + e, e); } }
介绍一下 MappedStatement :
作用:MappedStatement与Mapper配置文件中的一个select/update/insert/delete节点相对应。
mapper中配置的标签都被封装到了此对象中,主要用途是描述一条SQL语句。
初始化过程:回顾刚开 始介绍的加载配置文件的过程中,会对mybatis-config.xm l中的各个标签都进行
解析,其中有mappers 标签用来引入mapper.xml文件或者配置mapper接口的目录。
<select id="getUser" resultType="user" >
select * from user where id=#{id}
</select>
一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在
Configuration对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key
=全限定类名+方法名,value =对应的MappedStatement对象。
•在configuration中对应的属性为
Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>
("Mapped Statementscollection")
在 XMLConfigBuilder 中的处理:
private void parseConfiguration(XNode root) { try {
//省略其他标签的处理
mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause:" + e, e); } }
到此对xml配置文件的解析就结束了,回到步骤2.中调用的重载build方法
// 5.调用的重载方法
public SqlSessionFactory build(Configuration config) {
//创建了 DefaultSqlSessionFactory 对象,传入 Configuration 对象。
return new DefaultSqlSessionFactory(config);
}
2、源码剖析-执行SQL流程
先简单介绍SqlSession :
SqlSession是一个接口,它有两个实现类:DefaultSqlSession (默认)和
SqlSessionManager (弃用,不做介绍)
SqlSession是MyBatis中用于和数据库交互的顶层类,通常将它与ThreadLocal绑定,一个会话使用一 个
SqlSession,并且在使用完毕后需要close
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor; j
SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执行器
Executor:
Executor也是一个接口,他有三个常用的实现类:
BatchExecutor (重用语句并执行批量更新)
ReuseExecutor (重用预处理语句 prepared statements)
SimpleExecutor (普通的执行器,默认)
继续分析,初始化完毕后,我们就要执行SQL 了
SqlSession sqlSession = factory.openSession();
List<User> list = sqlSession.selectList("com.lagou.mapper.UserMapper.getUserByName");
获得 sqlSession
//6. 进入 o penSession 方法。
public SqlSession openSession() {
//getDefaultExecutorType()传递的是SimpleExecutor return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
//7. 进入penSessionFromDataSource。
//ExecutorType 为Executor的类型,TransactionIsolationLevel为事务隔离级别, autoCommit是否开启事务 //openSession的多个重载方法可以指定获得的SeqSession的Executor类型和事务的处理
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()
}
执行 sqlsession 中的 api
//8.进入selectList方法,多个重载方法。
public <E > List < E > selectList(String statement) { return this.selectList(statement, null); public <E > List < E > selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); public <E > List < E > selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据传入的全限定名+方法名从映射的Map中取出MappedStatement对象
MappedStatement ms = configuration.getMappedStatement(statement);
//调用Executor中的方法处理
//RowBounds是用来逻辑分页
// wrapCollection(parameter)是用来装饰集合或者数组参数
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: + e, e); } finally { ErrorContext.instance().reset(); }
源码剖析-executor:
继续源码中的步骤,进入executor.query()
//此方法在SimpleExecutor的父类BaseExecutor中实现
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
//为本次查询创建缓存的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
//进入query的重载方法中
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); }if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }List<E> list; try {queryStack++; 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); } } finally { queryStack--; }if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); }
// issue #601 deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } }return list; }
//从数据库查询
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;
}
// SimpleExecutor中实现父类的doQuery抽象方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try {Configuration configuration = ms.getConfiguration();
//传入参数创建StatementHanlder对象来执行查询
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//创建jdbc中的statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
// StatementHandler 进行处理
return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
//创建Statement的方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt;
//条代码中的getConnection方法经过重重调用最后会调用openConnection方法,从连接池中 获 得连接。
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt; }
//从连接池获得连接的方法
protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); }
//从连接池获得连接
connection = dataSource.getConnection(); if (level != null) { connection.setTransactionIsolation(level.getLevel()); }
}
上述的Executor.query()方法几经转折,最后会创建一个StatementHandler对象,然后将必要的参数传
递给StatementHandler,使用StatementHandler来完成对数据库的查询,最终返回List结果集。
从上面的代码中我们可以看出,Executor的功能和作用是:
(1、根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;
(2、为查询创建缓存,以提高性能
(3、创建JDBC的Statement连接对象,传递给*StatementHandler*对象,返回List查询结果。
源码剖析-StatementHandler:
StatementHandler对象主要完成两个工作:
对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使用的是SQL语句字符串会包
含若干个?占位符,我们其后再对占位符进行设值。StatementHandler通过
parameterize(statement)方法对 S tatement 进行设值;
StatementHandler 通过 List query(Statement statement, ResultHandler resultHandler)方法来
完成执行Statement,和将Statement对象返回的resultSet封装成List;
进入到 StatementHandler 的 parameterize(statement)方法的实现:
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler对象来完成对Statement的设值
parameterHandler.setParameters((PreparedStatement) statement); }
/** ParameterHandler 类的 setParameters(PreparedStatement ps) 实现 * 对某一个Statement进行设置参数 * */
public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value;String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) {
// issue #448 ask first for additional
params value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); }
// 每一个 Mapping都有一个 TypeHandler,根据 TypeHandler 来对 preparedStatement 进 行设置参数
TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
//设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType); }}
} }
从上述的代码可以看到,StatementHandler的parameterize(Statement)方法调用了
ParameterHandler的setParameters(statement)方法,
ParameterHandler的setParameters(Statement )方法负责根据我们输入的参数,对statement对象的 ?
占位符处进行赋值。
进入到StatementHandler 的 List query(Statement statement, ResultHandler resultHandler)方法的
实现:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// 1.调用preparedStatemnt。execute()方法,然后将resultSet交给ResultSetHandler处理
PreparedStatement ps = (PreparedStatement) statement; ps.execute();
//2.使用 ResultHandler 来处理
ResultSet return resultSetHandler.<E> handleResultSets(ps);
}
从上述代码我们可以看出,StatementHandler 的List query(Statement statement, ResultHandler
resultHandler)方法的实现,是调用了 ResultSetHandler 的 handleResultSets(Statement)方法。
ResultSetHandler 的 handleResultSets(Statement)方法会将 Statement 语句执行后生成的 resultSet
结果集转换成List结果集
public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(
mappedStatement.getId());
//多ResultSet的结果集合,每个ResultSet对应一个Object对象。而实际上,每 个 Object 是 List<Object> 对象。
//在不考虑存储过程的多ResultSet的情况,普通的查询,实际就一个ResultSet,也 就是说, multipleResults最多就一个元素。
final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0;
//获得首个ResultSet对象,并封装成ResultSetWrapper对象
ResultSetWrapper rsw = getFirstResultSet(stmt);
//获得ResultMap数组 //在不考虑存储过程的多ResultSet的情况,普通的查询,实际就一个ResultSet,也 就是说, resultMaps就一个元素。
List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount);
// 校验
while (rsw != null && resultMapCount > resultSetCount) {
//获得ResultMap对象
ResultMap resultMap = resultMaps.get(resultSetCount);
//处理ResultSet,将结果添加到multipleResults中
handleResultSet(rsw, resultMap, multipleResults, null);
//获得下一个ResultSet对象,并封装成ResultSetWrapper对象
rsw = getNextResultSet(stmt);
//清理
cleanUpAfterHandlingResultSet();
// resultSetCount ++
resultSetCount++; } }//因为'mappedStatement.resultSets'只在存储过程中使用,本系列暂时不考虑,忽略即可
String[] resultSets = mappedStatement.getResultSets(); if(resultSets!=null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); }rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } }
//如果是multipleResults单元素,则取首元素返回
return collapseSingleResultList(multipleResults);
}
九、设计模式
虽然我们都知道有3类23种设计模式,但是大多停留在概念层面,Mybatis源码中使用了大量的设计模式,观察设计模式
在其中的应用,能够更深入的理解设计模式Mybati s至少用到了以下的设计模式的使用:
1、 Builder构建者模式
uilder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表
示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范
围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加
复杂的对象的构建,甚至只会构建产品的一个部分,直白来说,就是使用多个简单的对象一步一步构建
成一个复杂的对象
例子:使用构建者设计模式来生产computer
主要步骤:
1、将需要构建的目标类分成多个部件(电脑可以分为主机、显示器、键盘、音箱等部件);
2、 创建构建类;
3、 依次创建部件;
4、 将部件组装成目标对象
1. 定义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;
}
public String getMainUnit() {
return mainUnit;
}
public void setMainUnit(String mainUnit) {
this.mainUnit = mainUnit;
}
public String getMouse() {
return mouse;
}
public void setMouse(String mouse) {
this.mouse = mouse;
}
public String getKeyboard() {
return keyboard;
}
public void setKeyboard(String keyboard) {
this.keyboard = keyboard;
}
@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中的体现
SqlSessionFactory 的构建过程:
Mybatis的初始化工作非常复杂,不是只用一个构造函数就能搞定的。所以使用了建造者模式,使用了
大量的Builder,进行分层构造,核心对象Configuration使用了 XmlConfigBuilder来进行构造
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的
MybatisMapConfig.xml 和所有的 *Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration
对 象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
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")); }
其中 XMLConfigBuilder 在构建 Configuration 对象时,也会调用 XMLMapperBuilder 用于读取
*Mapper 文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。
//解析<mappers />标签
mapperElement(root.evalNode("mappers"));
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser
解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所
能包括的,因此大量采用了 Builder模式来解决
SqlSessionFactoryBuilder类根据不同的输入参数来构建SqlSessionFactory这个工厂对象
2、工厂模式
在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂
模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于创
建型模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建
其他类的实例,被创建的实例通常都具有共同的父类
例子:生产电脑
假设有一个电脑的代工生产商,它目前已经可以代工生产联想电脑了,随着业务的拓展,这个代工生产
商还要生产惠普的电脑,我们就需要用一个单独的类来专门生产电脑,这就用到了简单工厂模式。
下面我们来实现简单工厂模式:
1. 创建抽象产品类
我们创建一个电脑的抽象产品类,他有一个抽象方法用于启动电脑:
public abstract class Computer {
/** *产品的抽象方法,由具体的产品类去实现 */
public abstract void start();
}
2. 创建具体产品类
接着我们创建各个品牌的电脑,他们都继承了他们的父类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("惠普电脑启动");
}
}
3. 创建工厂类
接下来创建一个工厂类,它提供了一个静态方法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 体现:
Mybatis中执行Sql语句、获取Mappers、管理事务的核心接口SqlSession的创建过程使用到了工厂模式。
有一个 SqlSessionFactory 来负责 SqlSession 的创建
SqlSessionFactory
可以看到,该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,autoComm it);
//根据参数创建制定类型的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
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!");
}
}
(3) 创建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对象,然后调用T
newInstance(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 的执行和返回