本文介绍mybatis的三个部分:Plugin、TypeHandler、二级缓存问题。
mybatis插件Plugin使用与原理
插件适用场景:通过拦截mybatis底层执行时的一些方法,可以在sql执行前改写sql,或者做路由配置等等。常用场景如下:分页插件、分表分库等等。。。。。
插件的使用:
1. 插件必须实现org.apache.ibatis.plugin.Interceptor接口。它可以拦截executor、statementHandler、resultHandler、parameterHandler接口。
由下图可见,插件在构建时候的入口只在这四个接口的实现之中。
2. 添加@Intercepts,@Signature注解完成对需要拦截的对象和接口的定义。@Signature可以表示唯一的一个方法签名,可以通过拦截多个方法。
3. 将插件配置至mybatis-config.xml文件中
举例一个简单的分页插件:
需要分页的dao层查询方法如下
@MybatisDao
public interface OperationLogDAO {
List<OperationLogDO> find(OperationLogQuery operationLogQuery,
RowBounds rowBounds);
}
分页插件实现类,实现Interceptor接口,添加拦截注解@Intercepts、@Signature
//.....
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PaginationInterceptor implements Interceptor {
//......
private Dialect dialect;
@Override
public Object intercept(Invocation invocation) throws Throwable {
processInterceptor(invocation.getArgs());
return invocation.proceed();
}
// 修改参数,加上LIMIT 参数
private void processInterceptor(final Object[] queryArgs) {
//.....获取到rowbounds参数的分页参数,做字符串拼接在sql的最后即可
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
将插件配置添加到mybatis配置文件中
<configuration>
<plugins>
<plugin interceptor="com.youzan.beauty.helper.mybatis.PaginationInterceptor">
<property name="dialectClass"
value="com.youzan.beauty.helper.mybatis.MySQLDialect"/>
</plugin>
</plugins>
</configuration>
通过上述配置插件的实现便已完成。但是它的原理是怎样的呢?用一句话解释就是:
在实例化executor、statementHandler、resultHandler、parameterHandler时生成的就是一个这些接口实例为目标的一个动态代理对象。仍然以分页插件例子来看,PaginationInterceptor是拦截executor的,所以实例化executor源码如下:
//org.apache.ibatis.session.Configuration#newExecutor源码
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//.....
//下面的一句话便是插件嵌入的过程
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
//org.apache.ibatis.plugin.InterceptorChain#pluginAll 方法源码如下
//将所有的自定义插件都做判断,满足条件的会生成代理类
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
//PaginationInterceptor的plugin方法可以直接用mybatis的plugin类的编织方法。
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//org.apache.ibatis.plugin.Plugin#wrap方法源码
public static Object wrap(Object target, Interceptor interceptor) {
//通过解析intercepter的注解,得到需要拦截的类与方法的map
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;
}
//Plugin是实现InvocationHandler的。它的invoke方法源码如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//如果执行的是插件需要拦截的目标方法,则执行插件的intercept方法。否则按原方法执行。
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
至此为止,就知道插件是如何实现的。通过生成目标类的动态代理实现。
mybatis的二级缓存问题
以前总听说mybatis二级缓存有问题,但总是记不住什么问题。这一次算是弄清楚了。
先做个总结:
mybatis二级缓存对象作用域是对应一个mapper配置文件的,就是mapper的作用域namespace。
假设有两个mapper文件 aMapper.xml, bMapper.xml。 (他们两的namespace不一样)
当开启二级缓存时,aMapper.xml用的是aCache这个缓存对象,bMapper.xml用的的bCache对象(因为二级缓存对象会保存在
一个mybatis的map里,这个map的key就是namespace,value就是二级缓存对象)。
aMapper有一个查询a表的方法,当第一次查询时会将缓存保存到aCache中,
以便第二次同样的查询直接可以取aCache缓存中的值。
但是如果bMapper对a表做了update/delete/insert方法后,它不会清除aCache的缓存
(更新操作只会清除本作用域下的缓存). 以至于aCache中的数据还是旧的
当再次在aMapper做同样的查询时查出的是旧数据。
二级缓存初始化源码:
<configuration>
<settings>
//cacheEnabled表示二级缓存是否开启
<setting name="cacheEnabled" value="true"/>
//.....
</settings>
</configuration>
//初始化二级缓存是在解析mybatis配置文件时候开始的。会将二级缓存对象放入configuration的caches属性中。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//初始化二级缓存实例时,id就是currentNamespace
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
mybatis的类型处理器typeHandler
根据上一篇mybatis框架解读可知:
mybatis参数和结果处理器ParameterHandler、ResultHandler用来设置参数和返回结果转换,它们的内部实现都是通过TypeHandler。TypeHandler是mybatis中将javatype和jdbctype相互转换的核心处理器。mybatis有大量默认的类型处理器,可以满足大部分需求。即使mapper配置文件不指定类型,它依然可以转化。当时需要转化我们自定义的类型时,则需要自定义类型处理器。自定义类型处理器只需要实现TypeHandler接口即可。
默认类型处理器都会在实例化configuration时注册在configuration的TypeHandlerRegistry属性中。
//org.apache.ibatis.type.TypeHandlerRegistry#TypeHandlerRegistry
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
//......
}