mybatis源码学习:插件定义+执行流程责任链


前文传送门:
mybatis源码学习:从SqlSessionFactory到代理对象的生成
mybatis源码学习:一级缓存和二级缓存分析
mybatis源码学习:基于动态代理实现查询全过程

一、自定义插件流程

  • 自定义插件,实现Interceptor接口。

  • 实现intercept、plugin和setProperties方法。

  • 使用@Intercepts注解完成插件签名。

  • 在主配置文件注册插件。

/**
 * 自定义插件
 * Intercepts:完成插件签名,告诉mybatis当前插件拦截哪个对象的哪个方法
 *
 * @author Summerday
 */
@Intercepts({
        @Signature(type = StatementHandler.class,method = "parameterize",args = Statement.class)
})
public class MyPlugin implements Interceptor {
    /**
     * 拦截目标方法执行
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyPlugin.intercept getMethod: "+invocation.getMethod());
        System.out.println("MyPlugin.intercept getTarget:"+invocation.getTarget());
        System.out.println("MyPlugin.intercept getArgs:"+ Arrays.toString(invocation.getArgs()));
        System.out.println("MyPlugin.intercept getClass:"+invocation.getClass());
        //执行目标方法
        Object proceed = invocation.proceed();
        //返回执行后的返回值
        return proceed;
    }

    /**
     * 包装目标对象,为目标对象创建一个代理对象
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("MyPlugin.plugin :mybatis将要包装的对象:"+target);
        //借助Plugin类的wrap方法使用当前拦截器包装目标对象
        Object wrap = Plugin.wrap(target,this);
        //返回为当前target创建的动态代理
        return wrap;
    }

    /**
     * 将插件注册时的properties属性设置进来
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的信息:" + properties);
    }
}

xml配置注册插件

    <!--注册插件-->
    <plugins>
        <plugin interceptor="com.smday.interceptor.MyPlugin">
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </plugin>
    </plugins>

二、测试插件

在这里插入图片描述

三、源码分析

1、inteceptor在Configuration中的注册

关于xml文件的解析,当然还是需要从XMLConfigBuilder中查找,我们很容易就可以发现关于插件的解析:

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //获取到全类名
        String interceptor = child.getStringAttribute("interceptor");
        //获取properties属性
        Properties properties = child.getChildrenAsProperties();
        //通过反射创建实例
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //设置属性
        interceptorInstance.setProperties(properties);
        //在Configuration中添加插件
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

  public void addInterceptor(Interceptor interceptor) {
    //interceptorChain是一个存储interceptor的Arraylist
    interceptorChain.addInterceptor(interceptor);
  }

此时初始化成功,我们在配置文件中定义的插件,已经成功加入interceptorChain。

2、基于责任链的设计模式

我们看到chain这个词应该并不会陌生,我们之前学习过的过滤器也存在类似的玩意,什么意思呢?我们以Executor为例,当创建Executor对象的时候,并不是直接new Executor然后返回:

在这里插入图片描述

在返回之前,他进行了下面的操作:

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

我们来看看这个方法具体干了什么:

  public Object pluginAll(Object target) {
    //遍历所有的拦截器
    for (Interceptor interceptor : interceptors) {
        //调用plugin,返回target包装后的对象
      target = interceptor.plugin(target);
    }
    return target;
  }

很明显,现在它要从chain中一一取出interceptor,并依次调用各自的plugin方法,暂且不谈plugin的方法,我们就能感受到责任链的功能:让一个对象能够被链上的任何一个角色宠幸,真好。

3、基于动态代理的plugin

那接下来,我们就成功进入我们自定义plugin的plugin方法:

在这里插入图片描述

  //看看wrap方法干了点啥
  public static Object wrap(Object target,Interceptor interceptor) {
    //获取获取注解的信息,拦截的对象,拦截的方法,拦截方法的参数。
    Map<Class<?>,Set<Method>> signatureMap = getSignatureMap(interceptor);
    //获取当前对象的Class
    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;
  }

getSignatureMap(interceptor)方法:其实就是获取注解的信息,拦截的对象,拦截的方法,拦截方法的参数。

  private static Map<Class<?>,Set<Method>> getSignatureMap(Interceptor interceptor) {
    //定位到interceptor上的@Intercepts注解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
	//如果注解不存在,则报错
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //获取@Signature组成的数组
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>,Set<Method>> signatureMap = new HashMap<Class<?>,Set<Method>>();
    
    for (Signature sig : sigs) {
      //先看map里有没有methods set
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        //没有再创建一个
        methods = new HashSet<Method>();
        //class:methods设置进去
        signatureMap.put(sig.type(),methods);
      }
      try {
        //获取拦截的方法
        Method method = sig.type().getMethod(sig.method(),sig.args());
        //加入到set中
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,e);
      }
    }
    return signatureMap;
  }

getAllInterfaces(type,signatureMap)方法:确定是否为拦截对象

  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()) {
        //如果确实是拦截的对象,则加入interfaces set
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      //从父接口中查看
      type = type.getSuperclass();
    }
    //最后set里面存在的元素就是要拦截的对象
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

我们就可以猜测,插件只会对我们要求的对象和方法进行拦截。

4、拦截方法的intercept(invocation)

确实,我们一路debug,遇到了Executor、ParameterHandler、ResultHandler都没有进行拦截,然而,当StatementHandler对象出现的时候,就出现了微妙的变化,当我们调用代理的方法必然会执行其invoke方法,不妨来看看:

在这里插入图片描述

ok,此时进入了我们定义的intercept方法,感觉无比亲切。

在这里插入图片描述

  //调度被代理对象的真实方法
  public Object proceed() throws InvocationTargetException,IllegalAccessException {
    return method.invoke(target,args);
  }

如果有多个插件,每经过一次wrap都会产生上衣个对象的代理对象,此处反射调用的方法也是上衣个代理对象的方法。接着,就还是执行目标的parameterize方法,但是当我们明白这些执行流程的时候,我们就可以知道如何进行一些小操作,来自定义方法的实现了。

四、插件开发插件pagehelper

插件文档地址:https://github.com/pagehelper/Mybatis-PageHelper

这款插件使分页操作变得更加简便,来一个简单的测试如下:

1、引入相关依赖

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.2</version>
        </dependency>

2、全局配置

    <!--注册插件-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
    </plugins>

3、测试分页

    @Test
    public void testPlugin(){
        //查询第一页,每页3条记录
        PageHelper.startPage(1,3);
        List<User> all = userDao.findAll();
        for (User user : all) {
            System.out.println(user);
        }
    }

在这里插入图片描述

五、插件总结

参考:《深入浅出MyBatis技术原理与实战》

  • 插件生成地是层层代理对象的责任链模式,其中设计反射技术实现动态代理,难免会对性能产生一些影响。
  • 插件的定义需要明确需要拦截的对象、拦截的方法、拦截的方法参数。
  • 插件将会改变MyBatis的底层设计,使用时务必谨慎。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


1.pom.xml引入依赖 &lt;dependency&gt; &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt; &lt;artifactId&gt;pagehelper&lt;/artifactId&gt; &lt;version&gt;5
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt; &lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &qu
准备工作 ① 创建数据库&amp;数据表 ## 创建数据库 CREATE DATABASE `dbtest1`; ## 创建数据表 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(20) DEF
MyBatis逆向工程是指根据数据库表结构自动生成对应的实体类、Mapper接口以及SQL映射文件的过程。这个过程可以通过MyBatis提供的逆向工程工具来完成,极大地方便了开发人员,避免了重复的代码编写,提高了开发效率。 创建逆向工程的步骤 1、添加依赖&amp;插件 &lt;!-- 控制Mave
MyBatis获取参数值的两种方式:${}和#{} ${}的本质就是字符串拼接,#{}的本质就是占位符赋值。 ${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自
resultMap作用是处理数据表中字段与java实体类中属性的映射关系。 准备工作 ① 创建数据库&amp;数据表 CREATE DATABASE `dbtest1`; CREATE TABLE `t_emp` ( `emp_id` int NOT NULL AUTO_INCREMENT, `em
EHCache缓存针对于MyBatis的二级缓存。 MyBatis默认二级缓存是SqlSessionFactory级别的。 添加依赖 &lt;!-- MyBatis-EHCache整合包 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.mybatis.cac
MyBatis 提供了一级缓存和二级缓存的支持,用于提高数据库查询的性能,减少不必要的数据库访问。 一级缓存(SqlSession 级别的缓存) 一级缓存是 MyBatis 中最细粒度的缓存,也称为本地缓存。它存在于每个 SqlSession 的生命周期中,当 SqlSession 被关闭或清空时,
动态SQL是 MyBatis 中非常强大且灵活的功能,允许你根据不同的条件构建SQL查询。 这主要通过 &lt;if&gt;、&lt;choose&gt;、&lt;when&gt;、&lt;otherwise&gt;、&lt;foreach&gt;等标签实现。 查询场景 /** * 根据条件查询员工
本教程操作系统:windows10系统、DELL G3电脑。 MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。在 MyBatis 中,配置数据库连接是非常重要的第一步。下面将详细介绍如何配置 MyBatis 的
今天小编给大家分享的是MyBatis批量查询、插入、更新、删除如何实现,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。
今天小编给大家分享的是Mybatis操作多数据源实现的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇文章和大家了解一下mybatis集成到spring的方式有哪些。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1 前言1.1 集成spring前使用mybat...
今天小编给大家分享的是mybatis-plus分页查询的3种方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇内容主要讲解“mybatis之BaseTypeHandler怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“mybatis...
这篇文章主要介绍了mybatisforeach怎么传两个参数的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇mybatisforeach怎...
这篇“MyBatis映射文件中parameterType与resultType怎么使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的...
这篇文章主要介绍“MyBatis怎么获取自动生成的键值”,在日常操作中,相信很多人在MyBatis怎么获取自动生成的键值问题上存在疑惑,小编查阅了各式资料,整理出
这篇文章主要讲解了“怎么去掉IntelliJIDEA中mybatis对应的xml文件警告”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入...
这篇文章主要介绍“MybatisPlus使用@TableId主键id自增长无效如何解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这...