持续原创输出,点击上方蓝字关注我吧

目录

  • 前言
  • 环境配置
  • 什么是插件?
  • 如何自定义插件?

    • 举个栗子
    • 用到哪些注解?
    • 如何注入Mybatis?
    • 测试
  • 插件原理分析

    • 如何生成代理对象?
    • 如何执行?
    • 总结
  • 分页插件的原理分析
  • 总结

前言

  • Mybatis的分页插件相信大家都使用过,那么可知道其中的实现原理?分页插件就是利用的Mybatis中的插件机制实现的,在Executorquery执行前后进行分页处理。
  • 此篇文章就来介绍以下Mybatis的插件机制以及在底层是如何实现的。

环境配置

  • 本篇文章讲的一切内容都是基于Mybatis3.5SpringBoot-2.3.3.RELEASE

什么是插件?

  • 插件是Mybatis中的最重要的功能之一,能够对特定组件的特定方法进行增强。
  • MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

    • 「Executor」update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
    • 「ParameterHandler」: getParameterObject, setParameters
    • 「ResultSetHandler」handleResultSets, handleOutputParameters
    • 「StatementHandler」: prepare, parameterize, batch, update, query

如何自定义插件?

  • 插件的实现其实很简单,只需要实现Mybatis提供的Interceptor这个接口即可,源码如下:
  1. public interface Interceptor {
  2. //拦截的方法
  3. Object intercept(Invocation invocation) throws Throwable;
  4. //返回拦截器的代理对象
  5. Object plugin(Object target);
  6. //设置一些属性
  7. void setProperties(Properties properties);
  8. }

举个栗子

  • 有这样一个需求:需要在Mybatis执行的时候篡改selectByUserId的参数值。
  • 「分析」:修改SQL的入参,应该在哪个组件的哪个方法上拦截篡改呢?研究过源码的估计都很清楚的知道,ParameterHandler中的setParameters()方法就是对参数进行处理的。因此肯定是拦截这个方法是最合适。
  • 自定义的插件如下:
  1. /**
  2. * @Intercepts 注解标记这是一个拦截器,其中可以指定多个@Signature
  3. * @Signature 指定该拦截器拦截的是四大对象中的哪个方法
  4. * type:拦截器的四大对象的类型
  5. * method:拦截器的方法,方法名
  6. * args:入参的类型,可以是多个,根据方法的参数指定,以此来区分方法的重载
  7. */
  8. @Intercepts(
  9. {
  10. @Signature(type = ParameterHandler.class,method ="setParameters",args = {PreparedStatement.class})
  11. }
  12. )
  13. public class ParameterInterceptor implements Interceptor {
  14. @Override
  15. public Object intercept(Invocation invocation) throws Throwable {
  16. System.out.println("拦截器执行:"+invocation.getTarget());
  17. //目标对象
  18. Object target = invocation.getTarget();
  19. //获取目标对象中所有属性的值,因为ParameterHandler使用的是DefaultParameterHandler,因此里面的所有的属性都封装在其中
  20. MetaObject metaObject = SystemMetaObject.forObject(target);
  21. //使用xxx.xxx.xx的方式可以层层获取属性值,这里获取的是mappedStatement中的id值
  22. String value = (String) metaObject.getValue("mappedStatement.id");
  23. //如果是指定的查询方法
  24. if ("cn.cb.demo.dao.UserMapper.selectByUserId".equals(value)){
  25. //设置参数的值是admin_1,即是设置id=admin_1,因为这里只有一个参数,可以这么设置,如果有多个需要需要循环
  26. metaObject.setValue("parameterObject", "admin_1");
  27. }
  28. //执行目标方法
  29. return invocation.proceed();
  30. }
  31. @Override
  32. public Object plugin(Object target) {
  33. //如果没有特殊定制,直接使用Plugin这个工具类返回一个代理对象即可
  34. return Plugin.wrap(target, this);
  35. }
  36. @Override
  37. public void setProperties(Properties properties) {
  38. }
  39. }
  • intercept方法:最终会拦截的方法,最重要的一个方法。
  • plugin方法:返回一个代理对象,如果没有特殊要求,直接使用Mybatis的工具类Plugin返回即可。
  • setProperties:设置一些属性,不重要。

