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>
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的内置日志工厂输出时的日志查找顺序:(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();
如果,您希望更容易地发现我的新博客,不妨点击一下左下角的【关注我】。
如果,您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是【码猿手】。