深入浅出MyBatis:MyBatis插件及开发过程

本篇文章是「深入浅出MyBatis:技术原理与实践」书籍的总结笔记。

上一篇介绍了 MyBatis解析和运行原理 ,包括SqlSessionFactory的构建和SqlSession的执行过程,其中,SqlSession包含四大对象,可以在四大对象调度的时候插入自定义的代码,以满足特殊的需求,这便是MyBatis提供的插件技术。

有些特殊场景,需要使用插件统一处理,比如:在进行多租户开发时,数据要按租户隔离,可以在sql语句后面统一添加租户编号筛选条件。

本篇就来介绍下插件,通过本篇的介绍,你会了解到:

  • 插件接口和初始化
  • 插件的代理和反射设计
  • 工具类MetaObject介绍
  • 插件的开发过程

插件的接口和初始化分析

插件接口

在MyBatis中使用插件,需要实现Interceptor接口,定义如下:

public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);
}

详细说说这3个方法:

  • intercept:它将直接覆盖你所拦截的对象,有个参数Invocation对象,通过该对象,可以反射调度原来对象的方法;
  • plugin:target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象;
  • setProperties:允许在plugin元素中配置所需参数,该方法在插件初始化的时候会被调用一次;
插件初始化

插件的初始化时在MyBatis初始化的时候完成的,读入插件节点和配置的参数,使用反射技术生成插件实例,然后调用插件方法中的setProperties方法设置参数,并将插件实例保存到配置对象中,具体过程看下面代码。

plugin配置示例如下:

<plugins>
    <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin">
        <property name="dbType" value="mysql"/>
    </plugin>
<plugins>

插件初始化过程:

public class XMLConfigBuilder extends BaseBuilder {
  ......
  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的添加插件方法:

public class Configuration {
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }
}

InterceptorChain是一个类,主要包含一个List属性,保存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 List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

插件的代理和反射设计原理

责任链模式

插件用的是责任链模式,责任链模式是一种对象行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理此请求。

设计细节

前面提到了InterceptorChain类,其中有个pluginAll方法,责任链就是在该方法定义的。

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

上面介绍过plugin方法,它是生成代理对象的方法,从第一个对象(四大对象中的一个)开始,将对象传递给了plugin方法,返回一个代理;如果存在第二个插件,就拿着第一个代理对象,传递给plugin方法,返回第一个代理对象的代理.....

plugin方法是需要我们去实现的,如何生成代理类呢,MyBatis提供了Plugin工具类,它实现了InvocationHandler接口(JDK动态代理的接口),看看它的2个方法:

public class Plugin implements InvocationHandler {
  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;
  }

  @Override
  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);
    }
  }
}

分析下这块代码,Plugin提供了静态方法wrap方法,它会根据插件的签名配置,使用JDK动态代理的方法,生成一个代理类,当四大对象执行方法时,会调用Plugin的invoke方法,如果方法包含在声明的签名里,就会调用自定义插件的intercept方法,传入Invocation对象。

另外,Invocation对象包含一个proceed方法,这个方法就是调用被代理对象的真实方法,如果有n个插件,第一个传递的参数是四大对象本身,然后调用一次wrap方法产生第一个代理对象,这里的反射就是四大对象的真实方法,如果有第二个插件,这里的反射就是第一个代理对象的invoke方法。

所以,在多个插件的情况下,调度proceed方法,MyBatis总是从最后一个代理对象运行到第一个代理对象,最后是真实被拦截的对象方法被执行。

工具类MetaObject介绍

MetaObject是MyBatis给我们提供的工具类,它可以有效的获取或修改一些重要对象的属性。

举例说明,我们拦截StatementHandler对象,首先要获取它要执行的SQL,添加返回行数限制。

编写一个自定义插件,实现intercept方法,方法实现如下

StatementHandler statementHandler=(StatementHandler)invocation.getTarget();
MetaObject metaObj=SystemMetaObject.forObject(statementHandler);

//获取sql
String sql=(String)metaStatementHandler.getValue("delegate.bound.sql");
//添加limit条件
sql="select * from (" + sql + ") limit 1000";
//重新设置sql
metaStatementHandler.setValue("delegate.bound.sql",sql);

插件的开发过程

最后总结下插件的开发步骤。

确定要拦截的签名
  • 确定要拦截的对象,四大对象之一;
  • 确定拦截的方法和参数;

比如想拦截StatementHandler对象的prepare方法,该方法有一个参数Connection对象,可以这样声明:

@Intercepts({
    @Signature(type =StatementHandler.class,
        method="prepare" ,
        args={Connection.class})})
public class MyPlugin implements Interceptor{
    ......
}
定义插件类,实现拦截方法

上面已经分析过原理,实现Interceptor接口的方法即可,通过Plugin工具类方便生成代理类,通过MetaObject工具类方便操作四大对象的属性,修改对应的值。

配置

最后配置自定义的插件:

<plugins>
    <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin">
        <property name="dbType" value="mysql"/>
    </plugin>
<plugins>

