MyBatis源码学习小结

本文详细介绍了MyBatis的各个方面,从基本概念到架构设计,再到异常处理和数据库环境配置。重点探讨了MyBatis的缓存机制,包括一级缓存的生命周期管理和二级缓存的过期时间。同时,解析了MyBatis的内建数据源和PooledDataSource的底层实现,揭示了数据库连接池的工作原理。
摘要由CSDN通过智能技术生成

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元素可配置属性如下表所示:

属性名属性值默认属性值描述
evictionLRU|FIFO|SOFT|WEAKLRU缓存淘汰策略:
LRU – 最近最少使用:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval任意正整数,单位为毫秒null缓存刷新(清空)间隔,不设置刷新(清空)间隔的话,缓存只会在执行更新操作时被刷新(清空)。
size任意正整数1024最多可以存储的结果对象或列表的引用数量
readOnlytrue|falsefalse只读的缓存会给所有调用者返回缓存对象的相同实例,性能较高但是不安全,设置ReadOny=True的目的是:告诉用户不要从缓存中取出之后,对对象进行修改。
可读写的缓存会通过序列化,返回缓存对象的拷贝,速度上会慢一些,但是更安全,因此默认值是false。
设置ReadOny=False的目的是:告诉用户从缓存中取出之后,可以对对象进行修改,而不影响cache里面的对象。
但在使用Redis的情况下,即便设置为:readOnly=“true”,每次取到对象也是不一样的,因为中间有个序列化的过程。
typeorg.mybatis.caches.redis.RedisCachenull除了上述自定义缓存的方式,也可以通过实现自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

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查询流程如下图所示:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值