Mybatis自定义插件

即使你没开发过 MyBatis 插件,估计也能猜出来,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
  }

}

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

intercept:这个就是具体的拦截方法,我们自定义 MyBatis 插件时,一般都需要重写该方法,我们插件所完成的工作也都是在该方法中完成的。
plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般来说我们不需要重写该方法。
Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过动态代理拦截目标对象。

setProperties:这个方法用来传递插件的参数,可以通过参数来改变插件的行为。我们定义好插件之后,需要对插件进行配置,在配置的时候,可以给插件设置相关属性,设置的属性可以通过该方法获取到。插件属性设置像下面这样:

<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>

.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 {
    //...
}

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

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);

}

各方法含义分别如下:

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 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
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 时会被触发。
在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。

案例1:

当我们查询数据时。返回的这种结果可能不是我们想要的,这时,我们可以对其进行拦截,然后自定义结果。
在这里插入图片描述

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        List<Object> list = (List<Object>) invocation.proceed(); // proceed 查询到的结果
        list.forEach(item -> { // 遍历list
            if (item instanceof Map) { // 如果是个map
                Map<String, Object> map = (Map<String, Object>) item;
                HashSet<String> keys = new HashSet<>(map.keySet());
                for (String key : keys) {
                    Object value = map.get(key);
                    map.remove(key);
                    map.put(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,key),value ); // 使用谷歌的工具类把下划线命名转为驼峰命名
                }
            }
        });
        return list;
    }
}

在Mybatis配置文件中使用我们刚刚配置的插件:
在这里插入图片描述
最后的结果:
在这里插入图片描述

案例2: 自定义分页插件


@Intercepts(@Signature(
        type = Executor.class, /*拦截executor 执行器*/
        method = "query", /*拦截query方法 当执行查询操作时,就会被拦截下来*/
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} /*query 有一个重载方法 需要通过参数锁定*/
))
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /*Invocation(Object target, Method method, Object[] args)
        * args[0] --->  MappedStatement
        * args[1] ---> 拦截到方法的参数
        * args[2]  ---> Rowbounds 默认会有一个Rowbounds ,传的话就用传的那个Rowbound 不传的话就用默认的
        * args[3] ---> ResultHandler
        * */
        Object[] args = invocation.getArgs(); // 获取参数
        MappedStatement ms = (MappedStatement) args[0]; // 获取到方法对应的mapper文件里面写的那些 执行语句
        Object parameterObject = args[1]; // 获取到拦截的方法的参数
        System.out.println("parameterObject = " + parameterObject);
        RowBounds rowBounds = (RowBounds) args[2]; // mybatis所有的查询都有一个默认的RowBounds
        // 如果不等于 就是 需要分页
        if (rowBounds != RowBounds.DEFAULT) {
            Executor executor = (Executor) invocation.getTarget(); // 拿到执行器
            BoundSql boundSql = ms.getBoundSql(parameterObject); // 拿到拦截下来的那条sql 以及那条sql的参数
            System.out.println("boundSql.toString() = " + boundSql.toString());
            /*
            BoundSql 对象里有一个参数 : additionalParameters  这个参数里面放的是 sql的一些额外参数
            BoundSql 没有直接提供这个属性 ,所以需要通过反射获取
            * */
            Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
            Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
            Set<String> strings = additionalParameters.keySet();
            for (String string : strings) {
                System.out.println(string + "---" + additionalParameters.get(string));
            }
            // 创建出缓存的key 当前配置的key
            CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);

            // 防止二级缓存失效
            // cacheKey.update();

            // 重新组装sql 在原来的sql上加上分页
            String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();

            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);

            // 需要把原来的额外参数 放到新的pageBoundSql 里面
            Set<String> keySet = additionalParameters.keySet();
            keySet.forEach(key ->{
                pageBoundSql.setAdditionalParameter(key,additionalParameters.get(key));
            });
            // 执行查询
            List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], cacheKey, pageBoundSql);
            return list;
        }
        return invocation.proceed();
    }
}

    <!--配置拦截器-->
    <plugins>
        <!----><plugin interceptor="com.java.lang.plugin.CamelInterceptor"></plugin>
        <plugin interceptor="com.java.lang.plugin.PageInterceptor"></plugin>
    </plugins>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值