Mybatis日志打印原理
1. Mybatis日志开启和源码分析
日志开启
一般常见的都是这种开启方式,这种日志打印是最简单的,采用的标准输出和错误输出的方式,如果采用的别的日志框架的话,name和value和这里不一样。这里就先作为开始。在下面会有介绍的
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
源码分析
Mybatis一切的开始都要从XML文件解析开始。
只要debug从创建SqlSessionFactory
开始,就能看到解析操作
sqlMapper = new SqlSessionFactoryBuilder().build(reader);
//debug进去
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties")); //解析properties标签,并把他放在 parser和config的 Variables 里面
//解析settings标签。并且将里面的属性和值,变为Properties,并且验证是否有属性。
Properties settings = settingsAsProperties(root.evalNode("settings"));//加载setting标签
loadCustomVfs(settings); //lcnote 这里的vfs是啥?怎么用 我知道这个
//我现在知道他的vfs是什么了,vfs(virtual file system)他抽象出了几个api,通过这些api就可以访问文件系统上的资源;比如在
// 在解析 mapperElement(root.evalNode("mappers"));的时候,如果指定package,就可以通过VFS来获取包路径下面所有的class文件。
// 并且会将他添加到mappe里面,和spring中的classPathSacnner一样差不多,可以指定过滤器。
//自定义LogImpl
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
//从这里开始,都是解析具体的标签,new出对象,将标签下面的属性设置进去,
// 从解析的这里基本也能看出mybatis里面重要的几个点,首先是objectFactory,objectFactory。objectFactory,plugins
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectFactory"));
reflectorFactoryElement(root.evalNode("objectFactory"));
//这里就具体设置setting标签了
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//lcnote 这里是重点,解析mapper文件,
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
loadCustomLogImpl
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
//resolveClass方法会通过调用typeAliasRegistry的resolveAlias来判断
//TypeAliasRegistry是类型别名注册器
//初始化的时候默认注入了好多类型,具体的可以看TypeAliasRegistry和Configuration的构造方法。
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
在解析到对应的实体类之后,就会通过LogFactory
创建出实例。并且将class对象赋值给Configuration的logImpl属性
LogFactory.useCustomLogging
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
LogFactory.useCustomLogging(this.logImpl);
}
}
// useCustomLogging里面调用的方法
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.");
}
//注意,这里将找到的Constructor赋值给了LogFactory的logConstructor属性
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
还有一个小tips,Mybatis里面还支持log的前缀
configuration.setLogPrefix(props.getProperty("logPrefix")); //这个是比较意思的,居然是可以设置log的前缀,很奇妙
// 可以在setting里面设置。
// 对应的属性是configuration的logPrefix的属性
在这里设置好日志的实现类,并且也赋值给configuration了。那他是怎么使用的呢?在上面的代码中从LogFactory new出实例之后,也没有赋值,就答应了一下日志,然后就结束了,只有在LogFactory中保留了构造方法,用于创建实例。别的都没有了,可是在Mybatis中日志都长这个样子的。
那么,下面就看看他是如果做这样的事情的?
抛开Mybatis实现不谈,要实现这样的功能,第一时间想到的就是切面。然后就想到了代理的创建方式。在Mybatis里面肯定用的不是Spring的切面。他用的JDK的动态代理。
想想,如果生成了代理对象给Statement,和Result的Next方法。那么在实际执行方法之前就可以将日志打印出来。事实Mybatis的确是这样做的。
在说一个预备知识,平常在使用日志的时候,每个类都有一个Logger对象。注意,Logger对象是每一个类都有的。
那么下面就具体看看。
// SimpleExecutor##doQuery方法。
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 重点就会这个。在这里面创建Connection对象。
//会将ms中的StatementLog传递进去。
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);//查询并且映射结果集
} finally {
closeStatement(stmt);
}
}
MappedStatement
这个就是一个Mapper中一个方法对应的实体类。这个对象是在解析XMl的时候创建的。StatementLog的属性也肯定是在这个时候设置进去的。
顺着prepareStatement
方法点进去,就会看到创建Connection对象的方法。写过JDBC的都知道,这个对象是查询的开始。
给Connection创建代理对象
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
//代码很简单。如果为true就会创建一个Connection的代理对象。
// StdOutImpl的debugEnabled默认是true。
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
ConnectionLogger.newInstance
/**
* Creates a logging version of a connection.
*
* @param conn
* the original connection
* @param statementLog
* the statement log
* @param queryStack
* the query stack
* @return the connection with logging
*/
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
//很明确了,这里是通过JDK的代理来实现日志的,既然是JDK,那么肯定是要看InvocationHandler了。在这里就是ConnectionLogger
ConnectionLogger类详情
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// 如果方法名字是prepareStatement或者prepareCall,打印日志。
// 具体的打印就很简单了,
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
// 这里的debug方法会调用到BaseJdbcLogger里面的statementLog属性,这属性就是在MapperStatement里面的。
debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
}
//当利用Connection调用prepareStatement的时候,就会生成一个PreparedStatement。
// 这里的PreparedStatement也是BaseJdbcLogger的实现类。
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
首先可以看到他是继承与BaseJdbcLogger,BaseJdbcLogger的实现类表示,能在那些地方打印日志。
在BaseJdbcLogger的类里面有两个集合,一个是SET_METHODS(保存着PreparedStatement里面set方法,并且参数大于1的方法)
EXECUTE_METHODS保存着名字为execute,executeUpdate,executeQuery,addBatch
的方法
那么在看看ConnectionLogger
invoke方法干了什么事情。
在上面的代码里面已经写了注释了。
给Statement创建代理对象
可以看到,他也是继承于BaseJdbcLogger
并且也是一个InvocationHandler
,那肯定看invoke
方法,
public final class StatementLogger extends BaseJdbcLogger implements InvocationHandler {
private final Statement statement;
private StatementLogger(Statement stmt, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.statement = stmt;
}
/**
1. 这里的方法很明显,
2. 还是通过方法名字来判断,如果方法名字是executeQuery,在调用方法之后,生成ResultSet之后,还会使用ResultSetLogger包装
3. 如果方法名字是ResultSet,再到调用方法之后,对生成的ResultSet包装
**/
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug(" Executing: " + removeExtraWhitespace((String) params[0]), true);
}
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
return method.invoke(statement, params);
}
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
这里创建对象的方法代理对象的方法都是一致的。步骤都是一致的。
给Result创建代理对象
也是继承BaseJdbcLogger
并且实现了InvocationHandler
,同样看invoke
方法。
/**
1. 注意这里的static里面的东西
2. BLOB_TYPES包含了几种类型,这里面包含的是Blob类型的,遇到这种类型是不打印日志了,不能打印就用<<BLOB>>代替
3. 如果能打印就直接打印。
**/
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
private static final Set<Integer> BLOB_TYPES = new HashSet<>();
private boolean first = true;
private int rows;
private final ResultSet rs;
private final Set<Integer> blobColumns = new HashSet<>();
static {
BLOB_TYPES.add(Types.BINARY);
BLOB_TYPES.add(Types.BLOB);
BLOB_TYPES.add(Types.CLOB);
BLOB_TYPES.add(Types.LONGNVARCHAR);
BLOB_TYPES.add(Types.LONGVARBINARY);
BLOB_TYPES.add(Types.LONGVARCHAR);
BLOB_TYPES.add(Types.NCLOB);
BLOB_TYPES.add(Types.VARBINARY);
}
private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.rs = rs;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
Object o = method.invoke(rs, params);
if ("next".equals(method.getName())) {
if ((Boolean) o) {
rows++;
if (isTraceEnabled()) {
// 拿到ResultSet打印结果
ResultSetMetaData rsmd = rs.getMetaData();
final int columnCount = rsmd.getColumnCount();
if (first) {
first = false;
//打印头信息
printColumnHeaders(rsmd, columnCount);
}
// 打印具体的值
printColumnValues(columnCount);
}
} else {
//总共
debug(" Total: " + rows, false);
}
}
clearColumnInfo();
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
StringJoiner row = new StringJoiner(", ", " Columns: ", "");
for (int i = 1; i <= columnCount; i++) {
//如果类型是之前规定的BLOB类型,就添加到blobColumns里面,注意,这添加的可是列的id
if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
blobColumns.add(i);
}
row.add(rsmd.getColumnLabel(i));
}
trace(row.toString(), false);
}
//这里就也一样,判断一下是不是blobColumns里面包含的,用<<BLOB>>来代替
private void printColumnValues(int columnCount) {
StringJoiner row = new StringJoiner(", ", " Row: ", "");
for (int i = 1; i <= columnCount; i++) {
try {
if (blobColumns.contains(i)) {
row.add("<<BLOB>>");
} else {
row.add(rs.getString(i));
}
} catch (SQLException e) {
// generally can't call getString() on a BLOB column
row.add("<<Cannot Display>>");
}
}
trace(row.toString(), false);
}
}
到这里就分析完了,Mybatis里面日志是怎么打印的,并且是怎么起作用的。
下面就整体的看看Mybatis是如果将大多数的日志整合在一起的,并且它还支持自定义的日志实现类。
3. Mybatis日志分析接口和实现类分析(源码分析)
MapperStatement是怎么创建日志的,在哪里创建的?
- 在解析Xml文件中创建MapperStatement的时候创建的
//MapperStatement内部类Builder方法
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.resultSetType = ResultSetType.DEFAULT;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
mappedStatement.resultMaps = new ArrayList<>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
//重点是这里
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
在上面分析过,在解析Setting
标签的时候,会在LogFactory
保留所选构造日志的构造函数,之后凡是创建日志,就可以通过这个构造函数来创建Log对象。
LogFactory是怎么创建Log对象的
//LogFactory的getLog方法,通过之前保留的构造函数来创建对象
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);
}
}
Mybatis日志Log接口和实现类(Log是怎么将常见的日志都融合进的)
Log接口
定义Log接口,并且里面包含了一些打日志常见的方法
/**
* @author Clinton Begin
*/
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);
}
Log实现类
自定了一个Log接口,然后再不同的实现类里面采用不同的日志框架。主要是在构造方法里面先创建出具体的Logger对象,将这个对象用自定义的实现了Logger接口包装,这样就实现了统一。适配器模式
在xml中的Setting
中怎么配置能使用到上面的实现类(怎么用别的日志框架)
在Configuration
构造方法里面通过typeAliasRegistry
来注册别名和类型的映射关系
看两个个具体的Log接口实现的例子
这两具体的例子都很简单。
StdOutImpl
这个实现就很简单了,都是调用System正常和错误的输出来打印的。
/*
* Copyright 2009-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.logging.stdout;
import org.apache.ibatis.logging.Log;
/**
* @author Clinton Begin
*/
public class StdOutImpl implements Log {
public StdOutImpl(String clazz) {
// Do Nothing
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
@Override
public void debug(String s) {
System.out.println(s);
}
@Override
public void trace(String s) {
System.out.println(s);
}
@Override
public void warn(String s) {
System.out.println(s);
}
}
Log4jImpl
/*
* Copyright 2009-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.logging.log4j;
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/**
* @author Eduardo Macarron
*/
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
// 这个Logger是log4j的,适配器模式,真正打日志的还是Log4j
private final Logger log;
public Log4jImpl(String clazz) {
// 传递进来的class就是MapperStatement的id
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);
}
}
2. 自定义日志
1. 实现Log接口
package org.apache.ibatis.session.mybatis;
import org.apache.ibatis.logging.Log;
public class MyLog implements Log {
public MyLog(String id){
System.out.println("MyLog:"+id);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
System.out.println("MyLog:"+s);
e.printStackTrace();
}
@Override
public void error(String s) {
System.out.println("MyLog:"+s);
}
@Override
public void debug(String s) {
System.out.println("MyLog:"+s);
}
@Override
public void trace(String s) {
System.out.println("MyLog:"+s);
}
@Override
public void warn(String s) {
System.out.println("MyLog:"+s);
}
}
2. 配置文件配置
注意自定义的value必须是全限定类名。不能配置别名。因为上面章节的代码里面,解析Setting
在前面,解析typeAliases
在后面,故不能通过别名来配置。
3. 结果展示
可以看到日志都以MyLog开头了。
Mybatis日志打印就分析到这里了。如有不正确的地方,欢迎指出。谢谢。