MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。

默认情况下,可以使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

Mybatis提供的插件很强大,可以由外切入到底层的核心模块,所以用起来要非常小心,至少要熟悉一些底层的原理。

但是使用起来还是很简单,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

接着来剖析下插件的原理。

配置与解析

以开源的某个分页插件为例:

<plugins>
<!-- 分页操作对象 -->
<plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
<property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.MySQLDialect" />
</plugin>
</plugins>

解析配置文件:

private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
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);
}
}

处理插件元素:

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);
configuration.addInterceptor(interceptorInstance);
}
}
}

Configuration中持有interceptorChain:

protected final InterceptorChain interceptorChain = new InterceptorChain();

把配置文件中的插件添加到InterceptorChain中:

  public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}

InterceptorChain中持有Interceptor集合,每个Interceptor都被添加到这个集合中。

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

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

切入并执行

拦截器接口:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

DefaultSqlSessionFactory

final Executor executor = configuration.newExecutor(tx, execType);

使用Configuration的newExecutor方法来创建Executor:

 public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
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, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

同理还有
- Configuration#newParameterHandler
- Configuration#newResultSetHandler
- Configuration#newStatementHandler

就是对应上面的几处埋点,在Configuration中构造的时候使用了下面的方法挂载插件:

executor = (Executor) interceptorChain.pluginAll(executor);

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

这是一个层层包裹的过程,后面的拦截器会不断包装前面的。最后一个拦截器的方法会优先执行,然后层层代理。

一般在插件的XxInterceptor实现中,会包装一个代理类:

@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class OffsetLimitInterceptor implements Interceptor { @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
} //...... }

Plugin类的包装方法:

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;
}

这个包装方法首先会获取拦截器的相关注解(Intercepts,Signature),构造一个

Map

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}

如果指定的type有接口就创建代理,否则返回其本身。

根据Plugin的实现:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
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);
}
}

首先检查签名集合中是否包含指定的方法,如果没有则直接调用目标方法。

如果匹配上就new一个Invocation传给interceptor的intercept方法,到时候在自定义的interceptor中可以直接通过invocation.proceed()调用目标方法:

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}

有点类似过滤器的放行。

插件的实现就是在几处需要拦截的地方用动态代理技术来生成代理类,以拦截器的形式完成方法的增强。根据@Signature注解的type元素的接口有无来判断是否生成代理类,根据指定方法和注解的method是否一致来决定是否调用拦截器方法。

使用动态代理技术让我们可以切入到底层的实现却不用修改底层的代码,就像一个楔子插进去也可以拔出来,这就是所谓的插件。

插件相关的总结

插入

  • 插件是通过JDK动态代理实现的

这是插件的原理和核心。

了解了动态代理的机制,再写个例子看下JDK为我们生成的代理类,可以有个更清晰的把握。

  • MetaObject在插件中的运用

可以通过MetaObject利用反射完成相关属性的赋值,偷梁换柱达到我们的目的。

不管有多少拦截器,只要满足条件,就是一层层的代理,我们可以层层剥离它,如针对StatementHandler的prepare方法的一个插件:

MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 层层剥离
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(object);
}

只要它含有h属性,就说明它是一个代理(生成的代理类是继承自Proxy的,所以继承了InvocationHandler类型的h)。

具体代码可以点此查看:
模拟分页插件

拔出

  • Invocation就是让方法切回去

Invocation封装了让原方法执行的必要参数,并提供了一个proceed方法:

public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}

当你拦截了指定方法,做了该做的事之后,调用Invocation的proceed方法可以让框架继续执行。

  • 方法切回去后是否达到目的

当我们为了达到某个目的写了一个插件,要考虑当前的一系列操作是否会对后面方法的执行造成影响。

有时候我们需要更改某些东西,这些东西改完是否能顺利set进去(几大关键类提供的set方法不多);有时候是想获取某些东西,不能影响后续的执行。这些都要对底层的方法有所了解。

Mybatis源码分析之插件的原理的更多相关文章

  1. 【MyBatis源码分析】插件实现原理

    MyBatis插件原理----从<plugins>解析开始 本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyB ...

  2. MyBatis源码分析(2)—— Plugin原理

    @(MyBatis)[Plugin] MyBatis源码分析--Plugin原理 Plugin原理 Plugin的实现采用了Java的动态代理,应用了责任链设计模式 InterceptorChain ...

  3. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

  4. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  5. 精尽MyBatis源码分析 - 插件机制

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  6. Mybatis源码分析--关联表查询及延迟加载原理(二)

    在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...

  7. MyBatis源码分析(各组件关系+底层原理

    MyBatis源码分析MyBatis流程图 下面将结合代码具体分析. MyBatis具体代码分析 SqlSessionFactoryBuilder根据XML文件流,或者Configuration类实例 ...

  8. Mybatis源码分析之Cache二级缓存原理 (五)

    一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...

  9. MyBatis 源码分析系列文章合集

    1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...

随机推荐

  1. POJ 1389 Area of Simple Polygons 扫描线+线段树面积并

    ---恢复内容开始--- LINK 题意:同POJ1151 思路: /** @Date : 2017-07-19 13:24:45 * @FileName: POJ 1389 线段树+扫描线+面积并 ...

  2. CF821 D. Okabe and City 图 最短路

    Link 题意:给出$n*m$大小的地图,已有$k$盏灯亮,人从左上角出发,右下角结束,期间必须走路灯点亮的地方,他可以在任意时刻消耗一枚硬币点亮一行或一列灯,他最多同时点亮一行或一列灯,要想点亮别的 ...

  3. python中拷贝对象的区别

    一.赋值.引用 在python中赋值语句总是建立对象的引用值,而不是复制对象.因此,python变量更像是指针,而不是数据存储区域 这点和大多数语音类似吧,比如C++.Java等 1.先看个例子: v ...

  4. js javascript变量提升

    var:变量提升(无论声明在何处,都会被提至其所在作用域的顶部) let:无变量提升(所在的块内,未到let声明时(即let声明之前),是无法访问该变量的(not defined)),let变量不能重 ...

  5. html css 如何将表头固定

    position属性取值为fixed时,则元素的位置将不受滚动条的影响,而是直接依据窗口定位,这就是将表头固定的最直接方法,网上其他途径感觉都是在走弯路.但是与此同时必须解决两个问题.第一:表体将随之 ...

  6. Go语言 2 变量、常量和数据类型

    文章由作者马志国在博客园的原创,若转载请于明显处标记出处:http://www.cnblogs.com/mazg/ Go学习群:415660935 2.1 变量 变量是对一块内存空间的命名,程序可以通 ...

  7. python之supervisor进程管理工具

    supervisor是python写的一个管理进程运行的工具,可以很方便的监听.启动.停止.重启一个或多个进程:有了supervisor后,就不用字节写启动和监听的shell脚本了,非常方便. sup ...

  8. option和 usb-serial驱动基本区别

    option.c This driver exists because the "normal" serial driver doesn't work too well   wit ...

  9. nginx与PHP的关系和交互方式【转】

    nginx与PHP的关系. 对比, apache和PHP的关系, 将PHP安装成apache的一个功能模块, 导致的结果, 对外只有一个apache程序, PHP并不独立出现, 仅仅是apache的模 ...

  10. fastDFS 命令笔记【转】

    端口开放 这是命令运行的前提 iptables -I INPUT -p tcp -m state –state NEW -m tcp –dport 22 -j ACCEPT iptables -I I ...