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. 用js实现千位分隔符

    function mm(num) { return num && num .toString() .replace(/(\d)(?=(\d{3})+\.)/g, function($0 ...

  2. ASP.NET Session详解笔记

    (一) 描述 当用户在 Web 应用程序中导航 ASP.NET 页时,ASP.NET 会话状态使您能够存储和检索用户的值.HTTP 是一种无状态协议.这意味着 Web 服务器会将针对页面的每个 HTT ...

  3. 45、如何使用python删除一个文件?

    若想利用python删除windows里的文件,这里需要使用os模块!那接下来就看看利用os模块是如何删除文件的! 具体实现方法如下! os.remove(path) 删除文件 path. 如果pat ...

  4. python基础===对字符串进行左右中对齐

    例如,有一个字典如下: >>> dic = { "name": "botoo", "url": "http:// ...

  5. C#技术分享【PDF转换成图片——11种方案】

    1.[iTextSharp.dll],C# 开源PDF处理工具,可以任意操作PDF,并可以提取PDF中的文字和图片,但不能直接将PDF转换成图片. DLL和源码 下载地址:http://downloa ...

  6. uWSGI+Nginx+Flask在Linux下的部署

    搞了一天多,终于搞通了uWSGI的部署原理,下面总结一下遇到的一些坑,希望给读者能够少走弯路.        简单来说,uWSGI是一个web服务器,Nginx进行反向代理的其实跟这些服务器可以说没有 ...

  7. 守护进程daemon函数

    #include  <unistd.h> int daemon(int nochdir,int noclose) 在创建精灵进程的时候,往往需要将精灵进程的工作目录修改为"/&q ...

  8. C语言调用正则表达式

    标准的C和C++都不支持正则表达式,但有一些函数库可以辅助C/C++程序员完成这一功能,其中最著名的当数Philip Hazel的Perl-Compatible Regular Expression库 ...

  9. Django 注册

    一. 本地图片上传预览 1. 上传文件框隐藏到图片上面,点击图片相当于点上传文件框 <div class="login"> <div style="po ...

  10. POJ 2337 Catenyms(欧拉回(通)路:路径输出+最小字典序)

    题目链接:http://poj.org/problem?id=2337 题目大意:给你n个字符串,只有字符串首和尾相同才能连接起来.请你以最小字典序输出连接好的单词. 解题思路:跟POJ1386一个意 ...