Druid技巧之输出Mybatis的SqlId

自上次的PrepareStatement输出完整SQL语句之后,工作上又接到如本文标题所示的需求。

1. 前言

提出这个需求的项目现状是: 前期的持久层执行都是使用原始的JDBC完成的,后期切换为Mybatis,所以选择使用Druid来进行SQL日志文件的输出。因此该需求的准确描述是:“如果该语句是Mybatis相关的,就同时打印出SqlId。”

2. 分析

  1. 按照前一篇文章PrepareStatement输出完整SQL语句中提到的,日志基类LogFilter中的私有方法logExecutableSql; 将执行SQL中的 ? 替换为 实际的值, 便于调试。
  2. 因为logExecutableSql方法是私有的,所以我们退而求次地找到了方法logParameter,该方法的修饰级别为protected,所以我们是可以覆写其逻辑的。
  3. 方法logParameter的签名有着唯一的参数statement,其类型为PreparedStatementProxy。这就给予了我们加入自定义逻辑的可能性(在执行逻辑达到方法logParameter前替换掉默认实现)。

3. 实现

按照上文分析的结果,于是有了如下设计。

3.1 接口MybatisSqlIdProvider

该接口的主要作用是提供一个标志,标识本次需要进行Mybatis SqlId的日志输出

// 注意本接口不需要public化
interface MybatisSqlIdProvider {
    String getMybatisSqlId();
}

3.2 扩展Druid之CustomPreparedStatementProxyImpl

直接继承自Druid中的PreparedStatementProxyImpl类。

final class CustomPreparedStatementProxyImpl extends PreparedStatementProxyImpl implements MybatisSqlIdProvider {

    private final MappedStatement mappedStatement;

    public CustomPreparedStatementProxyImpl(PreparedStatementProxyImpl under, MappedStatement ms) {
        super(under.getConnectionProxy(), under.getRawObject(), under.getSql(), under.getId());

        this.mappedStatement = ms;
    }

    @Override
    public String getMybatisSqlId() {
        return mappedStatement.getId();
    }

}

3.3 扩展Druid之CustomDruidPooledPreparedStatement

直接继承自Druid中的DruidPooledPreparedStatement类。

final class CustomDruidPooledPreparedStatement extends DruidPooledPreparedStatement {

    public CustomDruidPooledPreparedStatement(DruidPooledPreparedStatement under, MappedStatement ms)
            throws SQLException {
        super((DruidPooledConnection) under.getConnection(), decorate(under.getPreparedStatementHolder(), ms));

    }

    private static PreparedStatementHolder decorate(PreparedStatementHolder holder, MappedStatement ms) {
        ReflectUtil.setFieldValue(holder, "statement",
                new CustomPreparedStatementProxyImpl((PreparedStatementProxyImpl) holder.statement, ms));

        return holder;
    }
}

3.4 扩展Mybatis之LogSqlIdIntoDruidLogInterceptor

我们需要用上面的自定义扩展类替换掉Druid中相应的默认实现。

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class LogSqlIdIntoDruidLogInterceptor implements Interceptor {
    private static final Logger LOG = LoggerFactory.getLogger(LogSqlIdIntoDruidLogInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        final StatementHandler shPLugined = (StatementHandler) invocation.getTarget();
        final StatementHandler shOrigin = (StatementHandler) PluginUtils
                .<StatementHandler>getOriginalObject(shPLugined);

        final MappedStatement ms = MetaObjectUtil.<MappedStatement>eval("delegate.mappedStatement", shOrigin);//(MappedStatement) ReflectUtil.getFieldValue(shOrigin, "delegate.mappedStatement");

        final Object proceed = invocation.proceed();
        LOG.debug("### current statement decorated By LQ type is [ {} ]", ClassUtil.getClassName(proceed, true));
        if (proceed instanceof DruidPooledPreparedStatement) {
            return preparedStatement((DruidPooledPreparedStatement) proceed, ms);
        } else {
            return proceed;
        }

    }

    private Object preparedStatement(DruidPooledPreparedStatement statement, final MappedStatement ms)
            throws SQLException {
        // DruidPooledPreparedStatement的基类DruidPooledStatement 进行了 toString的重写
        return new CustomDruidPooledPreparedStatement(statement, ms);
    }

    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        // 我们暂时不需要外部的自定义配置属性
    }

}

