Mybatis日志模块和适配器模式、代理模式
一、日志模块
- Mybatis本身并没有实现日志功能,换言之框架本身是不具备打印日志的能力的,但是我们在使用的过程中却是可以看到打印相关的日志的,比如预编译的语句信息等,Mybatis在设计日志模块时遵循的原则是:
1、使用已有的日志组件。它会尝试加载项目依赖的日志组件,加载到哪一个就使用哪一个。
2、加载时遵循优先级,按照优先级加载,优先级如下:slf4j > common logging > log4j2 > log4j > jdk logging > 没有日志
二、适配器模式
- 适配器模式请参考,这里不赘述07-结构型模式(中)
三、目录结构
- 包:org.apache.ibatis.logging包
- MyBatis中的日志可以接入不同的实现类,如图所示slf4j,log4j2,log4j,jdklog甚至没有日志组件也没关系,另外jdbc包下提供了能够具备日志打印功能的代理类,代理了JDBC的Statement,ResultSet,PreparedStatement和Connection,增强这些类让这些类具备日志能力,由此我们才能看到预编译的语句信息。
四、源码解析
- 因为在这里面有很多的适配者,目标接口只有一个,但是适配者有很多,比如要适配前面提到的很多种,因此为了方便,我们先看slf4j的,后面方法类似在小结一下
4.1 目标接口
- 目标接口就是MyBatis的框架源码里面使用的接口,是MyBatis自定义的一个接口。如下所示,Mybatis定义的日志接口如下,但是Mybatis自身是没有实现的,实际使用的是第三方的日志接口
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
4.2 适配者
- slf4j包含2个适配者,Slf4jLocationAwareLoggerImpl和Slf4jLoggerImpl,并且这两个类也实现了目标接口,因此在后面的适配器Slf4jImpl中直接使用Log在内部持有他们(看源码注释,有两个类是因为在JDK的不同版本有所不同)
- Slf4jLocationAwareLoggerImpl
/**
* @author Eduardo Macarron
* 适配者,将目标接口Log的方法调用转换为LocationAwareLogger自身logger实例的方法调用
*/
class Slf4jLocationAwareLoggerImpl implements Log {
private static Marker MARKER = MarkerFactory.getMarker(LogFactory.MARKER);
private static final String FQCN = Slf4jImpl.class.getName();
private LocationAwareLogger logger;
Slf4jLocationAwareLoggerImpl(LocationAwareLogger logger) {
this.logger = logger;
}
/**
* 实现Log接口,并重写对应的方法,在方法内部调用的是slf4j的日志实现,
*/
@Override
public boolean isDebugEnabled() {
return logger.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return logger.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
logger.log(MARKER, FQCN, LocationAwareLogger.ERROR_INT, s, null, e);
}
@Override
public void error(String s) {
logger.log(MARKER, FQCN, LocationAwareLogger.ERROR_INT, s, null, null);
}
@Override
public void debug(String s) {
logger.log(MARKER, FQCN, LocationAwareLogger.DEBUG_INT, s, null, null);
}
@Override
public void trace(String s) {
logger.log(MARKER, FQCN, LocationAwareLogger.TRACE_INT, s, null, null);
}
@Override
public void warn(String s) {
logger.log(MARKER, FQCN, LocationAwareLogger.WARN_INT, s, null, null);
}
}
- Slf4jLoggerImpl
/**
* @author Eduardo Macarron
* 适配者,将目标接口Log的方法调用转换为Slf4jLoggerImpl自身log实例的方法调用
*/
class Slf4jLoggerImpl implements Log {
private Logger log;
public Slf4jLoggerImpl(Logger logger) {
log = logger;
}
/**
* 实现Log接口,并重写对应的方法,在方法内部调用的是slf4j的日志实现,
*/
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.error(s, e);
}
@Override
public void error(String s) {
log.error(s);
}
@Override
public void debug(String s) {
log.debug(s);
}
@Override
public void trace(String s) {
log.trace(s);
}
@Override
public void warn(String s) {
log.warn(s);
}
}
4.3 适配器Slf4jImpl
- 适配器模式中,适配器的角色需要实现目标接口(便于对外提供功能服务),同时还需要在内部持有适配者接口,因为最终要调用的是适配者接口。
public class Slf4jImpl implements Log {
//1.这里的Log是用于传入适配者
//这里有点特殊,一般来说适配者不一定实现了目标接口,比如第三方支付的时候第三方支付接口和自己使用的目标接口一般就不一样,
//因此这里持有的就是第三方的接口,但是在这里Slf4jLocationAwareLoggerImpl和Slf4jLoggerImpl这两个适配者也实现了Log
//接口,因此就统一起来了,不管是哪一个适配者都是用同一个接口来接收,其实即便Slf4jLocationAwareLoggerImpl没有实现Log
//接口也没有问题,他实现了A接口,这里用A接收就好了,调用的时候转换调用它的接口即可,整体来看就是适配器模式的经典运用
private Log log;
//2.通过构造方法传入是配置,对于slf4j来说,这里有2中可能,因此适配者有2种可能(看注释好像和版本有关)
//分别对应Slf4jLocationAwareLoggerImpl和Slf4jLoggerImpl
public Slf4jImpl(String clazz) {
Logger logger = LoggerFactory.getLogger(clazz);
//3.大于1.6的版本,适配者是Slf4jLocationAwareLoggerImpl
if (logger instanceof LocationAwareLogger) {
try {
// check for slf4j >= 1.6 method signature
logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
return;
} catch (SecurityException e) {
// fail-back to Slf4jLoggerImpl
} catch (NoSuchMethodException e) {
// fail-back to Slf4jLoggerImpl
}
}
// Logger is not LocationAwareLogger or slf4j version < 1.6
//4.小于1.6的版本,适配者是Slf4jLoggerImpl
log = new Slf4jLoggerImpl(logger);
}
/**
* Slf4jImpl是适配器,因此
* 1.实现Log接口,并重写对应的方法,便于对外调用
* 2.在方法内部调用的是slf4j的日志实现,是Slf4jLoggerImpl或者Slf4jLocationAwareLoggerImpl
* 我们最初说过Mybatis的Log接口只是定义了自己想要的功能而已,功能的实
* 现自己并不会去做,而是绑定第三方日志组件之后交由第三方组件去做,这里
* 看的很清楚了,就是交给slf4j组件去做。
*/
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.error(s, e);
}
@Override
public void error(String s) {
log.error(s);
}
@Override
public void debug(String s) {
log.debug(s);
}
@Override
public void trace(String s) {
log.trace(s);
}
@Override
public void warn(String s) {
log.warn(s);
}
}
4.4 小结
- 适配器类会实现Mybatis的日志接口,然后内部重写继承自Mybatis接口的方法,与此同时适配器类内部还会持有一个目标接口的对象也就是一个Slf4j的实现类对象,在继承自Mybatis的Log方法的内部就是调用Slf4j的实现类的方法,由此完成了由Mybatis接口到Slf4j接口的转换适配。
五、其他适配器
- 我们再看一个相对简单的,Log4jImpl是Mybatis的Log到log4j的适配器,只有一个类就搞定了。这里的Log4jImpl类就是适配器,它实现了Log接口,而适配者就是构造方法的创建的log对象,因此很简单。
/**
* 适配器,将目标接口Log的方法调用转换为Logger自身log实例的方法调用
*/
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
- 其他的还有几种适配器,就不一一解读了,方法都类似
六、工厂LogFactory
- LogFactory是日志工厂,具体Mybatis和哪一个日志组件绑定,如何绑定,整个实现流程都在这里面实现,我们来解读。
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers
* 给支持marker功能的logger使用(目前有slf4j, log4j2)
*/
public static final String MARKER = "MYBATIS";
/**
* 存放绑定的日志框架的构造方法;(绑定哪个日志框架,就把这个日志框架所对应logger的构造函数放进来)
*/
private static Constructor<? extends Log> logConstructor;
/**
* 1.静态代码块,用来完成Mybatis和第三方日志框架的绑定过程
* 2.优先级别是 slf4j > common logging > log4j2 > log4j > jdk logging > 没有日志
* 3.执行逻辑是:按照优先级别的顺序,依次尝试绑定对应的日志组件,一旦绑定成功,后面的就不会再执行了。
* 我们看tryImplementation方法,tryImplementation方法首先会判断logConstructor是否为空,为空则尝试绑定,
* 不为空就什么都不做(不空说明已经绑定成功)。
* 4.假如第一次进来绑定slf4j,logConstructor肯定为空,那么在useSlf4jLogging方法的逻辑里面就会将slf4j的构造方法放到logConstructor里面去,
* 后面再执行common logging的绑定流程时发现logConstructor不为空,说明前面已经成功初始化了,就不会执行了;
* 反过来假如slf4j绑定失败,比如依赖包没有或者版本之类的报错,那么setImplementation抛出异常,在tryImplementation里面捕获到异常之后会直接
* 忽略,然后就继续尝试绑定common logging,直到成功。这就是绑定的整体流程。
* */
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();
}
});
}
private LogFactory() {
// disable construction
}
/**
* 对外提供2种获取日志实例的方法,类似于Slf4j的LoggerFactory.getLogger(XXX.class);
*/
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);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
/**
* 1.下面的方法都是类似的,对应于前面绑定几种日志组件的情况,就是把对应的类放到setImplementation方法里面去做
* 具体的绑定细节,细节的处理流程时一样的。优先级降低
*/
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
//2
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
//4
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
//3
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
//5
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
//这个好像是测试用的,没看到代码中使用了
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
//6
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
/**
* 绑定方法;所有尝试绑定的动作都会走这个方法,如果已经有绑定的了,logConstructor就不为null,就不会再尝试绑定了
*/
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
/**
* 绑定的细节
*/
private static void setImplementation(Class<? extends Log> implClass) {
try {
//1.获取绑定类的构造方法
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
//2.通过构造方法创建一个实例赋值给Log,因为采用了适配器模式,传进来的都是适配者,适配者本身是实现了目标接口的,
//因此进来的类都是Log接口的子类,这是一个多态的写法
Log log = candidate.newInstance(LogFactory.class.getName());
//3.这里第2步的赋值只是为了在这里打印日志,打印提示初始化适配器的类型
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
//3.把绑定的日志组件的构造方法放到logConstructor里面,后面就不会再尝试绑定其他的日志组件了
logConstructor = candidate;
} catch (Throwable t) {
//4.抛出的异常会在tryImplementation方法中捕获,捕获之后会尝试绑定下一个日志组件
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
- 由此我们看到了整个绑定流程,优先级是:slf4j > common logging > log4j2 > log4j > jdk logging > 没有日志
七、JDBC日志代理增强
- 在日志模块的jdbc包下,包含很多个类,他们对JDBC的几个核心类进行的动态代理增强,变成了具备日志打印功能的类,我们看看StatementLogger,他是具备日志打印功能的Statement。
/**
* invoke方法,动态代理的核心方法
*
* */
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
//1.如果是Object定义的方法,使用当前对象直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
/**
* 2.如果是:"execute"、"executeUpdate"、"executeQuery"或者"addBatch"方法,那就打印日志
* EXECUTE_METHODS.add("execute");
* EXECUTE_METHODS.add("executeUpdate");
* EXECUTE_METHODS.add("executeQuery");
* EXECUTE_METHODS.add("addBatch");
* */
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug(" Executing: " + removeBreakingWhitespace((String) params[0]), true);
}
if ("executeQuery".equals(method.getName())) {
//3.如果是executeQuery查询方法会返回ResultSet,那就执行之后,将ResultSet包装成一个具备日志功能的ResultSetLogger
ResultSet rs = (ResultSet) method.invoke(statement, params);
//4.返回具备日志能力的ResultSetLogger
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
//5.如果不是executeQuery方法(其余三个方法都是返回int,不需要包装),那就直接调用,不需要使用增强了日志功能的对象
return method.invoke(statement, params);
}
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
//6.如果返回的ResultSet不是null,那就返回一个ResultSet的代理对象,一个具备日志打印能力的ResultSet
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
//7.如果是其他方法,就直接调用,不需要使用增强了日志功能的对象
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
- 在这里我们看到了,Mybatis中使用动态代理为Statement添加日志功能,同时具备日志功能的Statement返回的不是原始的ResultSet,而是具备日志功能的ResultSetLogger,就这样一环一环相扣,让JDBC的基础组件具备了日志打印功能具体还可以参照源码,结构和方法类似
八、小结
- 本文主要分析了日志模块和JDBC核心类的日志实现。日志模块使用适配器模式来接入主流的日志框架,使用动态代理模式来对JDBC核心类进行日志功能的增强,让它具备日志打印的能力,本文分析有限,还有一些没有进行分析,但是主体思路是这样,有兴趣的话可以自行研究。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。