一、缓存
在 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 插件能拦截的方法有哪些?
其实在官网有说明的:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- 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下载地址:
提取码:u8gt
最后,看完这篇文章的小伙伴要是觉得还行的话,麻烦点一个赞哦 !!!如果有不足的地方,可以留言,我会加以改进的哦 !!!