3.5 扩展Druid之DruidSlf4jLogFilterExtendEx

/**
 * 再扩展, 输出SQL语句的同时再输出Mybatis SqlId
 * @author LQ
 *
 */
public final class DruidSlf4jLogFilterExtendEx extends DruidSlf4jLoggerEx {
    @Override
    protected void logParameter(PreparedStatementProxy statement) {
        // 因为Druid将logExecutableSql设置为private, 所以只能退出求其次覆写本方法.
        outputMyBatisSqlId(statement);
        super.logParameter(statement);
    }

    @Override
    protected void statement_executeErrorAfter(StatementProxy statement, String sql, Throwable error) {
        // 报错时更要打印出SqlId
        outputMyBatisSqlId(statement);
        super.statement_executeErrorAfter(statement, sql, error);
    }

    private void outputMyBatisSqlId(StatementProxy statement) {
        if (statement instanceof MybatisSqlIdProvider) {
            final String mybatisSqlId = Convert.convert(MybatisSqlIdProvider.class, statement)
                    .getMybatisSqlId();

            StringBuffer buf = new StringBuffer();
            buf.append("{conn-");
            buf.append(statement.getConnectionProxy().getId());
            buf.append(", ");
            buf.append(stmtId(statement));
            buf.append("}");
            buf.append(" SqlId : [");

            buf.append(mybatisSqlId);

            buf.append("]");
            statementLog(buf.toString());
        }
    }

    // 基类中设置为private, 所以我们复制一份
    private String stmtId(StatementProxy statement) {
        StringBuffer buf = new StringBuffer();
        if (statement instanceof CallableStatementProxy) {
            buf.append("cstmt-");
        } else if (statement instanceof PreparedStatementProxy) {
            buf.append("pstmt-");
        } else {
            buf.append("stmt-");
        }
        buf.append(statement.getId());

        return buf.toString();
    }
}

4. 配置

到此开发工作算是基本完成了,接下来我们需要将它们并入到执行的生命周期中。

  1. 首先是Mybatis配置,这里我们编写的是Mybatis里的拦截器,所以配置如下

    <plugins>
        <!-- 分页插件 -->
         <plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
             <property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.OracleDialect"/>
         </plugin>
         <!-- 按照Mybatis中plugin的执行机制,我们需要将我们自定义的拦截器配置在下方 -->
         <plugin interceptor="com.zzz.base.thirdjar.mybatis.LogSqlIdIntoDruidLogInterceptor">            
         </plugin>        
     </plugins>
  2. 然后就是配置Druid的自定义Filter。这一步我就偷个懒省略了,网上例子太多了。

5. 效果图

效果图

6. 补充

  1. 在Druid打印出来的日志里, 搜索 类似 {conn-10007, pstmt-20018} 就可以找到相关的参数, SqlId, SQL语句了。
  2. LogFilter类中的实现表明 : 当前只有为 PreparedStatementProxy 时才输出参数日志。所以要以上的扩展生效,我们需要在编写Mybatis映射文件时,使用 # { }。不过出于SQL注入安全的考虑,我们一般都是采用这种方式,所以这一块不是大问题。

    if (statement instanceof PreparedStatementProxy) {
        logParameter((PreparedStatementProxy) statement);
    }
  3. DruidPooledPreparedStatement的基类 DruidPooledStatement 进行了 toString的重写, 所以我们在IDE里进行调试时,比如Eclipse里的Variables里查看时,显示出来的不是实际类型。

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/80367097