死磕 Mybatis 源码解析系列(二)

915 阅读5分钟

一、缓存

在 MyBatis 里面,我们常听说缓存,那么它究竟是怎么使用或者怎么配置的呢?比如,一级缓存、二级缓存,它们又有什么区别呢?

1.1 一级缓存

会话级别的缓存,只存在于一个 sqlsession 中。

问题 1:那么它设置的位置在那里呢?是什么时候创建的呢?

SqlSession 有个默认实现 DefaultSqlSession,DefaultSqlSession 有一个 Executor 属性。

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  // 这里有个 执行器的属性
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }
}

而 Executor 下的实现类里面有个基础执行器的实现 BaseExecutor,在这个执行器里面我们会看到 PerpetualCache 属性,这里就是一级缓存的产出地。

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  
  //一级缓存,我们在上面一章会发现有对一级缓存的使用  
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;
}

首先我们看下基于 BaseExecutor 所在的目录结构:

在这里插入图片描述

看到 decorators 我们就知道这里用了装饰模式,当然,实际实现也是装饰模式,这里我们看到 Cache 它的基础实现就是 PerpetualCache;从功能来说,我们可以把装饰的几个 Cache 划分为下面几种:

#缓存实现的基础类
PerpetualCache 

#缓存装饰类(强化缓存的功能)
LoggingCache
BlockingCache
ScheduledCache
SerializedCache
SynchronizedCache

#缓存淘汰算法
FifoCache
LruCache

#JVM回收算法
WeakCache
SoftCache

配置文件加入(关闭全局缓存):

<setting name="cacheEnabled" value="false" />
package com.mybatis.test;

import com.alibaba.fastjson.JSONObject;
import com.mybatis.entity.UserDTO;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;

public class Test2 {

    public static void main(String[] args){
        try {
            Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
            // sqlSession能够执行配置文件中的SQL语句
            SqlSession sqlSession1 = factory.openSession();
            SqlSession sqlSession2 = factory.openSession();

            UserMapper userMapper1=sqlSession1.getMapper(UserMapper.class);
            UserDTO u1=userMapper1.getById(1L);
            System.out.println("第一次:"+JSONObject.toJSONString(u1));

            UserMapper userMapper2=sqlSession1.getMapper(UserMapper.class);
            UserDTO u2=userMapper2.getById(1L);
            System.out.println("第二次:"+JSONObject.toJSONString(u2));

            UserMapper userMapper3=sqlSession2.getMapper(UserMapper.class);
            UserDTO u3=userMapper3.getById(1L);
            System.out.println("第三次:"+JSONObject.toJSONString(u3));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Opening JDBC Connection
Created connection 1615039080.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@60438a68]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, sss
<==      Total: 1
第一次:{"id":1,"userName":"sss"}
第二次:{"id":1,"userName":"sss"}
Opening JDBC Connection
Created connection 591391158.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@233fe9b6]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, sss
<==      Total: 1
第三次:{"id":1,"userName":"sss"}

你会发现第二次没有输出 SQL 语句,由此证明它是走得缓存,然后 sqlSession2 开启的会话中,会发现,它是直接查询的数据库,也能证明 SqlSession 会话作用域在局部。

问题 2:一级缓存什么时候会被销毁呢?

其实在数据 delete/install/update 都会更新,在 mapper.xml 标签里面 这三个方法默认的 flushCache 是为 true 的,而 select 标签 默认是为 false 的,如果我们把它也设置为 true ,那么它将不会使用一级缓存了。

代码证明:

<select id="getById" resultMap="resultMap" flushCache="true">
        SELECT <include refid="Base_Column_List" />  from deo_user where id= #{id}
</select>
package com.mybatis.test;

import com.alibaba.fastjson.JSONObject;
import com.mybatis.entity.UserDTO;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;

public class Test3 {

