目录
一、缓存
1.1 缓存-一级缓存的设计与原理
一级缓存基于 SqlSession
,所以我们可以直接创建 SqlSessionFactory
,并从中开启一个新的 SqlSession
,默认情况下它会自动开启事务,所以一级缓存会自动使用。
实际我们在第三章讲解excute的query源码里面就有一级查询的源码,我们来看一下:
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// ......
// 如果statement指定了需要刷新缓存,则清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 查询之前先检查一级缓存中是否存在数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 有,则直接取缓存
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 没有,则查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
// ......
// 全局localCacheScope设置为statement,则清空一级缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
这里查询的时候直接先去根据查询statment的特性拼接CacheKey,然后根据CacheKey查一级缓存,有的话直接存入拿出赋值给查询结果。
这个缓存的生成是什么时候呢:
// queryFromDatabase 方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql
boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
以上代码说明:一级缓存放置时机,实际就是queryFromDatabase方法中,直接将查询数据库结果封装后结果类放到一级缓存。
总结一下可以发现,一级缓存起作用的位置,是在向数据库发起查询之前,先拦截检查一下,如果一级缓存中有数据,则直接从缓存中取数据并返回,否则才查询数据库,然后放入一级缓存。
实际一级缓存是有一定问题的,大家看源码会发现,一级缓存操作直接操作的都是一个对象,将查询结果直接放入缓存,然后查询缓存赋值给结果的是相同对象地址,导致在同一个sqlsession中查询后,对结果类进行修改,在进行查询,会查询缓存拿到修改后的对象,这是需要注意的。
1.2 缓存-二级缓存的设计与原理
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件namespace的,也 就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
1.2.1 开启二级缓存
和一级缓存默认开启不一样,二级缓存需要我们手动开启。最最简单的使用方式,只需要在 mapper.xml 上打一个 <cache />
标签,就算开启二级缓存了:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.linkedbear.mybatis.mapper.DepartmentMapper">
<cache />
<!-- ...... -->
</mapper>
对应的 Mapper 接口,则需要用 @CacheNamespace
注解开启:
@CacheNamespace
public interface DepartmentMapper {
// ......
}
另外,不要忘记给实体类实现 Serializable
接口,否则二级缓存也是不能用的。
1.2.2 二级缓存源码分析
CachingExecutor
// CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds
rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
// 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
// 我们在上面解析blog.xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
Cache cache = ms.getCache();
// 如果配置文件中没有配置 <cache>,则 cache 为空
if (cache != null) {
//如果需要刷新缓存的话就刷新:flushCache="true"
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 访问二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
// 缓存未命中
if (list == null) {
// 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的
话,则进行DB查询
list = delegate.<E>query(ms, parameterObject, rowBounds,
resultHandler, key, boundSql);
// 缓存查询结果
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key,boundSql);
}
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。
- 二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
- 二级缓存具有丰富的缓存策略。
- 二级缓存可由多个装饰器,与基础缓存组合而成
- 二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache完成。
一级缓存在第一次查出数据后,在一个sqlsession中,若果直接修改该数据,之后第二次查询时,从一级缓存中查出来的数据是被修改过的,并非数据库的真实数据,原因是 MyBatis 利用一级缓存是直接将数据的引用交出去了。
二级缓存就不一样了,我们从二级缓存中查出来的数据那可是跨 SqlSession 的,谁知道你改不改数据(还不敢保证改的对不对),万一你改了那别人从二级缓存中拿的数据就是被你改过的,这样万一出点问题,那可就出大事了。MyBatis 自然帮我们考虑到了这一点,于是它给二级缓存设计了一个只读属性。这个只读属性如果设置为 true ,则通过二级缓存查询的数据会执行一次基于 jdk 序列化的对象深拷贝,这样就可以保证拿到的数据不会对原二级缓存产生影响(但一次对象的深拷贝会导致性能降低);而 readOnly
设置为 false ,则只读的缓存会像一级缓存那样,直接返回二级缓存本身,虽然可能不安全,但好在处理速度快。
二、Mapper代理方式
2.1 getmapper()
进入源码 sqlSession.getMapper(UserMapper.class )中:
//DefaultSqlSession 中的 getMapper
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
//configuration 中的给 getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//MapperRegistry 中的 getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从 MapperRegistry 中的 HashMap 中拿 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>)
knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the
MapperRegistry.");
}
try {
//通过动态代理工厂生成示例。
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: "
+ e, e);
}
}
//MapperProxyFactory 类中的 newInstance 方法
public T newInstance(SqlSession sqlSession) {
//创建了 JDK动态代理的Handler类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession,
mapperInterface, methodCache);
//调用了重载方法
return newInstance(mapperProxy);
}
//MapperProxy 类,实现了 InvocationHandler 接口
public class MapperProxy<T> implements InvocationHandler, Serializable {
//省略部分源码
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
//构造,传入了 SqlSession,说明每个session中的代理对象的不同的!
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface,
Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
//省略部分源码
}
2.2 invoke()
在动态代理返回了示例后,我们就可以直接调用mapper类中的方法了,但代理对象调用方法,执行是在MapperProxy中的invoke方法中:
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
try {
//如果是Object定义的方法,直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 获得 MapperMethod 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
//重点在这:MapperMethod最终调用了执行的方法
return mapperMethod.execute(sqlSession, args);
}
进入execute方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断mapper中的方法类型,最终调用的还是SqlSession中的方法 switch
(command.getType()) {
case INSERT: {
//转换参数
Object param = method.convertArgsToSqlCommandParam(args);
//执行INSERT操作
// 转换 rowCount
result = rowCountResult(sqlSession.insert(command.getName(),
param));
break;
}
case UPDATE: {
//转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.update(command.getName(),
param));
break;
}
case DELETE: {
//转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.delete(command.getName(),
param));
break;
}
case SELECT:
//无返回,并且有ResultHandler方法参数,则将查询的结果,提交给 ResultHandler 进行
处理
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
//执行查询,返回列表
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
//执行查询,返回Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
//执行查询,返回Cursor
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
//执行查询,返回单个对象
} else {
//转换参数
Object param = method.convertArgsToSqlCommandParam(args);
//查询单条
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null ||
!method.getReturnType().equals(result.getClass())))
{
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " +
command.getName());
}
//返回结果为null,并且返回类型为基本类型,则抛出BindingException异常
if(result ==null&&method.getReturnType().isPrimitive()
&&!method.returnsVoid()){
throw new BindingException("Mapper method '" + command.getName() + "
attempted to return null from a method with a primitive
return type(" + method.getReturnType() + "). ");
}
//返回结果
return result;
}
三、mybatis插件
3.1 插件介绍
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
3.2 插件原理
Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件
(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。
MyBatis所允许拦截的方法如下:
- 执行器Executor (update、query、commit、rollback等方法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
在四大对象创建的时候
- 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
- 获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返回 target 包装后的对象
- 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler)
interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象 。
3.3 简单应用
如果我们想要拦截Executor的update方法,那么可以这样定义插件:
@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}))
public class CustomInterceptor implements Interceptor {
//这里是每次执行操作的时候,都会进行这个拦截器的方法内
Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对方法进行了增强....");
return invocation.proceed(); //执行原方法
}
除此之外,我们还需将插件配置到sqlMapConfig.xm l中。
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
</plugin>
</plugins>