MyBatis 日志模块源码分析

本贴最后更新于 2166 天前,其中的信息可能已经水流花落

相关设计模式

适配器模式

@startuml
namespace  mybatis {
interface Log
class LogFactory
LogFactory .down.> Log
Jdk14LoggingImpl .up.|> Log
Slf4jImpl .up.|> Log
StdOutImpl .up.|> Log
NoLoggingImpl .up.|> Log
JakartaCommonsLoggingImpl .up.|> Log
Log4jImpl .up.|> Log
Log4j2Impl .up.|> Log
}

namespace slf4j_api {
interface Logger
}
namespace log4j_api_2 {
interface Logger
}
namespace java.lang {
class System
}
namespace java.util.logging {
class Logger
}
namespace jcl_over_slf4j {
interface Log
}
namespace log4j {
interface Logger
}
mybatis.Slf4jImpl o-down-  slf4j_api.Logger
mybatis.StdOutImpl o-down- java.lang.System
mybatis.Jdk14LoggingImpl o-down- java.util.logging.Logger
mybatis.JakartaCommonsLoggingImpl o-down- jcl_over_slf4j.Log
mybatis.Log4jImpl o-down- log4j.Logger
mybatis.Log4j2Impl o-down-  log4j_api_2.Logger
@enduml

目标接口为 Log 接口,需要适配的类为实际提供日志支持的类,比如 log4j_api_2 .Logger,
适配器实现了 Log 接口,并由 mybatis.Log4j2Impl 组成,两者之间是聚合关系。

门面模式

@startuml
namespace slf4j_api {
interface Logger
interface LocationAwareLogger
LocationAwareLogger -up-|> Logger
}
namespace log4j_slf4j_impl {
class Log4jLogger
}
log4j_slf4j_impl.Log4jLogger .up.|> slf4j_api.LocationAwareLogger

namespace Client {
}
Client -down-> slf4j_api.Logger
@enduml

slf4j_api.Logger 充当门面,Client 调用 slf4j_api.Logger 获得服务。
具体的服务支持由 og4j_slf4j_impl.Log4jLogger 或jul_over_slf4j.Log4jLogger等 等提供支持

代理模式

@startuml
interface Connection
ConnectionImpl .up.|> Connection
note left of ConnectionImpl:不需要创建此类
ConnectionProxy .up.|> Connection
ConnectionProxy  o-up- "1" ConnectionImpl
@enduml

ConnectionProxy 作为 Connection 接口实现类的代理类。

日志选择

org.apache.ibatis.logging.LogFactory 静态初始化块中

tryImplementation(new Runnable() {
  @Override
  public void run() {
    useSlf4jLogging();
  }
});
tryImplementation(new Runnable() {
  @Override
  public void run() {
    useCommonsLogging();
  }
});
tryImplementation(new Runnable() {
  @Override
  public void run() {
    useLog4J2Logging();
  }
});
tryImplementation(new Runnable() {
  @Override
  public void run() {
    useLog4JLogging();
  }
});
tryImplementation(new Runnable() {
  @Override
  public void run() {
    useJdkLogging();
  }
});
tryImplementation(new Runnable() {
  @Override
  public void run() {
    useNoLogging();
  }
});
private static void tryImplementation(Runnable runnable) {
  if (logConstructor == null) {
    try {
      runnable.run();
    } catch (Throwable t) {
      // ignore
  }
  }
}

  一共调用了 6 次 tryImplementation,每次调用传入不同的门面,按顺序找到第 1 个实现了门面逻辑的子系统,写入 logConstructor,不再往下找。

  这里虽然创建了 Runnable,但并没有启动线程,因为 tryImplementation 调用的是 run 而不是 start

以 log4j2 为例

只贴出流程中需要的代码,其他的用 ... 表示省略

public Slf4jImpl(String clazz) {
  Logger logger = LoggerFactory.getLogger(clazz);
  ...
}

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
     ...
}

iLoggerFactory 打上条件断点,条件是 name.contains("LogFactory")

在 expression 中输出 class org.apache.logging.slf4j.Log4jLoggerFactory,可看到
name 为 org.apache.logging.slf4j.Log4jLoggerFactory,在 log4j-slf4j-impl 中。

选择了 slf4j 门面,log4j-slf4j-impl 为子系统

日志打印逻辑

@startuml
abstract class BaseJdbcLogger
interface InvocationHandler
StatementLogger -up-|> BaseJdbcLogger
ConnectionLogger -up-|> BaseJdbcLogger
ResultSetLogger -up-|> BaseJdbcLogger
PreparedStatementLogger -up-|> BaseJdbcLogger
StatementLogger .down.|> InvocationHandler
ConnectionLogger .down.|> InvocationHandler
ResultSetLogger .down.|> InvocationHandler
PreparedStatementLogger .down.|> InvocationHandler
@enduml

在代理类的 invoke 方法中打印 log

打印完整日志

import lombok.extern.log4j.Log4j2;
import org.apache.commons.collections.CollectionUtils;
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 java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import org.springframework.stereotype.Component;

@Component
@Log4j2
@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 {

    @Override
  public Object intercept(Invocation invocation) throws Throwable {
        try {
            MappedStatement mappedStatement = (MappedStatement) invocation
                .getArgs()[0];
            Object parameter = null;
            if (invocation.getArgs().length > 1) {
                parameter = invocation.getArgs()[1];
            }
            String sqlId = mappedStatement.getId();
            BoundSql boundSql = mappedStatement
                .getBoundSql(parameter);
            Configuration configuration = mappedStatement.getConfiguration();
            String sql = getSql(configuration, boundSql, sqlId);
            log.info(sql);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return invocation.proceed();
    }

    private static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
        String sql = showSql(configuration, boundSql);
        return sqlId.concat(":").concat(sql);
    }

    /**
 * 如果参数是String,则添加单引号 
  * 如果是日期,则转换为时间格式器并加单引号 
  * 对参数是null和不是null的情况作了处理 
  */
  private static String getParameterValue(Object obj) {
        String value = null;
        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;
    }

    // 进行?的替换
  private static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List parameterMappings = boundSql
            .getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration
                .getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?",
                    Matcher.quoteReplacement(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("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql
                            .replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        sql = sql.replaceFirst("\\?", "missing");
                    }
                }
            }
        }
        return sql;
    }

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

    @Override
  public void setProperties(Properties properties) {

    }
}

关于拦截器、JDK 动态代理、CGLIB,有时间再开一篇。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 203 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 387 关注
1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...