JavaMybatis
Java 开发中常用的几款日志框架有很多种,并且这些日志框架来源于不同的开源组织,给用户暴露的接口也有很多不同之处,所以很多开源框架会自己定义一套统一的日志接口,兼容上述第三方日志框架,供上层使用。
一般实现的方式是使用适配器模式,将各个第三方日志框架接口转换为框架内部自定义的日志接口。MyBatis 也提供了类似的实现,这里就来简单了解一下。

适配器模式是什么?

简单来说,适配器模式主要解决的是由于接口不能兼容而导致类无法使用的问题,这在处理遗留代码以及集成第三方框架的时候用得比较多。其核心原理是:通过组合的方式,将需要适配的类转换成使用者能够使用的接口。

日志模块

MyBatis 自定义的 Log 接口位于 org.apache.ibatis.logging 包中,相关的适配器也位于该包中。
首先是 LogFactory 工厂类,它负责创建 Log 对象,在 LogFactory 类中有一段静态代码块,其中会依次加载各个第三方日志框架的适配器。

  1. static {
  2. tryImplementation(LogFactory::useSlf4jLogging);
  3. tryImplementation(LogFactory::useCommonsLogging);
  4. tryImplementation(LogFactory::useLog4J2Logging);
  5. tryImplementation(LogFactory::useLog4JLogging);
  6. tryImplementation(LogFactory::useJdkLogging);
  7. tryImplementation(LogFactory::useNoLogging);
  8. }

以 JDK Logging 的加载流程(useJdkLogging() 方法)为例,其具体代码实现和注释如下:

  1. /**
  2. * 首先会检测 logConstructor 字段是否为空,
  3. * 1.如果不为空,则表示已经成功确定当前使用的日志框架,直接返回;
  4. * 2.如果为空,则在当前线程中执行传入的 Runnable.run() 方法,尝试确定当前使用的日志框架
  5. */
  6. private static void tryImplementation(Runnable runnable) {
  7. if (logConstructor == null) {
  8. try {
  9. runnable.run();
  10. } catch (Throwable t) {
  11. // ignore
  12. }
  13. }
  14. }
  15. public static synchronized void useJdkLogging() {
  16. setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  17. }
  18. private static void setImplementation(Class<? extends Log> implClass) {
  19. try {
  20. // 获取适配器的构造方法
  21. Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
  22. // 尝试加载适配器,加载失败会抛出异常
  23. Log log = candidate.newInstance(LogFactory.class.getName());
  24. // 加载成功,则更新logConstructor字段,记录适配器的构造方法
  25. logConstructor = candidate;
  26. } catch (Throwable t) {
  27. throw new LogException("Error setting Log implementation. Cause: " + t, t);
  28. }
  29. }

打印SQL语句

如何开启打印

这里演示Mybatis在运行时怎么输出SQL语句,具体分析见原理章节。

单独使用Mybatis

在mybatis.xml配置文件中添加如下配置:

  1. <setting name="logImpl" value="STDOUT_LOGGING" />

和SpringBoot整合

有两种方式,第一种也是利用StdOutImpl实现类去实现打印,在application.yml配置文件填写如下:

  1. #mybatis配置
  2. mybatis:
  3. # 控制台打印sql日志
  4. configuration:
  5. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

其次还可以通过指定日志级别来输出SQL语句:
SpringBoot默认使用的SL4J(日志门面)+Logback(具体实现)的日志组合

  1. logging:
  2. level:
  3. xx包名: debug

简单分析原理

这里直接看到org.apache.ibatis.executor.BaseExecutor#getConnection方法,了解Mybatis的应该都知道Mybatis在执行sql操作的时候会去获取数据库连接

  1. protected Connection getConnection(Log statementLog) throws SQLException {
  2. Connection connection = transaction.getConnection();
  3. // 判断日志级别是否为Debug,是的话返回代理对象
  4. if (statementLog.isDebugEnabled()) {
  5. return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  6. } else {
  7. return connection;
  8. }
  9. }

可以看到注释的那行,它通过判断日志级别来判断是否返回ConnectionLogger代理对象,那么前面提到 Log 接口的实现类中StdOutImpl它的isDebugEnabled其实是永远返回 true,代码如下:
并且它直接用的 System.println去输出的SQL信息

  1. public class StdOutImpl implements Log {
  2. // ...省略无关代码
  3. @Override
  4. public boolean isDebugEnabled() {
  5. return true;
  6. }
  7. @Override
  8. public boolean isTraceEnabled() {
  9. return true;
  10. }
  11. @Override
  12. public void error(String s, Throwable e) {
  13. System.err.println(s);
  14. e.printStackTrace(System.err);
  15. }
  16. @Override
  17. public void error(String s) {
  18. System.err.println(s);
  19. }
  20. // ...省略无关代码
  21. }

到这里起码知道了为什么通过配置 MyBatis 所用日志的具体实现 logImpl就可以实现日志输出到控制台的效果了。
那么还可以深究一下 statementLog 是在什么时候变成 StdOutImpl的,在解析Mybatis配置文件的时候,会去读取配置的logImpl属性,然后通过LogFactory.useCustomLogging方法先指定好适配器的构造方法

  1. // org.apache.ibatis.builder.xml.XMLConfigBuilder#loadCustomLogImpl
  2. private void loadCustomLogImpl(Properties props) {
  3. Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
  4. configuration.setLogImpl(logImpl);
  5. }
  6. public void setLogImpl(Class<? extends Log> logImpl) {
  7. if (logImpl != null) {
  8. this.logImpl = logImpl;
  9. LogFactory.useCustomLogging(this.logImpl);
  10. }
  11. }

然后在构建MappedStatement的时候就已经将日志对象初始化好了
每个MappedStatement对应了自定义Mapper接口中的一个方法,它保存了开发人员编写的SQL语句、参数结构、返回值结构、Mybatis对它的处理方式的配置等细节要素,是对一个SQL命令是什么、执行方式的完整定义。

  1. public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
  2. // ...省略无关代码
  3. mappedStatement.statementLog = LogFactory.getLog(logId);
  4. mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
  5. }
  6. public static Log getLog(String logger) {
  7. try {
  8. return logConstructor.newInstance(logger);
  9. } catch (Throwable t) {
  10. throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
  11. }
  12. }

最后SpringBoot的就不概述了

  • 第一种方式其实也是同理
  • 第二种方式是通过修改了日志级别,然后使 isDebugEnabled 返回true,去返回代理对象,然后去输出SQL语句。

感兴趣的还可以看看SQL语句的输出是怎么输出的,具体在 ConnectionLoggerinvoke方法中,会发现熟悉的Preparing: “和”Parameters: “。