Mybatis日誌原始碼探究

碼猿手發表於2021-03-29

一.專案搭建

  1.pom.xml

Mybatis日誌原始碼探究
    <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類

Mybatis日誌原始碼探究
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的內建日誌工廠輸出時的日誌查詢順序:

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()) {
        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)。因此 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;
    }

 四.問題探究

  前面中講到在實現方法中呼叫指定日誌實現日誌切換,但卻發現失效,無法實現,這是為什麼呢?

  這時我們可以從下圖得知:

  由此當程式碼變為下面時,就可以實現日誌切換了:

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

相關文章