天天看點

Mybatis日志源碼探究

Mybatis筆記

一.項目搭建

  1.pom.xml

Mybatis日志源碼探究
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日志源碼探究
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.html、https://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日志源碼探究
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>      
Mybatis日志源碼探究

   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();      

   

Mybatis日志源碼探究

  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      

   

Mybatis日志源碼探究

  其中 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日志源碼探究

三.源碼探究

   我們從依賴 mybatis-3.4.6.jar 的logging包可以看出,它提供了很多日志架構依賴的實作類,那麼它是怎麼确定使用哪種日志呢?我們可以從 LogFactory 這個類來探究下它的實作原理。

Mybatis日志源碼探究

  從 LogFactory 類我們可以看到它有一些靜态代碼,這表示當 Spring Framework 加載這個類時,會執行這些靜态代碼,一個個按順序執行,同樣這些靜态代碼驗證了前面日志輸出的第一點講到了使用Mybatis的内置日志工廠輸出時的日志查找順序:

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 對象

Mybatis日志源碼探究
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 對象是:

Mybatis日志源碼探究
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 重新指派,如下圖所示:

Mybatis日志源碼探究

當然,你同樣可以通過如下配置來實作指定日志架構:(推薦使用)

@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;
    }      

 四.問題探究

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

Mybatis日志源碼探究

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

Mybatis日志源碼探究

  由此當代碼變為下面時,就可以實作日志切換了:

org.apache.ibatis.logging.LogFactory.useStdOutLogging();
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonService bean = context.getBean(PersonService.class);
        List<Person> list = bean.getList();      
Mybatis日志源碼探究