1.什么是MyBatis
根据官方介绍,MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。
MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。
MyBatis可以通过简单的XML或注解来配置和映射原始类型、接口和Java POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录。
MyBatis官网地址如下:https://mybatis.org/mybatis-3/zh/index.html
但在实际项目应用中,我们一般都会使用Spring框架,因此通常会使用Mybatis-Spring。但其本质还是MyBatis,只是通过Spring框架的特性,省去了部分机械化的操作,更加便捷一些。
2.什么是MyBatis-Spring
根据官方介绍,MyBatis-Spring会帮助我们将MyBatis代码无缝地整合到Spring中。
它将允许MyBatis参与到Spring的事务管理之中,创建映射器mapper和SqlSession并注入到bean中,以及将Mybatis的异常转换为Spring的DataAccessException。
最终,可以做到应用代码不依赖于MyBatis、Spring或MyBatis-Spring。
Mybatis-Spring官网地址如下:http://mybatis.org/spring/zh/index.html
3.MyBatis冷知识
MyBatis的英文读音是:[mai’bətɪs],听说建议读音是:买 - 啵额蒂斯(啵额要连读,以略去啵“bo”中的o音)。
MyBatis前世是ibatis,这个词是由"internet"和"abatis"组合而成,创始人是Clinton Begin。
abatis的含义是:篱笆墙,这是用来保护院子的一种设施,一般都是由木头,棍子,竹子,芦苇、灌木或者石头构成,常见于我国北方农村以及欧美等地广人稀的国家,用于保护院子。
ibatis项目最初侧重于密码软件的开发,从ibatis的含义可知,其最初的目的是想当做互联网的篱笆墙,后来成为了一个基于Java的持久层框架。
2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。
4.MyBatis架构
MyBatis架构图如下所示,来源于网络:https://www.jianshu.com/p/15781ec742f2,里面还简单分析了MyBatis的功能架构设计。
5.MyBatis的异常包装
MyBatis的异常统一都是通过ErrorContext这个对象,进行收集和打印的。
MyBatis在遇到异常时,会获取一个ErrorContext的实例,然后以此进行异常信息的包装和打印。
这个实例是全局ThreadLocal中,当前线程的ErrorContext实例,如果不存在则创建一个,因此也可以保证多线程下的并发安全。
异常包装完成后,MyBatis也会在finally语句块中,对ErrorContext实例进行重置,即清空所有属性值,及从全局ThreadLocal中移除当前线程的ErrorContext实例,防止内存泄漏。
ErrorContext的内容形式和格式可以从它的toString方法中看出:
// 行分隔符,它会根据当前的电脑系统返回对应的行分隔符
private static final String LINE_SEPARATOR = System.lineSeparator();
private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);
@Override
public String toString() {
StringBuilder description = new StringBuilder();
// 概括性的错误信息描述
if (this.message != null) {
description.append(LINE_SEPARATOR);
description.append("### ");
description.append(this.message);
}
// 错误可能出现的位置
if (resource != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may exist in ");
description.append(resource);
}
// 错误可能涉及到的对象
if (object != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may involve ");
description.append(object);
}
// 错误是在哪个环节发生的
if (activity != null) {
description.append(LINE_SEPARATOR);
description.append("### The error occurred while ");
description.append(activity);
}
// 错误涉及到的sql语句
if (sql != null) {
description.append(LINE_SEPARATOR);
description.append("### SQL: ");
description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
}
// 引起错误的原因
if (cause != null) {
description.append(LINE_SEPARATOR);
description.append("### Cause: ");
description.append(cause.toString());
}
return description.toString();
}
6.MyBatis的数据库环境配置
MyBatis可以配置成适应多种环境,这种机制有助于将SQL映射应用于多种数据库之中。environments元素定义了如何配置环境(environment)。
尽管可以配置多个环境(environment),但每个SqlSessionFactory实例只能选择一种环境。为了指定创建哪种环境,只要将它作为可选的参数传递给SqlSessionFactoryBuilder即可。
如果忽略了环境参数,那么将会加载默认环境,因此默认环境ID一定要与其中一个环境ID匹配。示例如下:
<environments default="development">
<environment id="development">
<!-- 事务管理器的配置,有2种type:JDBC|MANAGED -->
<!-- JDBC:直接使用了JDBC的提交和回滚处理,它依赖从数据源获得的连接来管理事务作用域 -->
<!-- MANAGED:让容器来管理事务的整个生命周期。默认情况下关闭事务管理器时会关闭数据库连接,但是如果有需要,可以将closeConnection属性设置为false来阻止关闭数据库连接的行为 -->
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<!-- 数据源的配置,有三种内建的数据源类型:UNPOOLED|POOLED|JNDI -->
<!-- 也可以通过实现接口org.apache.ibatis.datasource.DataSourceFactory来使用第三方数据源实现,
同时需要将type指定为自定义接口的全限定类名,如:type="org.myproject.C3P0DataSourceFactory" -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
6.1MyBatis的内建数据源
在MyBatis中有3种内建数据源类型,它们各自的特点及属性配置支持如下表所示:
数据源类型 | 特点 | 支持属性 |
---|---|---|
UNPOOLED | 在每次请求时打开连接,在请求结束后关闭连接 适用于对数据库连接可用性要求不高的简单应用程序 | driver – JDBC驱动的Java类全限定名 url – 数据库的JDBC URL地址 username – 登录数据库的用户名 password – 登录数据库的密码 defaultTransactionIsolationLevel – 默认的连接事务隔离级别 defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒) |
POOLED | 利用”池“的概念管理数据库连接,避免了创建新的连接实例时所必需的初始化和认证时间 能使并发Web应用快速响应请求 | 除了UNPOOLED下的属性外,还有以下属性: poolMaximumActiveConnections – 最大活跃连接数量,默认值:10 poolMaximumIdleConnections – 最大闲置连接数,默认值:5 poolMaximumCheckoutTime – 池中连接最大连接时间,默认值:20000毫秒 poolTimeToWait – 获取连接等待时间,默认值:20000毫秒 poolMaximumLocalBadConnectionTolerance – 最大废弃连接数量,默认值:3(新增于 3.4.5) poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求,默认值:"NO PING QUERY SET" poolPingEnabled – 是否启用侦测查询。若开启,需要设置poolPingQuery属性为一个可执行的 SQL 语句,默认值:false poolPingConnectionsNotUsedFor – 配置poolPingQuery的频率,默认值:0 |
JNDI | 能在如EJB或应用服务器这类容器中使用 容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的数据源引用 | 这种数据源配置只需要两个属性: initial_context – 可选属性,用于initialContext.lookup(initial_context),如果忽略则使用data_source属性 data_source – 引用数据源实例位置的上下文路径 |
6.2MyBatis的PooledDataSource底层实现
一般情况下,我们主要使用的数据源类型都是POOLED,也就是所谓的数据库连接池,因此下面主要分析的是:MyBatis的PooledDataSource底层实现。
首先看下组成数据库连接池的比较重要的3个类的类图,下图主要展示了类的成员变量构成:
从数据库连接池中,获取一个数据库连接的简单流程时序图,就如下图所示:
可以看到,数据库连接创建是在popConnection这个方法内完成的,并且最终返回的其实是真实数据库连接的代理连接:proxyConnection。
后续所有的数据库操作都是基于这个代理连接实现的,这也是组成数据库连接池的关键部分。
popConnection方法的整体实现逻辑,如下图所示:
从数据库连接池中,关闭一个数据库连接的简单流程时序图,就如下图所示:
关闭数据库连接时,如果使用的是JdbcTransaction,会调用resetAutoCommit方法,这个方法会将autoCommit置为true。
这是因为部分数据库在关闭连接前会强制执行commit或者rollback方法,将autoCommit置为true则会忽略commit或者rollback方法。详情可见源代码注释:
protected void resetAutoCommit() {
try {
if (!connection.getAutoCommit()) {
// MyBatis does not call commit/rollback on a connection if just selects were performed.
// Some databases start transactions with select statements
// and they mandate a commit/rollback before closing the connection.
// A workaround is setting the autocommit to true before closing the connection.
// Sybase throws an exception here.
if (log.isDebugEnabled()) {
log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(true);
}
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Error resetting autocommit to true "
+ "before closing the connection. Cause: " + e);
}
}
}
另外,由于获取到的连接都是代理连接,因此所有方法都会被拦截,当发现方法名为"close"时,会调用pushConnection方法完成数据库连接关闭流程。
pushConnection方法的整体实现逻辑,如下图所示:
7.MyBatis的缓存机制
MyBatis自带的缓存有一级缓存和二级缓存,可以通过配置settings元素来指定一级缓存的作用域,或在对应的Mapper.xml文件中定义需要使用的cache,来启用二级缓存。
配置示例如下:
<settings>
<!-- 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,有两种值:true|false,默认为true,开启二级缓存需要将其置为true(会将Executor包装为CachingExecutor) -->
<setting name="cacheEnabled" value="true"/>
<!-- MyBatis利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询,有两种值:SESSION|STATEMENT。
默认为SESSION,会缓存一个会话中执行的所有查询。若设置值为STATEMENT,本地缓存将仅用于执行语句,对相同SqlSession的不同查询将不会进行缓存。 -->
<setting name="localCacheScope" value="SESSION"/>
</settings>
...
<mapper namespace="...UserMapper">
<!-- 使用默认配置,启用二级缓存 -->
<cache/>
<!-- 也可以配置cache-ref元素,指定需要引用的Cache所在的namespace,使用其他Mapper的Cache配置,启用二级缓存
<cache-ref namespace="...OtherMapper"/>
-->
...
<!-- 如果不希望某个查询语句启用二级缓存,可以设置useCache为false,默认是true -->
<!-- 如果将Select元素的属性flushCache设置为true,无论是一级缓存还是二级缓存,每次执行这个查询语句前,都会被清空,默认是false -->
<!-- 除select外的语句,它们的flushCache默认都是true,因此后无论是一级缓存还是二级缓存,在它们被执行前都会被清空 -->
<select id="selectByPrimaryKey" resultMap="BaseResultMap" useCache="false" flushCache="true">
select * from user
</select>
</mapper>
cache元素可配置属性如下表所示:
属性名 | 属性值 | 默认属性值 | 描述 |
---|---|---|---|
eviction | LRU|FIFO|SOFT|WEAK | LRU | 缓存淘汰策略: LRU – 最近最少使用:移除最长时间不被使用的对象。 FIFO – 先进先出:按对象进入缓存的顺序来移除它们。 SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。 WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。 |
flushInterval | 任意正整数,单位为毫秒 | null | 缓存刷新(清空)间隔,不设置刷新(清空)间隔的话,缓存只会在执行更新操作时被刷新(清空)。 |
size | 任意正整数 | 1024 | 最多可以存储的结果对象或列表的引用数量 |
readOnly | true|false | false | 只读的缓存会给所有调用者返回缓存对象的相同实例,性能较高但是不安全,设置ReadOny=True的目的是:告诉用户不要从缓存中取出之后,对对象进行修改。 可读写的缓存会通过序列化,返回缓存对象的拷贝,速度上会慢一些,但是更安全,因此默认值是false。 设置ReadOny=False的目的是:告诉用户从缓存中取出之后,可以对对象进行修改,而不影响cache里面的对象。 但在使用Redis的情况下,即便设置为:readOnly=“true”,每次取到对象也是不一样的,因为中间有个序列化的过程。 |
type | org.mybatis.caches.redis.RedisCache | null | 除了上述自定义缓存的方式,也可以通过实现自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。 |
7.1MyBatis的一级缓存底层实现
一级缓存没有过期时间,只有生命周期。
MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个Executor对象,Executor对象中持有一个PerpetualCache对象,见下面代码:
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象会被一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
SqlSession中执行了任何一个更新操作,例如:update、delete、insert ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
基于一级缓存的SQL查询流程如下图所示:
7.2MyBatis的二级缓存底层实现
二级缓存有过期时间,但是它的过期时间并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval。
如果设置了flushInterval,cache会被包装成ScheduledCache,每当存取数据的时候,都会检测一下cache的存活时间,如果存活时间超过了规定时间,那么将整个清空一下,见下面代码:
/**
* Copyright 2009-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache.decorators;
import java.util.concurrent.TimeUnit;
import org.apache.ibatis.cache.Cache;
/**
* @author Clinton Begin
*/
public class ScheduledCache implements Cache {
private final Cache delegate;
protected long clearInterval;
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
this.clearInterval = TimeUnit.HOURS.toMillis(1);
this.lastClear = System.currentTimeMillis();
}
public void setClearInterval(long clearInterval) {
this.clearInterval = clearInterval;
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
clearWhenStale();
return delegate.getSize();
}
@Override
public void putObject(Object key, Object object) {
clearWhenStale();
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
return clearWhenStale() ? null : delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
clearWhenStale();
return delegate.removeObject(key);
}
@Override
public void clear() {
lastClear = System.currentTimeMillis();
delegate.clear();
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
}
基于二级缓存的SQL查询流程如下图所示: