Mybatis之插件原理

鲁春利的工作笔记,好记性不如烂笔头



转载自:深入浅出Mybatis-插件原理

Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。

代理链的生成

Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。

下面以Executor为例。Mybatis在创建Executor对象时:

org.apache.ibatis.session.SqlSessionFactory.openSession()
    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession()
        org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(execType, level, false) {
            final Executor executor = configuration.newExecutor(tx, execType);
        }

org.apache.ibatis.session.Configuration

package org.apache.ibatis.session;
public class Configuration {

 protected Environment environment;

 protected String logPrefix;
  protected Class <? extends Log> logImpl;
  protected Class <? extends VFS> vfsImpl;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
  protected Integer defaultStatementTimeout;
  protected Integer defaultFetchSize;
  // 默认的Executor
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  
   // InterceptorChain里保存了所有的拦截器,它在mybatis初始化的时候创建。
   protected final InterceptorChain interceptorChain = new InterceptorChain();
   
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
}

org.apache.ibatis.plugin.InterceptorChain

package org.apache.ibatis.plugin;

/**
 * @author Clinton Begin
 */
public class InterceptorChain {

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

  /**
   * 每一个拦截器对目标类都进行一次代理
    * @param target
    * @return 层层代理后的对象 
   */
  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);
  }

}

下面以一个简单的例子来看看这个plugin方法里到底发生了什么。

package com.invicme.common.persistence.interceptor;

import java.sql.Connection;
import java.lang.reflect.Method;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 
 * @author lucl
 * @version 2017-01-09
 * 
 * 示例的分页拦截器:
 * <pre>
 *         @Intercepts用于表明当前的对象是一个Interceptor
 *         @Signature则表明要拦截的接口(type)、方法(method)以及对应的参数类型(args)
 *             type: Mybatis只支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。
 *             method: 这几个类的方法
 *             args:   这几个类的方法的参数
 * </pre>
 *
 */

@Intercepts(value = { 
        @Signature(type=Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type=StatementHandler.class, method="prepare", args={Connection.class})
    })
public class SamplePagingInterceptor implements Interceptor {
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    /**
     * 拦截方法
     * Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        logger.info("target is {}", target.getClass().getName());
        
        Method method = invocation.getMethod();
        logger.info("method is {}", method.getName());
        
        Object[] args = invocation.getArgs();
        for (Object arg : args) {
            logger.info("arg is {}", arg);
        }
        
        Object proceed = invocation.proceed();
        logger.info("proceed is {}", proceed.getClass().getName());
        
        return proceed;
    }

    /**
     * 用于封装目标对象,该方法可以返回目标对象本身,也可以返回一个它的代理对象。
     */
    @Override
    public Object plugin(Object target) {
        logger.info("target is {}", target.getClass().getName());
        return Plugin.wrap(target, this);
    }

    /**
     * 用于在Mybatis配置文件中指定一些属性
     */
    @Override
    public void setProperties(Properties properties) {
        String value = properties.getProperty("sysuser");
        logger.info("value is {}", value);
    }

}

每一个拦截器都必须实现上面的三个方法,其中:

1)       Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。

2)       Object plugin(Object target)就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来完成的,把目标target和拦截器this传给了包装函数。

3)       setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。

注解里描述的是指定拦截方法的签名  [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。

代理链的生成

package com.invicme.common.aop.proxy;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.PluginException;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.invicme.common.persistence.interceptor.SamplePagingInterceptor;

/**
 * 
 * @author lucl
 * @version 2017-01-18
 * 
 * 测试Class对象
 *
 */
public class MainDriver {

    private static Logger logger = LoggerFactory.getLogger(MainDriver.class);

    public static void main(String[] args) {
        SamplePagingInterceptor interceptor = new SamplePagingInterceptor();
        wrap(interceptor);
    }
    
    public static Object wrap (SamplePagingInterceptor interceptor) {
        // 获取拦截器对应的@Intercepts注解
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        {
            for (Iterator<Class<?>> it = signatureMap.keySet().iterator(); it.hasNext(); ) {
                Class<?> clazz = it.next();
                Set<Method> set = signatureMap.get(clazz);
                logger.info("【Signature】clazz name is {}", clazz.getName());
                for (Iterator<Method> itt = set.iterator(); itt.hasNext(); ) {
                    Method method = itt.next();
                    logger.info("\tmethod name is {}", method.getName());
                    Type[] genericParameterTypes = method.getGenericParameterTypes();
                    for (Type type : genericParameterTypes) {
                        logger.info("\t\tparam is {}", type);
                    }
                }
            }
        }
        
        logger.info("================================================================");
        {
            Configuration configuration = new Configuration();
            Executor executor = configuration.newExecutor(null);
            Class<?>[] interfaces = executor.getClass().getInterfaces();
            for (Class<?> clazz : interfaces) {
                logger.info("【Interface】clazz name is {}", clazz);
            }
        }
        
        return null;
    }

    /**
     * 获取拦截器对应的@Intercepts注解
     * @param interceptor
     * @return
     */
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        // issue #251
        if (interceptsAnnotation == null) {
            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
        for (Signature sig : sigs) {
            // 处理重复值的问题
            Set<Method> methods = signatureMap.get(sig.type());
            if (methods == null) {
                methods = new HashSet<Method>();
                signatureMap.put(sig.type(), methods);
            }
            try {
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException e) {
                throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
            }
        }
        return signatureMap;
    }
}

从前面可以看出,每个拦截器的plugin方法是通过调用Plugin.wrap方法来实现的。代码如下:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  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) {
      // 生成代理对象,Plugin对象为该代理对象的InvocationHandler
      //(InvocationHandler属于java代理的一个重要概念,不熟悉的请参考相关概念)  
      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类有三个属性:

private Object target;                                                        // 被代理的目标类

private Interceptor interceptor;                                         // 对应的拦截器

private Map<Class<?>, Set<Method>> signatureMap;    // 拦截器拦截的方法缓存

关于InvocationHandler请参阅:Java反射机制与动态代理

时间: 2024-12-28 03:25:44

Mybatis之插件原理的相关文章

MyBATIS插件原理第一篇——技术基础(反射和JDK动态代理)(转)

在介绍MyBATIS插件原理前我们需要先学习一下一些基础的知识,否则我们是很难理解MyBATIS的运行原理和插件原理的. MyBATIS最主要的是反射和动态代理技术,让我们首先先熟悉它们. 1:Java反射技术 在Java中反射技术已经大行其道,通过不断的优化性能得到了巨大的提高,而反射技术使得Java的可配置性大大提高.让我们来写一个服务打印hello + 姓名. import java.lang.reflect.InvocationTargetException; import java.l

Mybatis内部原理与插件原理

Mybatis的运行分为两大问题,第一部分是读取配置文件保存在Configuration对象中,用以创建SqlSessionFactory,第二部分是SqlSession的执行过程.相对而言SqlSessionFactory创建比较容易,而SqlSession的执行过程就没那么简单了. 构建SqlSessionFactory Mybatis采用构造模式去创建SqlSessionFactory,我们可以直接使用SqlSessionFactoryBuilder去创建,其内部原理分为两步: 第一步,通

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

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

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

第四篇 mybatis的运行原理(1):重要组件的介绍

一.组件介绍 相比其他框架,mybatis的组件构成还是相对简单的,下面是网络上比较常见的一张图,关于mybatis的执行流程,其中是mybatis的最常用的组件:   在来学习mybatis的源码和底层原理之前,先来了解各大组件的作用和生命周期:    (1)SqlSessionFactoryBuilder                  SqlSessionFactoryBuilder是利用XML或者Java编码获得资源来构建SqlSessionFactory的,通过它可以构建多个Sqls

Mybatis框架(9)---Mybatis自定义插件生成雪花ID做为表主键项目

Mybatis自定义插件生成雪花ID做为主键项目 先附上项目项目GitHub地址 spring-boot-mybatis-interceptor 有关Mybatis雪花ID主键插件前面写了两篇博客作为该项目落地的铺垫. 1.Mybatis框架---Mybatis插件原理 2.java算法---静态内部类实现雪花算法 该插件项目可以直接运用于实际开发中,作为分布式数据库表主键ID使用. 一.项目概述 1.项目背景 在生成表主键ID时,我们可以考虑主键自增 或者 UUID,但它们都有很明显的缺点 主

Mybatis分页插件

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

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

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

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中可以输入以上地址,也可以点