MyBatis拦截器实现SQL打印

kyaa111 3年前 ⋅ 1107 阅读

mybatis有自带的sql打印, 但只会出现在抛异常的时候, 或者配置日志输出, 但是输出的日志较为冗长

像这样

### Error querying database.  Cause: java.lang.ArithmeticException: / by zero
### The error may exist in file [/usr/local/userMapper.xml]
### The error may involve com.xxx.xxx.UserMapper.query_COUNT
### The error occurred while handling results
### SQL: SELECT count(0) FROM (SELECT id, username FROM user) table_count
### Cause: java.lang.ArithmeticException: / by zero
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.6.jar:3.5.6]

翻了下源码, 找到了这个方法, 贴一下

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

其中ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);

顾名思义, 对异常进行包装

点进方法

 public static RuntimeException wrapException(String message, Exception e) {
    return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}

关键在于ErrorContext.instance()toString()

该方法的部分代码

    // sql
    if (sql != null) {
      description.append(LINE_SEPARATOR);
      description.append("### SQL: ");
      description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
    }

且通过ErrorContext.instance()这个方法也能知道, 该类维护了一个ThreadLocal, 保证sql是线程安全的

于是就有了如下拦截器代码

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        }
)
@Log4j2
public class MybatisSqlLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object proceed = invocation.proceed();
        try {
            Field field = ErrorContext.class.getDeclaredField("sql");
            field.setAccessible(true);
            Object sql = field.get(ErrorContext.instance());
            if (sql != null) {
                System.out.println("mybatis sql: " + sql.toString());
            }
        } catch (Exception e) {
            log.error("sql打印失败", e);
        }
        return proceed;
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}

执行查询, sql打印, 完成

如果想将参数也填充进去

可以这么写

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        }
)
@Log4j2
public class MybatisSqlLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object proceed = invocation.proceed();
        // 获取xml中的一个select/update/insert/delete节点,是一条SQL语句
        MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
        Object parameter = null;
        // 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        // BoundSql就是封装myBatis最终产生的sql类
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        // 获取节点的配置
        Configuration configuration = mappedStatement.getConfiguration();
        // 获取到最终的sql语句
        String sql = showSql(configuration, boundSql);
        System.out.println("mybatis sql: " + sql);
        return proceed;
    }

    /**
     * 如果参数是String,则添加单引号,
     * 如果是日期,则转换为时间格式器并加单引号;
     * 对参数是null和不是null的情况作了处理
     */
    private static String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
                    DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

    /**
     * 进行?的替换
     *
     * @param configuration
     * @param boundSql
     * @return
     */
    public String showSql(Configuration configuration, BoundSql boundSql) throws NoSuchFieldException, IllegalAccessException {
        // 获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // sql语句中多个空格都用一个空格代替
        String sql = getRealSql().replaceAll("[\\s]+", " ");
        if (parameterObject != null) {
            // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?",
                        Matcher.quoteReplacement(getParameterValue(parameterObject)));

            } else {
                // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
                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("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        // 该分支是动态sql
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 打印出缺失,提醒该参数缺失并防止错位
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 获取真实sql, mybatis将其存在ThreadLocal中
     * @return
     * @throws IllegalAccessException
     * @throws NoSuchFieldException
     */
    private String getRealSql() throws IllegalAccessException, NoSuchFieldException {
        Field field = ErrorContext.class.getDeclaredField("sql");
        field.setAccessible(true);
        Object sql = field.get(ErrorContext.instance());
        if (sql != null) {
            return sql.toString();
        }
        return "";
    }


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

    @Override
    public void setProperties(Properties properties) {
    }
}