    public static void main(String[] args){
        try {
            Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
            // sqlSession能够执行配置文件中的SQL语句
            SqlSession sqlSession1 = factory.openSession();

            UserMapper userMapper1=sqlSession1.getMapper(UserMapper.class);
            UserDTO u1=userMapper1.getById(1L);
            System.out.println("第一次:"+JSONObject.toJSONString(u1));

            UserMapper userMapper2=sqlSession1.getMapper(UserMapper.class);
            UserDTO u2=userMapper2.getById(1L);
            System.out.println("第二次:"+JSONObject.toJSONString(u2));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

结果:

Opening JDBC Connection
Created connection 2007331442.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@77a57272]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, sss
<==      Total: 1
第一次:{"id":1,"userName":"sss"}
Cache Hit Ratio [com.mybatis.mapper.UserMapper]: 0.0
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, sss
<==      Total: 1
第二次:{"id":1,"userName":"sss"}

在同一个会话中,我们会发现两次都是直接查询的数据库,第二次没有在走缓存了。

问题 3:那么一级缓存还有另外一个问题存在,那就是我 A 会话查询一次后,B 会话修改提交,在 A 会话第二次查询时还是以前的数据,发生了脏读呢,是不是发生了我们预想的结果,那么这个问题怎么解决呢?

public static void main(String[] args){
        try {
            Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
            // sqlSession能够执行配置文件中的SQL语句
            SqlSession sqlSession1 = factory.openSession();
            SqlSession sqlSession2 = factory.openSession();

            UserMapper userMapper1=sqlSession1.getMapper(UserMapper.class);
            UserDTO u1=userMapper1.getById(1L);
            System.out.println("a会话查询结果:"+JSONObject.toJSONString(u1));
            sqlSession1.commit();

            UserMapper userMapper2=sqlSession2.getMapper(UserMapper.class);
            int updateResult=userMapper2.update(1L,"小肥羊1000");
            System.out.println("B会话修改结果:"+JSONObject.toJSONString(updateResult));
            sqlSession2.commit();

            UserMapper userMapper3=sqlSession1.getMapper(UserMapper.class);
            UserDTO u3=userMapper3.getById(1L);
            System.out.println("a会话 第二次查询结果:"+JSONObject.toJSONString(u3));
            sqlSession1.commit();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  Opening JDBC Connection
Created connection 1615039080.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@60438a68]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, 小肥羊100
<==      Total: 1
a会话查询结果:{"id":1,"userName":"小肥羊100"}
Opening JDBC Connection
Created connection 591391158.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@233fe9b6]
==>  Preparing: update deo_user set user_name=? where id = ? 
==> Parameters: 小肥羊1000(String), 1(Long)
<==    Updates: 1
B会话修改结果:1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@233fe9b6]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, 小肥羊100
<==      Total: 1
a会话 第二次查询结果:{"id":1,"userName":"小肥羊100"}

这里就涉及到二级缓存了。

2.2 二级缓存

既然说到二级缓存,当二级缓存和一级缓存都开启了,那么我们先获取的缓存是二级缓存,因为二级缓存的作用域比一级缓存作用域大,二级缓存是通过装饰 Executor 执行器维护的,有一个 CachingExecutor 的实现类。

CachingExecutor.java

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

TransactionalCacheManager.java

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
}

首先我们开启下二级缓存,测试下 mybatis-config.xml:

<setting name="cacheEnabled" value="true" />

UserMapper.xml

<cache />
package com.mybatis.test;

import com.alibaba.fastjson.JSONObject;
import com.mybatis.entity.UserDTO;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;

public class Test5 {