用到哪些注解?

  • 自定义插件需要用到两个注解,分别是@Intercepts@Signature
  • @Intercepts:标注在实现类上,表示这个类是一个插件的实现类。
  • @Signature:作为@Intercepts的属性,表示需要增强Mybatis的某些组件中的某些方法(可以指定多个)。常用的属性如下:

    • Class<?> type():指定哪个组件(ExecutorParameterHandlerResultSetHandlerStatementHandler
    • String method():指定增强组件中的哪个方法,直接写方法名称。
    • Class<?>[] args():方法中的参数,必须一一对应,可以写多个;这个属性非常重用,区分重载方法。

如何注入Mybatis?

  • 上面已经将插件定义好了,那么如何注入到Mybatis中使其生效呢?

  • 「前提」:由于本篇文章的环境是SpringBoot+Mybatis,因此讲一讲如何在SpringBoot中将插件注入到Mybatis中。

  • 在Mybatis的自动配置类MybatisAutoConfiguration中,注入SqlSessionFactory的时候,有如下一段代码:

  • 上图中的this.interceptors是什么,从何而来,其实就是从容器中的获取的Interceptor[],如下一段代码:

  • 从上图我们知道,这插件最终还是从IOC容器中获取的Interceptor[]这个Bean,因此我们只需要在配置类中注入这个Bean即可,如下代码:

  1. /**
  2. * @Configuration:这个注解标注该类是一个配置类
  3. */
  4. @Configuration
  5. public class MybatisConfig{
  6. /**
  7. * @Bean : 该注解用于向容器中注入一个Bean
  8. * 注入Interceptor[]这个Bean
  9. * @return
  10. */
  11. @Bean
  12. public Interceptor[] interceptors(){
  13. //创建ParameterInterceptor这个插件
  14. ParameterInterceptor parameterInterceptor = new ParameterInterceptor();
  15. //放入数组返回
  16. return new Interceptor[]{parameterInterceptor};
  17. }
  18. }

测试

  • 此时自定义的插件已经注入了Mybatis中了,现在测试看看能不能成功执行呢?测试代码如下:
  1. @Test
  2. void contextLoads() {
  3. //传入的是1222
  4. UserInfo userInfo = userMapper.selectByUserId("1222");
  5. System.out.println(userInfo);
  6. }
  • 测试代码传入的是1222,由于插件改变了入参,因此查询出来的应该是admin_1这个人。

插件原理分析

  • 插件的原理其实很简单,就是在创建组件的时候生成代理对象(Plugin),执行组件方法的时候拦截即可。下面就来详细介绍一下插件在Mybatis底层是如何工作的?
  • Mybatis的四大组件都是在Mybatis的配置类Configuration中创建的,具体的方法如下:

  1. //创建Executor
  2. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  3. executorType = executorType == null ? defaultExecutorType : executorType;
  4. executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  5. Executor executor;
  6. if (ExecutorType.BATCH == executorType) {
  7. executor = new BatchExecutor(this, transaction);
  8. } else if (ExecutorType.REUSE == executorType) {
  9. executor = new ReuseExecutor(this, transaction);
  10. } else {
  11. executor = new SimpleExecutor(this, transaction);
  12. }
  13. if (cacheEnabled) {
  14. executor = new CachingExecutor(executor);
  15. }
  16. //调用pluginAll方法,生成代理对象
  17. executor = (Executor) interceptorChain.pluginAll(executor);
  18. return executor;
  19. }
  20. //创建ParameterHandler
  21. public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  22. ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  23. //调用pluginAll方法,生成代理对象
  24. parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  25. return parameterHandler;
  26. }
  27. //创建ResultSetHandler
  28. public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
  29. ResultHandler resultHandler, BoundSql boundSql) {
  30. ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
  31. //调用pluginAll方法,生成代理对象
  32. resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  33. return resultSetHandler;
  34. }
  35. //创建StatementHandler
  36. public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  37. StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  38. //调用pluginAll方法,生成代理对象
  39. statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  40. return statementHandler;
  41. }
  • 从上面的源码可以知道,创建四大组件的方法中都会执行pluginAll()这个方法来生成一个代理对象。具体如何生成的,下面详解。

