zoukankan      html  css  js  c++  java
  • MyBatis 插件讲解

    关于Mybatis插件,大部分人都知道,也都使用过,但很多时候,我们仅仅是停留在表面上,知道Mybatis插件可以在DAO层进行拦截,如打印执行的SQL语句日志,做一些权限控制,分页等功能;

    但对其内部实现机制,涉及的软件设计模式,编程思想往往没有深入的理解。

    本篇案例将帮助读者对Mybatis插件的使用场景,实现机制,以及其中涉及的编程思想进行一个小结,希望对以后的编程开发工作有所帮助。

    Mybatis插件适用场景:

    • 分页功能

        mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;

    • 公共字段统一赋值

        一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;

    • 性能监控

        对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;

    • 其它

        其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。

    Mybatis插件实际上就是一个拦截器,应用代理模式,在方法级别上进行拦截。

    1.MyBatis 插件接口

    MyBatis 框架在设计的时候,就已经为插件的开发预留了相关接口,如下:

    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
    
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }

    这个接口中就三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:

    1. intercept:这个就是具体的拦截方法,我们自定义 MyBatis 插件时,一般都需要重写该方法,我们插件所完成的工作也都是在该方法中完成的。
    2. plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般来说我们不需要重写该方法。Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过动态代理拦截目标对象。
    3. setProperties:这个方法用来传递插件的参数,可以通过参数来改变插件的行为。我们定义好插件之后,需要对插件进行配置,在配置的时候,可以给插件设置相关属性,设置的属性可以通过该方法获取到。插件属性设置像下面这样:
    <plugins>
        <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
            <property name="xxx" value="xxx"/>
        </plugin>
    </plugins>

    2.MyBatis 拦截器签名

    拦截器定义好了后,拦截谁?

    这个就需要拦截器签名来完成了!

    拦截器签名是一个名为 @Intercepts 的注解该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:

    • type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
    • method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
    • args: 拦截器所拦截方法的参数类型通过方法名和参数类型可以锁定唯一一个方法。

    一个简单的签名可能像下面这样:

    @Intercepts(@Signature(
            type = ResultSetHandler.class,
            method = "handleResultSets",
            args = {Statement.class}
    ))
    public class CamelInterceptor implements Interceptor {
        //...
    }

    3.被拦截的对象

    根据前面的介绍,被拦截的对象主要有如下四个:

    Executor

    public interface Executor {
    
      ResultHandler NO_RESULT_HANDLER = null;
    
      int update(MappedStatement ms, Object parameter) throws SQLException;
    
      <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    
      <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    
      <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
    
      List<BatchResult> flushStatements() throws SQLException;
    
      void commit(boolean required) throws SQLException;
    
      void rollback(boolean required) throws SQLException;
    
      CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
    
      boolean isCached(MappedStatement ms, CacheKey key);
    
      void clearLocalCache();
    
      void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
    
      Transaction getTransaction();
    
      void close(boolean forceRollback);
    
      boolean isClosed();
    
      void setExecutorWrapper(Executor executor);
    
    }
    View Code

    各方法含义分别如下:

    • update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
    • query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
    • queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
    • flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
    • commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
    • rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
    • getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
    • close:该方法在懒加载获取新的 Executor 后会被触发。
    • isClosed:该方法在懒加载执行查询前会被触发。

    ParameterHandler

    public interface ParameterHandler {
    
      Object getParameterObject();
    
      void setParameters(PreparedStatement ps) throws SQLException;
    
    }

    各方法含义分别如下:

    • getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
    • setParameters:设置 SQL 参数时该方法会被触发。

    ResultSetHandler

    public interface ResultSetHandler {
    
      <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    
      <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    
      void handleOutputParameters(CallableStatement cs) throws SQLException;
    
    }

    各方法含义分别如下:

    • handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor<E> 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
    • handleCursorResultSets:当查询方法的返回值类型为 Cursor<E> 时,该方法会被触发。
    • handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。

    StatementHandler

    public interface StatementHandler {
    
      Statement prepare(Connection connection, Integer transactionTimeout)
          throws SQLException;
    
      void parameterize(Statement statement)
          throws SQLException;
    
      void batch(Statement statement)
          throws SQLException;
    
      int update(Statement statement)
          throws SQLException;
    
      <E> List<E> query(Statement statement, ResultHandler resultHandler)
          throws SQLException;
    
      <E> Cursor<E> queryCursor(Statement statement)
          throws SQLException;
    
      BoundSql getBoundSql();
    
      ParameterHandler getParameterHandler();
    
    }

    各方法含义分别如下:

    • prepare:该方法在数据库执行前被触发。
    • parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
    • batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该方法会被调用。
    • update:更新操作时该方法会被触发。
    • query:该方法在 SELECT 方法执行时会被触发。
    • queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。

    在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。

    4.开发分页插件

    4.1 内存分页

    MyBatis 中提供了一个不太好用的内存分页功能,就是一次性把所有数据都查询出来,然后在内存中进行分页处理,这种分页方式效率很低,基本上没啥用,但是如果我们想要自定义分页插件,就需要对这种分页方式有一个简单了解。

    内存分页的使用方式如下,首先在 Mapper 中添加 RowBounds 参数,如下:

    public interface UserMapper {
        List<User> getAllUsersByPage(RowBounds rowBounds);
    }

    然后在 XML 文件中定义相关 SQL:

    <select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
        select * from user
    </select>

    可以看到,在 SQL 定义时,压根不用管分页的事情,MyBatis 会查询到所有的数据,然后在内存中进行分页处理。

    Mapper 中方法的调用方式如下:

    @Test
    public void test3() {
        UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
        RowBounds rowBounds = new RowBounds(1,2);
        List<User> list = userMapper.getAllUsersByPage(rowBounds);
        for (User user : list) {
            System.out.println("user = " + user);
        }
    }

    构建 RowBounds 时传入两个参数,分别是 offset 和 limit,对应分页 SQL 中的两个参数。也可以通过 RowBounds.DEFAULT 的方式构建一个 RowBounds 实例,这种方式构建出来的 RowBounds 实例,offset 为 0,limit 则为 Integer.MAX_VALUE,也就相当于不分页。

    这就是 MyBatis 中提供的一个很不实用的内存分页功能。

    了解了 MyBatis 自带的内存分页之后,接下来我们就可以来看看如何自定义分页插件了。

    4.2 自定义分页插件

    参考:

    https://segmentfault.com/a/1190000039305062?utm_source=tag-newest

    https://www.cnblogs.com/chenpi/p/10498921.html

    https://www.jianshu.com/p/cdff18f8cb97

    https://segmentfault.com/a/1190000021220956

  • 相关阅读:
    使用Index()+Match()函数实现更为灵活的VLookUp()
    Hexo搭建博客笔记
    Jenkins自动化部署项目
    Ubuntu安装docker
    Ubuntu的简单使用
    ansible之Ad-Hoc
    redis的集群
    redis的主从复制和哨兵
    redis的持久化存储
    redis数据库基础
  • 原文地址:https://www.cnblogs.com/Vincent-yuan/p/15231175.html
Copyright © 2011-2022 走看看