    public static void main(String[] args){
        try {
            Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
            // sqlSession能够执行配置文件中的SQL语句
            SqlSession sqlSession1 = factory.openSession();
            SqlSession sqlSession2 = factory.openSession();

            UserMapper userMapper1=sqlSession1.getMapper(UserMapper.class);
            UserDTO u1=userMapper1.getById(1L);
            System.out.println("a会话查询结果:"+JSONObject.toJSONString(u1));
            sqlSession1.commit();

            UserMapper userMapper2=sqlSession2.getMapper(UserMapper.class);
            UserDTO u2=userMapper2.getById(1L);
            System.out.println("b会话查询结果:"+JSONObject.toJSONString(u2));
            sqlSession2.commit();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

结果:

Opening JDBC Connection
Created connection 1485697819.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@588df31b]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, 小肥羊1002
<==      Total: 1
a会话查询结果:{"id":1,"userName":"小肥羊1002"}
Cache Hit Ratio [com.mybatis.mapper.UserMapper]: 0.5
b会话查询结果:{"id":1,"userName":"小肥羊1002"}

Process finished with exit code 0

这里证明我们二级缓存开启OK了,那么我们再看看跨会话数据更新后的结果。

public static void main(String[] args){
        try {
            Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
            // sqlSession能够执行配置文件中的SQL语句
            SqlSession sqlSession1 = factory.openSession();
            SqlSession sqlSession2 = factory.openSession();

            UserMapper userMapper1=sqlSession1.getMapper(UserMapper.class);
            UserDTO u1=userMapper1.getById(1L);
            sqlSession1.commit();
            System.out.println("A会话查询结果:"+JSONObject.toJSONString(u1));

            UserMapper userMapper2=sqlSession2.getMapper(UserMapper.class);
            int updateResult=userMapper2.update(1L,"小肥羊二级会话更新测试结果");
            System.out.println("B会话修改结果:"+JSONObject.toJSONString(updateResult));
            sqlSession2.commit();

            UserMapper userMapper3=sqlSession2.getMapper(UserMapper.class);
            UserDTO u3=userMapper3.getById(1L);
            sqlSession2.commit();
            System.out.println("A会话查询结果:"+JSONObject.toJSONString(u3));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Opening JDBC Connection
Created connection 1485697819.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@588df31b]
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, 小肥羊100
<==      Total: 1
A会话查询结果:{"id":1,"userName":"小肥羊100"}
Opening JDBC Connection
Created connection 1169794610.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@45b9a632]
==>  Preparing: update deo_user set user_name=? where id = ? 
==> Parameters: 小肥羊二级会话更新测试结果(String), 1(Long)
<==    Updates: 1
B会话修改结果:1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@45b9a632]
Cache Hit Ratio [com.mybatis.mapper.UserMapper]: 0.0
==>  Preparing: SELECT id, user_name from deo_user where id= ? 
==> Parameters: 1(Long)
<==    Columns: id, user_name
<==        Row: 1, 小肥羊二级会话更新测试结果
<==      Total: 1
A会话查询结果:{"id":1,"userName":"小肥羊二级会话更新测试结果"}

在这里我们看到跨会话查询的结果是最新的,也就解决了我们跨会话脏数据的问题了;除此之外,二级缓存还有标签开启的一种方式:

# 这里的 useCache 就是二级缓存的开启方法之一
<select id="getById" resultMap="resultMap" useCache="true">

在这里我们的一级缓存、二级缓存都是存在与内存中的,我们还可以借助三方来存储缓存,比如 Redis 等等。

二、插件

2.1 插件能拦截的方法有哪些?

其实在官网有说明的:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

除此之外,插件使用的是 JDK 的动态代理去实现的,后面代码会说明。

2.2 事例说明

比如我拦截到我的 SQL 去输出它的执行语句,及其出入参数(虽然这个代码已经有了,但是我们通过插件的角度来自己实现下,看看流程)。

自定义插件:

package com.mybatis;


import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@Intercepts({
        @Signature(
                //type=指拦截哪个接口;method=接口内的哪个方法名,直接点到上面那个接口,把方法复制出来  ; args= 拦截的方法的入参,复制该方法的出入参
                type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DemoTestPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime=System.currentTimeMillis();
        MappedStatement ms;
        String sql =null;
        Object parameter=null;
        Object result=null;
        try {
            Object[] args = invocation.getArgs();
            ms = (MappedStatement) args[0];
            parameter = args[1];
            BoundSql boundSql=ms.getBoundSql(parameter);
            sql =boundSql.getSql();
            result = invocation.proceed();
            return result;
        }finally {
            long endTime=System.currentTimeMillis();
            sql=sql.replaceAll("\n" ," ");
            System.out.println("语句:" + sql );
            System.out.println("入参:" + JSONObject.toJSONString(parameter));
            System.out.println("耗时:" +(endTime-startTime) +" ms");
            System.out.println("结果:" +JSONObject.toJSONString(result));
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

注册我们自己的插件:

<plugins>
        <plugin interceptor="com.mybatis.DemoTestPlugin"></plugin>
</plugins>

输出:

语句:SELECT           id, user_name        from deo_user where id= ?
入参:{"id":1,"param1":1}
耗时:279 ms
结果:[{"id":1,"userName":"小肥羊1000"}]

是不是很眼熟,这就是我们手动编写插件的效果,其实在我们工作中用得最多的就是分页的插件了,它实际上也是实现 Interceptor 来完成的。那么我们的插件工作流程是什么呢?

2.3 插件流程源码解读

2.3.1 插件配置解析

在 MyBatis 解析插件的时候:

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      //就是这一步,解析插件
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

然后会把所有的插件存放到一个 InterceptorChain 下一个集合属性里面:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        
        //将所有的插件都放到了 List<Interceptor> interceptors = new ArrayList<>();
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

2.3.2 自定义插件使用流程

当我们创建 openSession 的时候,我们会去调用 configuration.newExecutor(tx, execType) 方法:

 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);
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }

在最后我们会调用 interceptorChain.pluginAll 方法:

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

这里调用 pluginAll 方法:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

这里 interceptor.plugin(target); 调用的会找到所有对 Interceptor 接口的实现,这就到了我们自己的插件。

@Override
public Object plugin(Object target) {
  return Plugin.wrap(target,this);
}

点进去会发现最后的 JDK 动态代理:

public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      //很熟悉吧
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

时序图:

在这里插入图片描述

那么关于 MyBatis 基本源码解析就到这里了,其实里面还有修改的流程没有画,不过大致流程都相近的,看完它的源码实现,发现很多设计模式的运用,这种设计思路觉得很清晰的。

百度网盘 Demo下载地址:

pan.baidu.com/s/10kWReABL…

提取码:u8gt

最后,看完这篇文章的小伙伴要是觉得还行的话,麻烦点一个赞哦 !!!如果有不足的地方,可以留言,我会加以改进的哦 !!!