一.專案搭建
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>
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 + '}'; } }
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-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();