如何生成代理对象?

  • 创建四大组件过程中都执行了pluginAll()这个方法,此方法源码如下:
  1. public Object pluginAll(Object target) {
  2. //循环遍历插件
  3. for (Interceptor interceptor : interceptors) {
  4. //调用插件的plugin()方法
  5. target = interceptor.plugin(target);
  6. }
  7. //返回
  8. return target;
  9. }
  • pluginAll()方法很简单,直接循环调用插件的plugin()方法,但是我们调用的是Plugin.wrap(target, this)这行代码,因此要看一下wrap()这个方法的源码,如下:
  1. public static Object wrap(Object target, Interceptor interceptor) {
  2. //获取注解的@signature的定义
  3. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  4. //目标类
  5. Class<?> type = target.getClass();
  6. //获取需要拦截的接口
  7. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  8. if (interfaces.length > 0) {
  9. //生成代理对象
  10. return Proxy.newProxyInstance(
  11. type.getClassLoader(),
  12. interfaces,
  13. new Plugin(target, interceptor, signatureMap));
  14. }
  15. return target;
  16. }
  • Plugin.wrap()这个方法的逻辑很简单,判断这个插件是否是拦截对应的组件,如果拦截了,生成代理对象(Plugin)返回,没有拦截直接返回,上面例子中生成的代理对象如下图:

如何执行?

  • 上面讲了Mybatis启动的时候如何根据插件生成代理对象的(Plugin)。现在就来看看这个代理对象是如何执行的?
  • 既然是动态代理,肯定会执行的invoke()这个方法,Plugin类中的invoke()源码如下:
  1. @Override
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  3. try {
  4. //获取@signature标注的方法
  5. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  6. //如果这个方法被拦截了
  7. if (methods != null && methods.contains(method)) {
  8. //直接执行插件的intercept()这个方法
  9. return interceptor.intercept(new Invocation(target, method, args));
  10. }
  11. //没有被拦截,执行原方法
  12. return method.invoke(target, args);
  13. } catch (Exception e) {
  14. throw ExceptionUtil.unwrapThrowable(e);
  15. }
  16. }
  • 逻辑很简单,这个方法被拦截了就执行插件的intercept()方法,没有被拦截,则执行原方法。
  • 还是以上面自定义的插件来看看执行的流程:

    • setParameters()这个方法在PreparedStatementHandler中被调用,如下图:
    • 执行invoke()方法,发现setParameters()这个方法被拦截了,因此直接执行的是intercept()方法。

总结

  • Mybatis中插件的原理其实很简单,分为以下几步:

    1. 在项目启动的时候判断组件是否有被拦截,如果没有直接返回原对象。
    2. 如果有被拦截,返回动态代理的对象(Plugin)。
    3. 执行到的组件的中的方法时,如果不是代理对象,直接执行原方法
    4. 如果是代理对象,执行Plugininvoke()方法。

分页插件的原理分析

  • 此处安利一款经常用的分页插件pagehelper,Maven依赖如下:
  1. <dependency>
  2. <groupId>com.github.pagehelper</groupId>
  3. <artifactId>pagehelper</artifactId>
  4. <version>5.1.6</version>
  5. </dependency>
  • 分页插件很显然也是根据Mybatis的插件来定制的,来看看插件PageInterceptor的源码如下:
  1. @Intercepts(
  2. {
  3. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  4. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
  5. }
  6. )
  7. public class PageInterceptor implements Interceptor {}
  • 既然是分页功能,肯定是在query()的时候拦截,因此肯定是在Executor这个组件中。
  • 分页插件的原理其实很简单,不再一一分析源码了,根据的自己定义的分页数据重新赋值RowBounds来达到分页的目的,当然其中涉及到数据库方言等等内容,不是本章重点,有兴趣可以看一下GitHub上的文档

总结

  • 对于业务开发的程序员来说,插件的这个功能很少用到,但是不用就不应该了解吗?做人要有追求,哈哈。
  • 欢迎关注作者的微信公众号码猿技术专栏,作者为你们精心准备了springCloud最新精彩视频教程精选500本电子书架构师免费视频教程等等免费资源,让我们一起进阶,一起成长。

