Mybatis日志源码探究

一.项目搭建

  1.pom.xml

    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- spring连接池 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.30</version>
        </dependency>
    </dependencies>
View Code

  2.配置类

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Configuration
@ComponentScan("com.hrh.mybatis")
@MapperScan("com.hrh.mybatis.mapper")
public class MyBatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-config.xml"));//内置日志工厂配置
        return factoryBean;
    }
    @Bean
    public DataSource dataSource(){
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        driverManagerDataSource.setUsername("xxx");
        driverManagerDataSource.setPassword("xxx");
        driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");
        return driverManagerDataSource;
    }

}

  3.pojo类、dao类和service类

import org.springframework.stereotype.Repository;

@Repository
public class Person {
    private int id;
    private String name;
    private int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
View Code
import com.hrh.mybatis.bean.Person;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;
@Mapper
public interface PersonMapper {
    @Select("select *from tab_person;")
     List<Person> list();
}
import com.hrh.mybatis.bean.Person;
import com.hrh.mybatis.mapper.PersonMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class PersonService {
    @Autowired
    PersonMapper personMapper;

    public List<Person> getList() {
        return personMapper.list();
    }
}

  4.测试类

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonService bean = context.getBean(PersonService.class);
        List<Person> list = bean.getList();
        for (Person p : list) {
            System.out.println(p.toString());
        }
    }

二.日志输出

  以下内容是参考官网实现的:https://mybatis.org/mybatis-3/logging.htmlhttps://mybatis.org/mybatis-3/zh/logging.html

  1.使用Mybatis的内置日志工厂输出

  Mybatis 通过使用内置的日志工厂提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一:

    • SLF4J
    • Apache Commons Logging
    • Log4j 2
    • Log4j
    • JDK logging

  MyBatis 内置日志工厂会基于运行时检测信息选择日志委托实现。它会(按上面罗列的顺序)使用第一个查找到的实现。当没有找到这些实现时,将会禁用日志功能。

  不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging。注意,在这种配置环境下,MyBatis 会把 Commons Logging 作为日志工具。这就意味着在诸如 WebSphere 的环境中,由于提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。这个时候你就会感觉很郁闷:看起来 MyBatis 将你的 Log4J 配置忽略掉了(其实是因为在这种配置环境下,MyBatis 使用了 Commons Logging 作为日志实现)。

  如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志实现,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择其它日志实现。

  可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了 org.apache.ibatis.logging.Log 接口,且构造方法以字符串为参数的类完全限定名。

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 打印sql日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
</configuration>

  在 MyBatisConfig 配置类中或xml中添加 mybatis-config.xml 配置

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-config.xml"));//内置日志工厂配置
        return factoryBean;
    }
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>

 

   PS:经测试(测试背景有 log4j、logback),其实不用 mybatis-config.xml 这个配置也可以实现 log4j 或 logback 打印出 SQL 日志;

  2.在实现方法中调用指定日志:

  你应该在调用其它 MyBatis 方法之前调用以上的某个方法。另外,仅当运行时类路径中存在该日志实现时,日志实现的切换才会生效。如果你的环境中并不存在 Log4J,你却试图调用了相应的方法,MyBatis 就会忽略这一切换请求,并将以默认的查找顺序决定使用的日志实现。

org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useLog4J2Logging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();

   

  PS:经测试(测试背景有 log4j、log4j2、logback 等),发现 org.apache.ibatis.logging.LogFa

ctory.useXXXX 语句无效,无法实现指定日志打印出 SQL;

  3.log4j的其他设置

  当你想打印 SQL 的详细信息,比如执行查询时,控制台输出包括查询语句、字段名称、参数、每条数据都打印出来和总数时,可以在 log4j.properties 配置文件中添加如下配置:

log4j.logger.com.hrh.mybatis.mapper.ManagerMapper=TRACE

   

  其中 com.hrh.mybatis.mapper.ManagerMapper 是 ManagerMapper 接口的详细路径(对应XML的命名空间),当配置完上面的信息,执行 ManagerMapper 接口的方法时会输出详细的信息,也可以只针对特定方法进行详细输出,配置是

log4j.logger.com.hrh.mybatis.mapper.ManagerMapper.list=TRACE

   以此类推,当配置下面信息时,会对 mapper 包下的所有类进行详细输出:

log4j.logger.com.hrh.mybatis.mapper=TRACE

   某些查询可能会返回庞大的结果集。这时,你可能只想查看 SQL 语句,而忽略返回的结果集。为此,SQL 语句将会在 DEBUG 日志级别下记录(JDK 日志则为 FINE)。返回的结果集则会在 TRACE 日志级别下记录(JDK 日志则为 FINER)。因此,只要将日志级别调整为 DEBUG 即可:

log4j.logger.com.hrh.mybatis.mapper.ManagerMapper=DEBUG

 

三.源码探究

   我们从依赖 mybatis-3.4.6.jar 的logging包可以看出,它提供了很多日志框架依赖的实现类,那么它是怎么确定使用哪种日志呢?我们可以从 LogFactory 这个类来探究下它的实现原理。

  从 LogFactory 类我们可以看到它有一些静态代码,这表示当 Spring Framework 加载这个类时,会执行这些静态代码,一个个按顺序执行,同样这些静态代码验证了前面日志输出的第一点讲到了使用Mybatis的内置日志工厂输出时的日志查找顺序:(Mybatis 默认使用 Slf4j)

