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. 搜索:DLX算法

    精确覆盖问题:在一个0-1矩阵中,选定部分行,使得每一列都有且只有一个1.求解一种选法 舞蹈链(Dance Link),也就是一个循环十字链表,可以快速的删掉和恢复某行某列 结合了舞蹈链的搜索就称作D ...

  2. Android 菜单和上下文演示

    在Action添加个TextView控件(上下文演示用); package com.example.test; import android.app.Activity;import android.o ...

  3. 面试C++失败

    到今天,面试已经整整一周,一个offer没有收到,mmp. 无奈,痛苦,迷茫. 以前活的太安逸,太舒适了. 自以为是,异想天开. 要重新振作起来. 要不断学习,保持强大,未来之路才会越走越宽.

  4. Yii2 的 redis 应用

    在应用的时候需要先对yii2进行扩展安装 如果装有composer直接运行 php composer.phar require --prefer-dist yiisoft/yii2-redis 当然也 ...

  5. linux学习记录.1.安装

    最近想了想决定开始学习linux. 在百度了一番后开始了安装,虚拟机VirtualBox,ubuntu. 基于VirtualBox虚拟机安装Ubuntu图文教程: http://blog.csdn.n ...

  6. wordpress 模板制作之一

    WP模板工作原理图:

  7. 24 - 面向对象基础-多继承-super-mro-Mixin

    目录 1 类的继承 2 不同版本的类 3 基本概念 4 特殊属性和方法 5 继承中的访问控制 6 方法的重写(override) 6.1 super 6.2 继承中的初始化 7 多继承 7.1 多继承 ...

  8. py,pyc,pyw文件的区别和使用

    熟悉python编程的都知道,python三种最常见的py文件格式,.py,.pyc,.pyw,下面说一说它们各自的使用. py文件 python最常见的文件,是python项目的源码: 文件执行时l ...

  9. 轻量级运维工具-pssh,pscp,prsync,pslurp,pnuke

    批量执行,并行传输,远程killall #yum安装 yum install pssh -y #yun安装后pscp改名为pscp.pssh #源码编译 wget https://pypi.pytho ...

  10. php 学习try_catch测试抛出异常

    /** * Class show * 一个catch接收抛出异常 */ class show { // 错误的演示 //try { //require ('test_try_catch.php'); ...