自上次的PrepareStatement输出完整SQL语句之后,工作上又接到如本文标题所示的需求。
1. 前言
提出这个需求的项目现状是: 前期的持久层执行都是使用原始的JDBC完成的,后期切换为Mybatis,所以选择使用Druid来进行SQL日志文件的输出。因此该需求的准确描述是:“如果该语句是Mybatis相关的,就同时打印出SqlId。”
2. 分析
- 按照前一篇文章PrepareStatement输出完整SQL语句中提到的,日志基类
LogFilter
中的私有方法logExecutableSql
; 将执行SQL中的 ? 替换为 实际的值, 便于调试。 - 因为
logExecutableSql
方法是私有的,所以我们退而求次地找到了方法logParameter
,该方法的修饰级别为protected
,所以我们是可以覆写其逻辑的。 - 方法
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. 配置
到此开发工作算是基本完成了,接下来我们需要将它们并入到执行的生命周期中。
首先是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>
- 然后就是配置Druid的自定义Filter。这一步我就偷个懒省略了,网上例子太多了。
5. 效果图
6. 补充
- 在Druid打印出来的日志里, 搜索 类似
{conn-10007, pstmt-20018}
就可以找到相关的参数, SqlId, SQL语句了。 LogFilter
类中的实现表明 : 当前只有为PreparedStatementProxy
时才输出参数日志。所以要以上的扩展生效,我们需要在编写Mybatis映射文件时,使用# { }
。不过出于SQL注入安全的考虑,我们一般都是采用这种方式,所以这一块不是大问题。if (statement instanceof PreparedStatementProxy) { logParameter((PreparedStatementProxy) statement); }
DruidPooledPreparedStatement
的基类DruidPooledStatement
进行了toString
的重写, 所以我们在IDE里进行调试时,比如Eclipse里的Variables里查看时,显示出来的不是实际类型。