送命题:讲一讲Mybatis插件的原理及如何实现?的更多相关文章

  1. mybatis 插件的原理-责任链和动态代理的体现

    目录 1 拦截哪些方法 2 如何代理 3 代理对象 4 责任链设计模式 @ 如果没有自定义过拦截器, 可以看我前面的文章.如果不知道 JDK 动态代理怎么使用的, 可以看我这文章. 责任链设计模式理解 ...

  2. Mybatis九( mybatis插件的原理及使用)

    1.插件执行原理 一.demo 1.测试类 @Test public void test1() { String resource = "mybatis-config.xml"; ...

  3. mybatis插件机制原理

    mybatis插件机制及分页插件原理 参考链接:mybatis插件机制及分页插件原理 如何编写一个自定义mybatis插件 参考链接:mybatis 自定义插件的使用

  4. mybatis插件机制及分页插件原理

    MyBatis 插件原理与自定义插件: MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能.需要注意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式 ...

  5. 深入理解Mybatis插件

    Mybatis插件实现原理 本文如有任何纰漏.错误,请不吝指出,谢谢! 首先,我并没有使用过 Mybatis的插件,但是这个和我写这篇文章并不冲突,估计能真正使用到插件的人也比较少,写这篇文章的目的主 ...

  6. MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间

    Plugins 摘一段来自MyBatis官方文档的文字. MyBatis允许你在某一点拦截已映射语句执行的调用.默认情况下,MyBatis允许使用插件来拦截方法调用 Executor(update.q ...

  7. MyBatis 插件 : 打印 SQL 及其执行时间

    Plugins 摘一段来自MyBatis官方文档的文字. MyBatis允许你在某一点拦截已映射语句执行的调用.默认情况下,MyBatis允许使用插件来拦截方法调用: Executor(update. ...

  8. MyBatis插件及示例----打印每条SQL语句及其执行时间

    Plugins 摘一段来自MyBatis官方文档的文字. MyBatis允许你在某一点拦截已映射语句执行的调用.默认情况下,MyBatis允许使用插件来拦截方法调用 Executor(update.q ...

  9. 【原创】分布式之数据库和缓存双写一致性方案解析(三) 前端面试送命题(二)-callback,promise,generator,async-await JS的进阶技巧 前端面试送命题(一)-JS三座大山 Nodejs的运行原理-科普篇 优化设计提高sql类数据库的性能 简单理解token机制

    [原创]分布式之数据库和缓存双写一致性方案解析(三)   正文 博主本来觉得,<分布式之数据库和缓存双写一致性方案解析>,一文已经十分清晰.然而这一两天,有人在微信上私聊我,觉得应该要采用 ...

随机推荐

  1. HBA卡常用命令

    HBA卡信息查看 查看对应的PCI设备lspci | grep LSI 如下:对应的HBA卡命令为sas3ircu 如下:对应的HBA卡使用sas2ircu 查看LSI控制器类型和型号 sas2irc ...

  2. 【luogu4137】 Rmq Problem / mex - 莫队

    题目描述 有一个长度为n的数组{a1,a2,…,an}.m次询问,每次询问一个区间内最小没有出现过的自然数. 思路 莫队水过去了 233 #include <bits/stdc++.h> ...

  3. python3 - 常用的操作数据库

    # 获取手机号数据表的中的数据 sql2 = 'SELECT shoujihao FROM shoujihao' self.cursor.execute(sql2) sjh_dates = self. ...

  4. Lambda表达式的几种实现过程

    1.无参数+语句(代码块):适用于匿名内部类中方法无参数的情况 private void threadTest(){ //普通写法 new Thread(new Runnable(){ @Overri ...

  5. Invalid credentials for 'https://repo.magento.com/packages.json'

    Use your public key as username and private key as password from your magento connect account You ca ...

  6. Vscode配置C++环境

    (终于申请博客了qaq) 之前用了那么久Dev-C++,总算换了一个编辑器,Visual Studio Code (Vscode). 界面可比以前的舒适多了. Vscode作为一款功能极其丰富的开发工 ...

  7. AltiumDesigner画图不求人11 | 提高AD20启动速度的方法七选择手动释放工程 | 视频教程 | 你问我答

    往期文章目录 AD画图不求人1 | AD20软件安装视频教程 | 含软件安装包 AD画图不求人2 | 中英文版本切换 AD画图不求人3 | 高亮模式设置 AD画图不求人4 | 双击设计文件无法启动Al ...

  8. you-get 下载B站上的视频

    安装you-get pip install you-get 安装好后,我们可以查看一下you-get的参数 you-get -h 视频下载 单个视频下载 CMD下载 you-get -o /data/ ...

  9. docker run 创建容器

    docker run常用命令 docker run :创建一个新的容器并运行一个命令 - 语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 1.OPTI ...

  10. ReplayingDecoder 解码器:别以为我有多厉害,也只不过是使用了一下装饰器模式而已~

    原文地址 一.设计模式为啥老是用不好? 想要写出更屌的代码,提高代码的健壮性和可扩展性,那么设计模式可谓是必学的技能. 关于学习设计模式,大家可能都觉得设计模式的概念太过于抽象,理解起来有点费劲:又或 ...