1. @Intercepts 简介

MyBatis@Intercepts 注解用于表明当前的对象是一个拦截器,其配置值是一个@Signature数组,而@Signature 则用于声明要拦截的接口、方法以及对应的参数列表。所谓拦截器的作用就是可以拦截某些方法的调用,和 Spring 中的 AOP 是完全一致的。
MyBatis 拦截器设计的初衷是为用户提供一个实现自定义逻辑的解决方法,而不必去改动 MyBatis 自身的逻辑。比如如果认为几种实现Executor接口的子类的query方法都不能满足要求,就可以建立一个拦截器用于拦截 Executor接口的query方法,实现自己的 query方法逻辑

读者如对拦截器的原理有兴趣,可参考 MyBatis 拦截器 Interceptor 源码解析

2. 使用 @Intercepts 实现打印 SQL 语句

2.1 实现拦截器

  1. 拦截器的核心逻辑实现类 LogSqlHelper如下,其主要逻辑为取出 MappedStatement 封装的 SQL 语句,将参数格式化后填入 SQL 语句中,再根据 slowSqlThreshold 参数配置判断其耗时是否超过阈值,从而决定是否需要打印 SQL 语句

    需注意 SQL 语句参数的填充涉及到字符串的匹配替换,需要使用一定的 CPU 资源,如果 SQL 语句非常庞大,并且有非常多的参数需要填充那可能会使机器的 CPU 使用率飙升到 100,读者如有兴趣可参考 Java 线上机器 CPU 100 的一次排查过程

    public class LogSqlHelper {
    
     private static final Logger log = LoggerFactory.getLogger(LogSqlHelper.class);
    
     private static final String SELECT = "select";
    
     private static final String FROM = "from";
    
     private static final String SIMPLE_SELECT = "select * ";
    
     private static final int MAX_SQL_LENGTH = 120;
    
     private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
    
     public LogSqlHelper() {
     }
    
     public static Object intercept(Invocation invocation, int slowSqlThreshold, boolean optimizeSql) throws Throwable {
         long startTime = System.currentTimeMillis();
         Object returnValue = invocation.proceed();
         long cost = System.currentTimeMillis() - startTime;
         if (cost >= (long) slowSqlThreshold) {
             log.info("cost = {} ms, affected rows = {}, SQL: {}",
                     cost, formatResult(returnValue), formatSql(invocation, optimizeSql));
         }
         return returnValue;
     }
    
     private static Object formatResult(Object obj) {
         if (obj == null) {
             return "NULL";
         } else if (obj instanceof List) {
             return ((List) obj).size();
         } else if (!(obj instanceof Number) && !(obj instanceof Boolean) && !(obj instanceof Date)
                 && !(obj instanceof String)) {
             return obj instanceof Map ? ((Map) obj).size() : 1;
         } else {
             return obj;
         }
     }
    
     private static String formatSql(Invocation invocation, boolean isOptimizeSql) {
         MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
         Object parameter = null;
         if (invocation.getArgs().length > 1) {
             parameter = invocation.getArgs()[1];
         }
    
         BoundSql boundSql = mappedStatement.getBoundSql(parameter);
         Configuration configuration = mappedStatement.getConfiguration();
         Object parameterObject = boundSql.getParameterObject();
         List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
         String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
         String formatSql = sql.toLowerCase();
         if (isOptimizeSql && formatSql.startsWith(SELECT) && formatSql.length() > MAX_SQL_LENGTH) {
             sql = SIMPLE_SELECT + sql.substring(formatSql.indexOf(FROM));
         }
    
         if (parameterMappings.size() > 0 && parameterObject != null) {
             TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
             if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                 sql = sql.replaceFirst("\\?", formatParameterValue(parameterObject));
             } else {
                 MetaObject metaObject = configuration.newMetaObject(parameterObject);
                 for (ParameterMapping parameterMapping : parameterMappings) {
                     String propertyName = parameterMapping.getProperty();
                     Object obj;
                     if (metaObject.hasGetter(propertyName)) {
                         obj = metaObject.getValue(propertyName);
                         sql = sql.replaceFirst("\\?", formatParameterValue(obj));
                     } else if (boundSql.hasAdditionalParameter(propertyName)) {
                         obj = boundSql.getAdditionalParameter(propertyName);
                         sql = sql.replaceFirst("\\?", formatParameterValue(obj));
                     }
                 }
             }
         }
         return sql;
     }
    
     private static String formatParameterValue(Object obj) {
         if (obj == null) {
             return "NULL";
         } else {
             String value = obj.toString();
             if (obj instanceof Date) {
                 DateFormat dateFormat = new SimpleDateFormat(PATTERN);
                 value = dateFormat.format((Date) obj);
             }
             if (!(obj instanceof Number) && !(obj instanceof Boolean)) {
                 value = "'" + value + "'";
             }
             return value;
         }
     }
    }
    
  2. 通过 @Intercepts 注解声明 LogQueryAndUpdateSqlHandler 为拦截器,并使用@Signature配置拦截器拦截的目标接口,目标方法,目标方法的入参

    @Intercepts({
         @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
         @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
    })
    public class LogQueryAndUpdateSqlHandler implements Interceptor {
     private int slowSqlThreshold;
     private boolean isOptimizeSql;
    
     public LogQueryAndUpdateSqlHandler() {
     }
    
     public LogQueryAndUpdateSqlHandler(int slowSqlThreshold) {
         this.slowSqlThreshold = slowSqlThreshold;
     }
    
     public LogQueryAndUpdateSqlHandler(boolean isOptimizeSql) {
         this.isOptimizeSql = isOptimizeSql;
     }
    
     public LogQueryAndUpdateSqlHandler(int slowSqlThreshold, boolean isOptimizeSql) {
         this.slowSqlThreshold = slowSqlThreshold;
         this.isOptimizeSql = isOptimizeSql;
     }
    
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
         return LogSqlHelper.intercept(invocation, this.slowSqlThreshold, this.isOptimizeSql);
     }
    
     @Override
     public Object plugin(Object target) {
         return target instanceof Executor ? Plugin.wrap(target, this) : target;
     }
    
     @Override
     public void setProperties(Properties properties) {
     }
    }
    

2.2 将拦截器注入 Spring

通过自动配置类 LogSqlAutoConfiguration将拦截器注入到 Spring 中,使拦截器能够真正生效

@ConditionalOnClass({SqlSessionFactory.class})
@Configuration
public class LogSqlAutoConfiguration {
 public LogSqlAutoConfiguration() {
 }

 @Bean
 @ConditionalOnProperty(name = {"log.printQueryAndUpdateSql"}, havingValue = "true", matchIfMissing = true)
 public LogQueryAndUpdateSqlHandler getLogSqlHandler() {
     return new LogQueryAndUpdateSqlHandler(true);
 }
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