zoukankan      html  css  js  c++  java
  • MyBatis插件原理及应用(上篇)

    本文主要内容:

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

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

    如何集成分页插件

    Spring-Boot+Mybatis+PageHelper 。

    引入pom依赖

    <dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
       <version>1.2.3</version>
    </dependency>
    

    配置分页插件配置项

    pagehelper:
      helperDialect: mysql
      reasonable: true
      supportMethodsArguments: true
      params: count=countSql
    

    service接口代码中

    PageInfo selectUsersByName(int pageIndex, int pageSize);
    

    service实现类代码中

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

    Mapper代码代码

    <select id="selectUsersByName" resultMap="User">
        select * from m_user
        <where>
            <if test="userName != null and userName != ''">
                `name` = #{userName}
            </if>
        </where>
    </select>
    
    List<User> selectUsersByName(@Param("userName") String userName);
    

    controller中代码

    @GetMapping("/user/name")
    public PageInfo selectUsersByName(int pageIndex, int pageSize) {
        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 接口,并指定想要拦截的方法签名即可。

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

    自定义插件

    @Intercepts({@Signature(
            type= Executor.class,
            method = "update",
            args = {MappedStatement.class,Object.class})})
    public class TianPlugin implements Interceptor {
        private Properties properties = new Properties();
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("老田写的一个Mybatis插件--start");
            Object returnObject = invocation.proceed();
            System.out.println("老田写的一个Mybatis插件---end");
            return returnObject;
        }
    }
    

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

    这里的自定义完全是官网给出的案例。从自定义的插件类中看到有个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方法
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    

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

    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) {
          // 创建JDK动态代理对象
          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)) {
            // 回调intercept()方法
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    //...省略其他不相关代码
    }
    

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

    Map<Class<?>, Set> signatureMap:缓存需拦截对象的反射结果,避免多次反射,即target的反射结果。

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

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

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

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

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

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

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

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

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

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

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

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

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Intercepts {
      Signature[] value();
    }
    

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

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

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

    总结下:

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

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

  • 相关阅读:
    [转载]各种计算机语言的经典书籍
    [转载]VC 常用快捷键
    [转载]Visual Studio中的debug和release版本的区别
    [转载]Visual C++开发工具与调试技巧整理
    [转载]一个游戏程序员的学习资料
    [转载]C++资源之不完全导引(完整版)
    [转载]一个图形爱好者的书架/白话说学计算机图形学
    [摘录]这几本游戏编程书籍你看过吗?
    Oracle分析函数的使用
    [C/C++]C++下基本类型所占位数和取值范围
  • 原文地址:https://www.cnblogs.com/tianweichang/p/14149064.html
Copyright © 2011-2022 走看看