mybatis(3)—自定义拦截器(上)基础使用

mybatis自定义拦截器(一)基本使用
mybatis自定义拦截器(二)对象详解

1. 拦截器注解

1. mybatis自定义拦截器实现步骤:

  1. 实现org.apache.ibatis.plugin.Interceptor接口。
  2. 添加拦截器注解org.apache.ibatis.plugin.Intercepts
  3. 配置文件中添加拦截器。

2. 在mybatis中可被拦截的类型有四种(按照拦截顺序):

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理。

1. 不同拦截类型执行顺序:

com.galax.configuration.Aa#plugin打印拦截器对象顺序.png

2. 多个插件拦截的顺序?

image.png

需要注意的是,因为拦截器Aa和拦截器Bb均是拦截的StatementHandler对象,所以拦截器B在此获取StatementHandler的时候,获取的是代理对象。

拦截器对象的处理过程.png

3. 多个插件plugin()和intercept()方法的执行顺序

先执行每个插件的plugin方法,若是@Intercepts注解标明需要拦截该对象,那么生成类型对象的代理对象。(即使该插件需要拦截该类型对象,但是依旧会执行下一个插件的plugin方法)。知道执行完毕所有的plugin方法。在执行每个Intercept方法。

3. 拦截器注解的作用:

自定义拦截器必须使用mybatis提供的注解来声明我们要拦截的类型对象。

Mybatis插件都要有Intercepts [in特赛婆斯]注解来指定要拦截哪个对象哪个方法。我们知道,Plugin.wrap方法会返回四大接口对象的代理对象,会拦截所有的方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。

4. 拦截器注解的规则:

具体规则如下:

@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:标识该类是一个拦截器;
  2. @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
    2.1 type:对应四种类型中的一种;
    2.2 method:对应接口中的哪类方法(因为可能存在重载方法);
    2.3 args:对应哪一个方法;

5. 拦截器可拦截的方法:

拦截的类 拦截的方法
Executor update, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandler getParameterObject, setParameters
StatementHandler prepare, parameterize, batch, update, query
ResultSetHandler handleResultSets, handleOutputParameters

2. 拦截器方法

2.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);
   }
}

2.2 拦截器的方法

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

2.2.1 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这种做了很多的依赖注入。

2.2.2 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方法,否则的话,直接返回目标本身。

问题2:拦截器代理对象可能经过多层代理,如何获取到真实的拦截器对象?

    /**
     * <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;
    }

2.2.3 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语句。

因为已经执行了ParameterHandler拦截器,故Statement对象已经是完全拼接好的SQL语句。

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

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

    private Properties properties;

    private static final Logger logger = LoggerFactory.getLogger(MybatisLogInterceptor.class);

    public Object intercept(Invocation invocation) throws Throwable {

        long start = 0L;
        String sqlId = "";
        BoundSql boundSql = null;
        Configuration configuration = null;
        Object returnValue = null;

        try {
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            sqlId = mappedStatement.getId();
            if(sqlId.contains("History") || sqlId.contains("Tmp")){
                return invocation.proceed();
            }
            Object parameter = null;
            if (invocation.getArgs().length > 1) {
                parameter = invocation.getArgs()[1];
            }           
            boundSql = mappedStatement.getBoundSql(parameter);
            configuration = mappedStatement.getConfiguration();
            start = System.currentTimeMillis();
        } catch (Exception e) {
            logger.debug("Mybatis拦截器前置处理异常 原因:", e);
            logger.error("Mybatis拦截器前置处理异常 原因:" + e);
        }

        returnValue = invocation.proceed();

        try {
            long end = System.currentTimeMillis();
            long time = (end - start);
            String sql = getSql(configuration, boundSql, sqlId, time);
            // if (time >= Config.SQL_WARN_TIME) {
            // logger.warn(sql);
            // } else {
            // logger.info(sql);
            // }
        } catch (Exception e) {
            logger.debug("Mybatis拦截器后置处理异常 原因:", e);
            logger.error("Mybatis拦截器后置处理异常 原因:" + e);
        }

        return returnValue;
    }

    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time) {
        String sql = showSql(configuration, boundSql);
        StringBuilder str = new StringBuilder(100);
        str.append("【sqlId】").append(sqlId);
        str.append("【SQL耗时-").append(time).append("-毫秒】");
        str.append("【SQL】").append(sql);
        //logger.debug(SQLFormatter.format(str.toString()));
        logger.debug(str.toString());
        return str.toString();
    }

    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
            value = value.replaceAll("\\\\", "\\\\\\\\");
            value = value.replaceAll("\\$", "\\\\\\$");
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties0) {
        this.properties = properties0;
    }

}

2. MappedStatement.class

一个MappedStatement对象对应Mapper配置文件中的一个select/update/insert/delete节点,主要描述的是一条sql语句。其属性为:

  //节点中的id属性加要命名空间
  private String id;
  //直接从节点属性中取
  private Integer fetchSize;
  //直接从节点属性中取
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  //对应一条SQL语句
  private SqlSource sqlSource;
 
  //每条语句都对就一个缓存,如果有的话。
  private Cache cache;
  //这个已经过时了
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  //SQL的类型,select/update/insert/detete
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  
  //是否有内映射
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

文章参考

https://blog.csdn.net/Liu_York/article/details/88053053

https://www.jianshu.com/p/7c7b8c2c985d

MyBatis 插件之拦截器(Interceptor)

Mybatis Plugin(拦截器)的开发

Mybatis拦截器介绍

推荐阅读更多精彩内容