MyBatis插件原理分析,看完感觉自己better了

开发 架构
大多数框架都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。在Mybatis中最出名的就是PageHelper 分页插件,下面我们先来使用一下这个分页插件。

本文主要内容:

 

大多数框架都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。

在Mybatis中最出名的就是PageHelper 分页插件,下面我们先来使用一下这个分页插件。

如何集成分页插件

Spring-Boot+Mybatis+PageHelper 。

引入pom依赖

  1. <dependency> 
  2.    <groupId>com.github.pagehelper</groupId> 
  3.    <artifactId>pagehelper-spring-boot-starter</artifactId> 
  4.    <version>1.2.3</version> 
  5. </dependency> 

配置分页插件配置项

  1. pagehelper: 
  2.   helperDialect: mysql 
  3.   reasonable: true 
  4.   supportMethodsArguments: true 
  5.   params: count=countSql 

service接口代码中

  1. PageInfo selectUsersByName(int pageIndex, int pageSize); 

service实现类代码中

  1. @Override 
  2. public PageInfo selectUsersByName(int pageIndex, int pageSize) { 
  3.     PageHelper.startPage(pageIndex, pageSize); 
  4.     List<User> users = userMapper.selectUsersByName(null); 
  5.     return new PageInfo(users); 

Mapper代码代码

  1. <select id="selectUsersByName" resultMap="User"
  2.     select * from m_user 
  3.     <where
  4.         <if test="userName != null and userName != ''"
  5.             `name` = #{userName} 
  6.         </if> 
  7.     </where
  8. </select
  1. List<User> selectUsersByName(@Param("userName") String userName); 

controller中代码

  1. @GetMapping("/user/name"
  2. public PageInfo selectUsersByName(int pageIndex, int pageSize) { 
  3.     return userService.selectUsersByName(pageIndex, pageSize); 

然后我们访问

http://localhost:9002/user/name?pageIndex=1&pageSize=10

输出结果:

 

输出重要项说明:

  • pageNum:当前页码。
  • pageSize:每页数。
  • list:就是我们返回的业务数据。
  • total:总数据。
  • hasNextPage:是否存在下一页。

我们在看看输出SQL:

 

发现其实执行了两条SQL:count和limit。

猜测分页插件实现

1.这个分页插件无非就是在我们的查询条件上拼接了个limit和做了一个count查询。

2.我们这里使用的是Mysql作为数据库,如果是Oracle的话那就不是limit了,所以这里有多重数据库对应的方案。

3.在没有此插件的前面拦截并做了sql和相关处理。

根据官网快速入门插件

下面是来自官网的一段话:

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

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

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

那我们就尝试着按照官方来写一个插件。

自定义插件

  1. @Intercepts({@Signature( 
  2.         type= Executor.class, 
  3.         method = "update"
  4.         args = {MappedStatement.class,Object.class})}) 
  5. public class TianPlugin implements Interceptor { 
  6.     private Properties properties = new Properties(); 
  7.  
  8.     @Override 
  9.     public Object intercept(Invocation invocation) throws Throwable { 
  10.         System.out.println("老田写的一个Mybatis插件--start"); 
  11.         Object returnObject = invocation.proceed(); 
  12.         System.out.println("老田写的一个Mybatis插件---end"); 
  13.         return returnObject; 
  14.     } 

然后把插件类注入到容器中。

 

这里的自定义完全是官网给出的案例。从自定义的插件类中看到有个update,我们猜测肯定是需要执行update才会被拦截到。

访问前面的代码:http://localhost:9002/updateUser

 

成功了。

这是大家肯定会联想到我们刚刚开始学动态代理的时候,不就是在要调用的方法的前面和后面做点小东东吗?

Mybatis的插件确实就是这样的。

我们来分析一下官方的那段话和我们自定义的插件。

分析

首先,我们自定义的插件必须是针对下面这四个类以及方法。

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

其次,我们必须实现Mybatis的Interceptor。

 

Interceptor中三个方法的作用:

  • intercept():执行拦截内容的地方,比如:在调用某类方法前后做一些自己的处理,简单就是打印日志。
  • plugin():决定是否触发intercept()方法。
  • setProperties():给自定义的拦截器传递我们配置的属性参数(这个可以暂时不管他,后面我们写一个相对完整点的插件,你就明白是干啥的了)。

plugin方法

  1. default Object plugin(Object target) { 
  2.     return Plugin.wrap(target, this); 
  3.   } 

默认实现方法,里面调用了Plugin.wrap()方法。

  1. public class Plugin implements InvocationHandler { 
  2.  
  3.   private Object target; 
  4.   private Interceptor interceptor; 
  5.   private Map<Class<?>, Set<Method>> signatureMap; 
  6.  
  7.   private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { 
  8.     this.target = target; 
  9.     this.interceptor = interceptor; 
  10.     this.signatureMap = signatureMap; 
  11.   } 
  12.  
  13.   public static Object wrap(Object target, Interceptor interceptor) { 
  14.     Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); 
  15.     Class<?> type = target.getClass(); 
  16.     Class<?>[] interfaces = getAllInterfaces(type, signatureMap); 
  17.     if (interfaces.length > 0) { 
  18.       // 创建JDK动态代理对象 
  19.       return Proxy.newProxyInstance( 
  20.           type.getClassLoader(), 
  21.           interfaces, 
  22.           new Plugin(target, interceptor, signatureMap)); 
  23.     } 
  24.     return target; 
  25.   } 
  26.  
  27.   @Override 
  28.   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  29.     try { 
  30.       Set<Method> methods = signatureMap.get(method.getDeclaringClass()); 
  31.       // 判断是否是需要拦截的方法(很重要) 
  32.       if (methods != null && methods.contains(method)) { 
  33.         // 回调intercept()方法 
  34.         return interceptor.intercept(new Invocation(target, method, args)); 
  35.       } 
  36.       return method.invoke(target, args); 
  37.     } catch (Exception e) { 
  38.       throw ExceptionUtil.unwrapThrowable(e); 
  39.     } 
  40.   } 
  41. //...省略其他不相关代码 

这不就是一个JDK动态代理吗?

Map

所以,我们不要动不动就说反射性能很差,那是因为你没有像Mybatis一样去缓存一个对象的反射结果。

判断是否是需要拦截的方法,这句注释很重要,一旦忽略了,都不知道Mybatis是怎么判断是否执行拦截内容的,要记住。

Plugin.wrap(target, this)是干什么的?

使用JDK的动态代理,给target对象创建一个delegate代理对象,以此来实现方法拦截和增强功能,它会回调intercept()方法。

为什么要写注解?注解都是什么含义?

在我们自定义的插件上有一堆注解,别害怕。

Mybatis规定插件必须编写Annotation注解,是必须,而不是可选。

  1. @Intercepts({@Signature( type= Executor.class, method = "update"
  2.                         args = {MappedStatement.class,Object.class})} 
  3.            ) 
  4. public class TianPlugin implements Interceptor { 

@Intercepts注解:装载一个@Signature列表,一个@Signature其实就是一个需要拦截的方法封装。那么,一个拦截器要拦截多个方法,自然就是一个@Signature列表。

  1. type= Executor.class, method = "update",args = {MappedStatement.class,Object.class} 

解释:要拦截Executor接口内的query()方法,参数类型为args列表。

 

那如果想拦截多个方法呢?

  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target(ElementType.TYPE) 
  4. public @interface Intercepts { 
  5.   Signature[] value(); 

这就简单了吧,我们在@Intercepts注解中可以存放多个@Signature注解。

比如说前面分页插件中就是拦截多个方法的。

 

为什么拦截两个都是query方法呢?因为在Executor中有两个query方法。

 

总结下:

Mybatis规定必须使用@Intercepts注解。

@Intercepts注解内可以添加多个类多个方法,注意方法名和参数类型个数一定要对应起来。

本文转载自微信公众号「 Java后端技术全栈」,可以通过以下二维码关注。转载本文请联系 Java后端技术全栈公众号。

 

责任编辑:武晓燕 来源: Java后端技术全栈
相关推荐

2022-12-26 08:19:06

Mybatis核心类工厂类

2020-12-18 08:03:00

插件MyBatis Executor

2021-08-09 11:15:28

MybatisJavaSpring

2024-02-26 00:00:00

Docker容器

2010-03-25 16:08:19

2019-11-25 16:05:20

MybatisPageHelpeJava

2021-01-18 06:53:31

QQ牛角图标聊天软件

2017-08-09 15:07:08

大数据数据分析户画像

2023-04-28 08:30:56

MyBatis架构API

2020-12-15 08:03:57

Mybatis配置文件

2021-12-09 18:32:08

Chrome插件浏览器

2021-06-30 15:05:15

VS Code程序员编程

2011-08-25 09:30:22

2019-12-19 08:56:21

MybatisSQL执行器

2023-07-29 22:02:06

MyBatis数据库配置

2018-10-08 09:44:51

无线AP故障

2020-07-01 07:29:47

SpringbootRabbitmq服务器

2019-01-23 13:04:09

QLCNAND闪存

2020-03-03 20:04:30

SSD硬盘闪存

2021-11-05 11:10:13

MyBatisSQL查询
点赞
收藏

51CTO技术栈公众号