自定义插件还是比较复杂的,如果不了解原理,很容易出错,能不用插件尽量不要使用,因为它是修改MyBatis的底层设计。 插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,要考虑全面,特别是多个插件层层代理的逻辑。

下一篇会介绍MyBatis与Spring的集成。

欢迎扫描下方二维码,关注我的个人微信公众号 ~

原文地址:http://blog.51cto.com/13714880/2112522

时间: 2024-10-02 23:59:04

深入浅出MyBatis:MyBatis插件及开发过程的相关文章

Mybatis分页插件PageHelper的配置和使用方法

http://www.cnblogs.com/kangoroo/p/7998433.html 前言 在web开发过程中涉及到表格时,例如dataTable,就会产生分页的需求,通常我们将分页方式分为两种:前端分页和后端分页. 前端分页 一次性请求数据表格中的所有记录(ajax),然后在前端缓存并且计算count和分页逻辑,一般前端组件(例如dataTable)会提供分页动作. 特点是:简单,很适合小规模的web平台:当数据量大的时候会产生性能问题,在查询和网络传输的时间会很长. 后端分页 在aj

Mybatis分页插件

Mybatis分页插件 - PageHelper说明 如果你也在用Mybatis,建议尝试该分页插件,这个一定是最方便使用的分页插件. 该插件目前支持Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库分页. 点击提交BUG 版本说明 最新版本为3.7.5 PageInfo中的judgePageBoudary方法修改: isLastPage = pageNum == pages && pageNum != 1; //改为 isLastPage

Mybatis分页插件2.0版本发布

项目地址:http://git.oschina.net/free/Mybatis_PageHelper 分页插件示例: http://blog.csdn.net/isea533/article/details/24700339 v2.0更新内容: 支持Mybatis缓存,count和分页同时支持(二者同步) 修改拦截器签名,拦截Executor,签名如下: @Intercepts(@Signature(type = Executor.class, method = "query", a

Mybatis分页-利用Mybatis Generator插件生成基于数据库方言的分页语句,统计记录总数 (转)

众所周知,Mybatis本身没有提供基于数据库方言的分页功能,而是基于JDBC的游标分页,很容易出现性能问题.网上有很多分页的解决方案,不外乎是基于Mybatis本机的插件机制,通过拦截Sql做分页.但是在像Oracle这样的数据库上,拦截器生成的Sql语句没有变量绑定,而且每次语句的都要去拦截,感觉有点浪费性能. Mybatis Generator是Mybatis的代码生成工具,可以生成大部分的查询语句. 本文提供的分页解决方案是新增Mybatis Generator插件,在用Mybatis

关于struts2的过滤器和mybatis的插件的分析

网上一搜,发现一篇写的非常棒的博文,就直接复制过来了,供以后复习使用. 前辈博文链接:共三篇: http://jimgreat.iteye.com/blog/1616671: http://jimgreat.iteye.com/blog/1594981: http://jimgreat.iteye.com/blog/1594982: 以下为第一篇: 其实无论是AOP.拦截器还是Plugin 都是通过对目标点,一般来说就是对函数的拦截,扩展原有的功能,增加切面逻辑(日志,权限验证),修改上下文运行

Myeclipse2014添加mybatis generator插件

Myeclipse2014把mybatis generator插件直接放在dropins文件夹下,重启后不能成功安装mybatis插件. 既然离线安装不成功,可以选择在线安装 1.选择 Help->Install from site... 2.在弹出的对话框中点击右上角的Add按钮. 3.在弹出的对话框中输入 Name:mybatis Location:https://dl.bintray.com/mybatis/mybatis-generator (location中可以输入以上地址,也可以点

eclipse MyBatis Generator 插件的安装

今天做项目要用MyBatis Generator生成持久层,所以去网上找了 MyBatis Generator插件的地址. 但是发现地址链接都过期了.而离线版的都是直接将文件拷入eclipse插件文件夹的,这就会导致以后插件依赖冲突找不到解决方案. 后来才发现,eclipse 有自己的market 如图 点进去搜索mybatis就会看到MyBatis Generator 插件.接下来一路next就好了

Mybatis分页插件更新

分页插件介绍:http://blog.csdn.net/isea533/article/details/23831273 分页插件示例:http://blog.csdn.net/isea533/article/details/24700339 如果你也在用Mybatis,建议尝试该分页插件,一定是最方便使用的分页插件. 下载最新版代码:http://pan.baidu.com/s/1hq1KIos 最近使用分页插件时,发现一些特殊情况下报错的问题,报错的地方经过分析发现时求count时候的参数有

Java Mybatis Plugin插件实现分表路由规则

Mybatis Plugin插件种类 mybatis支持对于Executor.StatementHandler.PameterHandler.ResultSetHandler做拦截.要想通过拦截器做分表路由可以在Executor或StatementHandler两个阶段进行拦截.本次的路由实现是在StatementHandler拦截Sql在通过Rule修改Sql的表名,这样系统原有的Sql不用修改表名会自动替换成路由计算出的表名. 定义mybatis-config.xml配置文件 <plugin