MyBatis源码学习小结

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

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: MyBatis 是一个开源的持久层框架,可以方便地将 SQL 语句和 Java 对象进行映射。如果您想要学习 MyBatis 源码,可以按照以下步骤进行: 1. 了解 MyBatis 的架构和设计原理。可以阅读官方文档和相关书籍,例如《MyBatis 技术内幕》。 2. 下载 MyBatis 的源代码,并导入到 IDE 中。MyBatis 使用 Maven 进行构建,您可以使用 IDE 的 Maven 插件来下载依赖项。 3. 查看 MyBatis 的源代码结构。MyBatis 的主要代码在 `mybatis-3` 模块中,包括 `src/main/java` 和 `src/main/resources` 目录。其中,`src/main/java` 目录包含了 MyBatis 的核心代码,例如 `org.apache.ibatis.session.SqlSession` 类;`src/main/resources` 目录包含了 MyBatis 的配置文件和映射文件。 4. 阅读 MyBatis 的源代码。可以从 MyBatis 的入口处 `org.apache.ibatis.session.SqlSessionFactoryBuilder` 开始,深入了解 MyBatis 的初始化流程、SQL 语句的执行流程、映射文件的解析和缓存等。 5. 调试 MyBatis 的源代码。可以使用 IDE 的调试功能,对 MyBatis 进行单步调试,观察代码的执行流程,加深对 MyBatis 的理解。 6. 学习 MyBatis 的单元测试。MyBatis 的单元测试位于 `src/test/java` 目录中,可以通过单元测试来了解 MyBatis 的各个功能点的使用方法和测试用例。 7. 参与 MyBatis 的开发。如果您对 MyBatis 源码有深入的了解,并希望为 MyBatis 做出贡献,可以参与 MyBatis 的开发,贡献代码和文档,提交 issue 和 PR。MyBatis 的开发社区非常活跃,可以在官方网站和 GitHub 上找到相关信息。 希望这些步骤对您学习 MyBatis 源码有所帮助。 ### 回答2: MyBatis是一个开源的Java持久层框架,通过操作对象与数据库关系映射来提供数据持久化的功能。了解MyBatis源码学习和使用该框架的重要一步。 首先,MyBatis源码结构比较清晰,主要分为核心模块和附属模块。核心模块包括XML配置解析、SQL语句解析、参数处理、数据库连接管理等功能的实现,是实现MyBatis基本功能的核心部分。附属模块包括缓存、事务、插件等额外功能的实现,可以根据需要进行扩展和配置。 学习MyBatis源码可以从以下几个方面入手: 1. 配置文件解析:MyBatis通过XML配置文件来进行相关的配置,了解配置文件的解析过程可以帮助理解MyBatis的初始化过程和各项配置的作用。 2. SQL语句解析与执行:MyBatis将SQL语句封装成MappedStatement对象进行管理,了解MappedStatement的生成过程,以及SQL语句的解析、参数处理和执行过程,可以深入了解MyBatis的SQL执行原理。 3. 会话管理和事务处理:MyBatis采用SqlSessionFactory和SqlSession来管理数据库连接和事务,在MyBatis源码中可以学习到如何管理数据库连接池、事务的提交和回滚等核心功能的实现。 4. 缓存机制:MyBatis提供了一级缓存和二级缓存的功能,了解缓存的生成和更新过程,以及缓存的命中和失效原理,可以提高数据库查询性能。 总之,通过学习MyBatis源码,可以加深对该框架的理解,掌握其内部实现原理,有助于在使用时更加灵活和高效地进行开发。同时,也为以后解决一些特殊问题提供了更多的思路和方法。 ### 回答3: MyBatis是一个优秀的持久层框架,学习源码有助于理解其底层原理和设计思想。 首先,可以从MyBatis的入口开始学习,即SqlSessionFactoryBuilder类。该类负责解析配置文件、创建Configuration对象,并通过Configuration对象创建SqlSessionFactory实例。 接下来,可以学习Configuration类,该类负责管理整个MyBatis的配置信息。其中包括了数据库连接信息、映射文件信息、缓存信息等。在该类内部,会调用XMLMapperBuilder类解析映射文件,在解析映射文件过程中,会创建MappedStatement对象,该对象表示一条SQL语句的映射信息。 学习MappedStatement对象可以了解MyBatis的SQL语句解析过程。该对象包含了SQL语句的相关信息,包括参数映射关系、返回结果映射关系等。在执行SQL语句时,会使用ParameterHandler类处理参数,通过ResultSetHandler类处理查询结果。 同时,学习到Executor接口及其实现类,可以了解MyBatis的执行过程。Executor负责执行SQL语句,其中包括了写操作的update方法和读操作的query方法。在执行过程中,会通过StatementHandler类创建PreparedStatement对象,并通过ResultSetHandler类处理执行结果。 最后,还可以学习MyBatis的事务处理和缓存机制。Transaction接口及其实现类负责事务管理,通过JDBC的事务机制实现了事务的提交和回滚。而Cache接口及其实现类负责缓存查询结果,在查询时会先从缓存中查找结果。 总结来说,通过学习MyBatis源码可以深入理解其底层原理和设计思想。从SqlSessionFactory的创建开始,到Configuration的配置解析、MappedStatement的创建,再到Executor的执行过程和Transaction的事务管理,以及Cache的缓存机制,逐步掌握MyBatis的各个组件和它们之间的交互关系。这对于我们使用MyBatis开发项目,解决问题和优化性能都具有积极的意义。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值