1. 拦截器注解
  1. mybatis自定义拦截器实现步骤:
  1. 实现org.apache.ibatis.plugin.Interceptor接口。
  2. 添加拦截器注解org.apache.ibatis.plugin.Intercepts。
  3. 配置文件中添加拦截器。
  1. 在mybatis中可被拦截的类型有四种(按照拦截顺序):
  1. Executor: 拦截执行器的方法。
  2. ParameterHandler: 拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler: 拦截Sql语法构建的处理。
  1. 拦截器注解的作用:
    自定义拦截器必须使用mybatis提供的注解来声明我们要拦截的类型对象。Mybatis插件都要有Intercepts 注解来指定要拦截哪个对象哪个方法。我们知道,Plugin.wrap()方法会返回四大接口对象的代理对象,会拦截所有的方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。
  2. 拦截器注解的规则:
@Intercepts({
    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
1. @Intercepts:标识该类是一个拦截器;
      @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
    2.1 type:对应四种类型中的一种;
    2.2 method:对应接口中的哪类方法(因为可能存在重载方法);
    2.3 args:对应哪一个方法;
  1. 拦截器可拦截的方法:
    |拦截的类| 拦截的方法 |
    |Executor|update, query, flushStatements, commit, rollback,getTransaction, close, isClosed|
    |ParameterHandler | getParameterObject, setParameters |
    |StatementHandler|prepare, parameterize, batch, update, query|
    |ResultSetHandler|handleResultSets, handleOutputParameters|
  2. 拦截器方法
    6.1 官方插件开发方式
@Intercepts({@Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
   public Object intercept(Invocation invocation) throws Throwable {
     Object target = invocation.getTarget(); //被代理对象
     Method method = invocation.getMethod(); //代理方法
     Object[] args = invocation.getArgs(); //方法参数
     // do something ...... 方法拦截前执行代码块
     Object result = invocation.proceed();
     // do something .......方法拦截后执行代码块
     return result;
   }
   public Object plugin(Object target) {
     return Plugin.wrap(target, this);
   }
}

拦截 Executor 示例:

@Intercepts(@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class}))
@Component
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        if (!SqlCommandType.INSERT.equals(sqlCommandType)&&!SqlCommandType.UPDATE.equals(sqlCommandType)){
            return invocation.proceed();
        }
        Object parameters = args[1];
        if (parameters instanceof Map){
            Map<String,Object> paramMap = (Map<String, Object>) parameters;
            if (ObjectUtils.isEmpty(paramMap.get("createTime"))){
                paramMap.put("createTime",new Date());
            }
            if (ObjectUtils.isEmpty(paramMap.get("updateTime"))){
                paramMap.put("updateTime",new Date());
            }

        }else{
            Class<?> aClass = parameters.getClass();
            Field createTimeField = aClass.getDeclaredField("createTime");
            createTimeField.setAccessible(true);
            Object createObj = createTimeField.get(parameters);
            if(ObjectUtils.isEmpty(createObj)){
                createTimeField.set(parameters,new Date());
            }

            Field updateTimeField = aClass.getDeclaredField("updateTime");
            updateTimeField.setAccessible(true);
            Object updateObj = updateTimeField.get(parameters);
            if(ObjectUtils.isEmpty(updateObj)){
                updateTimeField.set(parameters,new Date());
            }
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

拦截StatementHandler 示例:

@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}))
@Slf4j
@Component
public class InterceptorConfig implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        BoundSql boundSql = statementHandler.getBoundSql();
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        log.info("方法执行前");
        if (SqlCommandType.SELECT.equals(sqlCommandType)){
            String sql = boundSql.getSql();
            log.info("sql before:"+ sql);
            sql = sql+" where is_delete = 0";
            log.info("sql after:"+ sql);
            Field sqlField = boundSql.getClass().getDeclaredField("sql");
            sqlField.setAccessible(true);
            sqlField.set(boundSql,sql);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

6.2 拦截器的方法

public interface Interceptor {   
   Object intercept(Invocation invocation) throws Throwable;       
   Object plugin(Object target);    
   void setProperties(Properties properties);
}

setProperties方法
如果我们的拦截器需要一些变量对象,而且这个对象是支持可配置的。
类似于Spring中的@Value("${}")从application.properties文件中获取。
使用方法:

<plugin interceptor="com.plugin.mybatis.MyInterceptor">
     <property name="username" value="xxx"/>
     <property name="password" value="xxx"/>
</plugin>

方法中获取参数:properties.getProperty(“username”);

问题:但是为什么不直接使用@Value("${}") 获取变量?

解答:因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。

plugin方法
这个方法的作用是就是让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理。

@Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

需要注意的是:每经过一个拦截器对象都会调用插件的plugin方法,也就是说,该方法会调用4次。根据@Intercepts注解来决定是否进行拦截处理。

问题1:Plugin.wrap(target, this)方法的作用?

解答:判断是否拦截这个类型对象(根据@Intercepts注解决定),然后决定是返回一个代理对象还是返回原对象。

故我们在实现plugin方法时,要判断一下目标类型,是本插件要拦截的对象时才执行Plugin.wrap方法,否则的话,直接返回目标本身。

intercept(Invocation invocation)方法
我们知道,mybatis只能拦截四种类型的对象。而intercept方法便是处理拦截到的对象。比如我们要拦截StatementHandler#query(Statement st,ResultHandler rh)方法,那么Invocation就是这个对象,Invocation中有三个参数。

target:StatementHandler;
method :query;
args[]:Statement st,ResultHandler rh
org.apache.ibatis.reflection.SystemMetaObject#forObject:方便的获取对象中的值。

案例:将参数拼接到sql语句。

@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
@Slf4j
public class Aa implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Statement statement;
        //获取方法参数
        Object firstArg = invocation.getArgs()[0];
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
        } else {
            statement = (Statement) firstArg;
        }
        MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
        //获取Statement对象(sql语法已经构建完毕)
        statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        //获取sql语句
        String originalSql = statement.toString();

        //将原始sql中的空白字符(\s包括换行符,制表符,空格符)替换为" "
        originalSql = originalSql.replaceAll("[\\s]+", " ");

        //只获取sql的select/update/insert/delete开头的sql
        int index = indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }

        // 计算执行 SQL 耗时
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long timing = System.currentTimeMillis()- start;

        //获取MapperStatement对象,获取到sql的详细信息
        Object realTarget = realTarget(invocation.getTarget());
        //获取metaObject对象
        MetaObject metaObject = SystemMetaObject.forObject(realTarget);
        //获取MappedStatement对象
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        StringBuilder formatSql = new StringBuilder()
                .append(" Time:").append(timing)
                //获取Mapper信息和方法信息
                .append(" ms - ID:").append(ms.getId())
                .append("Execute SQL:")
                .append(originalSql);
        //打印sql信息
        log.info(formatSql.toString());
        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }



    @Override
    public void setProperties(Properties prop) {

    }

    /**
     * 获取sql语句开头部分
     *
     * @param sql
     * @return
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }

    /**
     * <p>
     * 获得真正的处理对象,可能多层代理.
     * </p>
     */
    @SuppressWarnings("unchecked")
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }
}