public final class LogFactory {

  /**
   * Marker to be used by logging implementations that support markers
   */
  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;//全局变量,重要

  static {
    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();
      }
    });
  }
  .........
}

  下面我们看 tryImplementation 方法:第一次进来肯定等于空,表示 runnable 会执行 run 方法,后面会执行 useXXLogging() 方法,当执行 useXXLogging() 方法后  logConstructor 变量就不等于空了,后面的tryImplementation 不会再执行 run 方法了

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  接下来我们看看 useXXLogging() 方法:从中我们可以看到它是给 setImplementation 方法传递了一个日志实现类的class对象,然后通过获取 class 的构造方法,再通过构造方法创建出日志实现类的实例给 logConstructor 这个全局变量(如果创建实例失败,则抛出异常,然后再执行后面的 tryImplementation 方法),后面通过 LogFactory#getLog() 方法就可以得到 Log 对象

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {//判断是否允许输出debug信息,jul永远为false,不是debug级别,jul的配置不能修改,当日志是log4j时,则可在配置文件中定义日志级别
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  } 

  LogFactory#getLog() :

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  当如果项目依赖了 log4j,这时我们通过 debug 可以得知项目是经过 useCommonsLogging() 加载了 jcl (commons-logging.jar,从前文Spring笔记(10) - 日志体系可以得知 Spring Framework 是包含jcl日志的,案例项目中使用的是 Spring Framework 4.x,Spring Framework 5.x 不支持 Log4j)。因此 MyBatisConfig#sqlSessionFactory() 中的 SqlSessionFactoryBean 的 logger 对象是:

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

  private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
  
}

  Spring Framework 4.x 的日志框架默认为 jul,其中 log4j>jul,所以此时控制台打印的是 log4j 日志框架的日志信息。

  当然我们可以通过mybatis-config.xml 配置文件对 logConstructor 变量进行再赋值,比如配置信息是  <setting name="logImpl" value="LOG4J" />,而且配置文件也加载到了项目中,这时虽然 logConstructor 的值因Spring Framework 加载而是 jul,但当加载执行到 MyBatisConfig#sqlSessionFactory() 的 

factoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-config.xml"));

语句时,会重新进入 setImplementation 给 logConstructor 重新赋值,如下图所示:

当然,你同样可以通过如下配置来实现指定日志框架:(推荐使用

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setLogImpl(Log4jImpl.class);
        factoryBean.setConfiguration(configuration);
        return factoryBean;
    }

  前文的 isDebugEnabled 链路是:log.isDebugEnabled() -> org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl#isDebugEnabled() -> org.apache.commons.logging.LogAdapter.JavaUtilLog#isDebugEnabled()

        private static class JavaUtilLog implements Log, Serializable {

            private String name;

            private transient java.util.logging.Logger logger;

            public JavaUtilLog(String name) {
                this.name = name;
                this.logger = java.util.logging.Logger.getLogger(name);
            }
            
            public boolean isDebugEnabled() {
                return this.logger.isLoggable(java.util.logging.Level.FINE);//FINE是500,jul底层默认800,500<800,所以永远是false
            }
        }

   从上面代码可以看出,Mybatis 的 jul 包含 JavaUtilLog,JavaUtilLog中又包含了 jul,且 isDebugEnabled 永远是 false。

  那么如何来修改使其 debug 下也会打印日志呢?可以通过扩展来实现,通过实现 org.apache.ibatis.logging.Log 接口,重写它的方法,然后将接口实现加入到 Spring 容器中;

package com.hrh.mybatis;

import java.util.logging.Logger;

public class MyLog implements org.apache.ibatis.logging.Log {
    Logger log;

    public MyLog(String name) {
        this.log = java.util.logging.Logger.getLogger(name);
    }

    @Override
    public boolean isDebugEnabled() {
        return true;//设为true
    }

    @Override
    public boolean isTraceEnabled() {
        return false;
    }

    @Override
    public void error(String s, Throwable e) {

    }

    @Override
    public void error(String s) {

    }

    @Override
    public void debug(String s) {
        log.info(s);//重写debug方法
    }

    @Override
    public void trace(String s) {

    }

    @Override
    public void warn(String s) {

    }


}

   加入到容器中:

        LogFactory.useCustomLogging(MyLog.class);
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonServiceImpl bean = context.getBean(PersonServiceImpl.class);
        List<Person> list =bean .getList();

   容器开始加载,代码经过几次 tryImplementation() 方法后 logConstructor 的值为 JakartaCommonsLoggingImpl,然后再执行 LogFactory.useCustomLogging(MyLog.class); 这行代码,这时 logConstructor 值发生了变化,变成了 MyLog 实例对象:

 

 

 

四.问题探究

  前面中讲到在实现方法中调用指定日志实现日志切换,但却发现失效,无法实现,这是为什么呢?

  这时我们可以从下图得知:

  由此当代码变为下面时,就可以实现日志切换了:

        org.apache.ibatis.logging.LogFactory.useStdOutLogging();
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonService bean = context.getBean(PersonService.class);
        List<Person> list = bean.getList();

posted @ 2021-03-29 19:46  码猿手  阅读(387)  评论(0编辑  收藏  举报
Live2D