一、日志概述
衡量软件产品的质量时,是否具备完善的日志是个非常重要的因素。开发测试阶段,需要日志帮助我们完善功能和发现bug;生产上,当出现生产问题时,又需要日志帮助我们定位问题发生现场的情况。同时,日志还是开发与运维之间的桥梁,有助于运维管理人员快速查找系统的故障和瓶颈,良好的日志在一个软件中占了非常重要的地位。
日志对于排查问题很有帮助,但不是越多越好,过多冗余的日志,不管是日志输出还是保存日志到文件,都会消耗服务器的资源,我们希望某些日志在开发测试阶段打出来,在版本稳定投产之后只打印关键日志,还有想根据日志的不同类型分门别类归纳到日志文件中,日志文件达到一定大小或保存一定时间就删除,这些日志需求的场景都是很常见的,在 Java 的世界中已经有多种成熟开源的日志框架,常用的有 Log4j
、Log4j2
、Apache Commons Log
、java.util.logging
、slf4j
等,它们的框架 API 略有差异,不过使用上整体大同小异。
MyBatis 聚合了上述多种优秀的日志框架,提供框架内部输出详细日志的能力,并且为了屏蔽不同日志框架 API 的差异,提供了一个统一的接口,并且基于该接口定制了针对不同日志框架的适配器,使得用户可以根据自身喜好和使用习惯方便地选择要使用的日志框架。
MyBatis 的日志模块位于 org.apache.ibatis.logging
包中,如下所示:
二、整体设计
日志模块的整体设计如下:
日志模块使用了适配器模式,对外提供的统一日志接口是
Log
,每种日志框架都有对应的适配器来适配接口的定义,LogFactory
是日志工厂类,MyBatis 中最重要的日志就是执行 SQL 的时候打印调试日志,为了适应格式化日志的要求,定义了 BaseJdbcLogger
及其几个子类,通过动态代理的方式,在执行 JDBC 相关方法时拦截,并输出响应的日志,常见的有 SQL 语句、传入参数、执行影响行数等。
三、Log
不同的日志框架对日志级别的定义略有差异,比如 Log4j
的日志级别有:trace、debug、info、warn、error、fatal;java.util.logging
的日志级别有:ALL、FINEST、FINER、FINE、CONFIG、INFO、WARNING、SEVERE、OFF。MyBatis 统一提供了 trace、debug、warn、error 四个级别,这基本与主流日志框架的日志级别类似,可以满足绝大多数场景的日志需求。
org.apache.ibatis.logging.Log
定义了日志模块的功能,所有的日志适配器都要实现该接口,接口定义如下:
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);
}
下面以 Log4jImpl
和 Jdk14LoggingImpl
为例说明日志框架适配器的实现,其他适配器类似。
1、Log4jImpl
Log4jImpl
是 Log4j 日志框架对接口的适配器,其源码如下:
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);
}
}
这里有一个特别的地方,FQCN
参数,该参数传入的 #log()
方法的第一个参数传入,在 Log4j 中参数名叫 callFQCN
,log4j 把传递进来的 callerFQCN 在调用堆栈中一一比较,相等后,再往上一层即认为是用户的调用类。
2、Jdk14LoggingImpl
Jdk14LoggingImpl
是 JDK 内置日志框架对接口的适配器,其源码如下:
public class Jdk14LoggingImpl implements Log {
private Logger log;
public Jdk14LoggingImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isLoggable(Level.FINE);
}
@Override
public boolean isTraceEnabled() {
return log.isLoggable(Level.FINER);
}
@Override
public void error(String s, Throwable e) {
log.log(Level.SEVERE, s, e);
}
@Override
public void error(String s) {
log.log(Level.SEVERE, s);
}
@Override
public void debug(String s) {
log.log(Level.FINE, s);
}
@Override
public void trace(String s) {
log.log(Level.FINER, s);
}
@Override
public void warn(String s) {
log.log(Level.WARNING, s);
}
}
四、LogFactory
public static final String MARKER = "MYBATIS";
// 记录当前使用的第三方日志组件所对应的适配器的构造方法
private static Constructor extends Log> logConstructor;
// 针对每种日志组件调用 tryImplementation() 方法进行尝试加载,具体调用顺序是:
// useSlf4jLogging() --> useCommonsLogging() --> useLog4J2Logging() -->
// useLog4JLogging() --> useJdkLogging() --> useNoLogging()
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
}
LogFactory
中定义了 logConstructor
成员来保存当前使用的第三方日志所对应的适配器的构造方法,第一次加载工厂类时,会调用静态代码块初始化尝试加载各种日志组件,优先级从代码中可以看出,最先加载的适配器会赋值给 logConstructor
,代码如下:
private static void tryImplementation(Runnable runnable) {
// 先判断是否已加载到适配器,是则放弃加载其他适配器
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
虽然将方法调用包装成了一个 runnable,但这里注意不是用 new Thread(runnable).start()
去启动一个新线程,只是简单的调用 runnable.run()
所以对各个适配器的加载是串行的,优先级靠前的适配器加载不到才会真正尝试去加载后面的适配器,各个加载适配器的方法里,都会去调用 #setImplementation()
完成加载动作,传参是适配器对应的 Class 对象,以 Log4j 为例:
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
private static void setImplementation(Class extends Log> implClass) {
try {
// 获取构造参数为 String 的构造器
Constructor extends Log> candidate = implClass.getConstructor(String.class);
// 用构造器实例化 Log 对象
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);
}
}
这里的 #useLog4JLogging()
方法定义用了 synchronized
关键字,这里有点疑惑,如果是加载各个适配器都启动一个新线程还可以理解,但问题是,并没有启新线程,所以我猜测这里跟初始化加载无关,跟调用线程有关,因为 logConstructor
必须是线程安全的,因为暴露给外界的 #getLog
方法用了该变量去创建具体的 Log 对象,如下:
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);
}
}
五、JDBC 调试
业务代码在打印日志时,往往是先创建一个 Logger
,然后在一些关键的地方,比如接口调用、错误异常处理等地方打上日志,但有些时候,出于保持代码清晰或其他原因,在某些地方不想掺杂日志打印的代码,这时候就可以使用动态代理,动态代理常用的有 JDK 动态代理、CGLIB 等,MyBatis 打印 SQL 执行日志方便 JDBC 调试的功能,就是基于 JDK 动态代理实现的。
动态代理,一般无非是在某个方法调用的前后添加某些处理,对于使用者来说,只要拿到代理对象,就可以将它当成普通对象来使用,代理的逻辑对使用者来说是透明。
1、JDK 动态代理的使用
使用 JDK 动态代理,要满足几个条件:
- (1)定义一个接口。
- (2)被代理的类要实现(1)中接口。
- (3)定义一个实现了
InvocationHandler
接口的类,该类中会封装被代理的对象,并且该类实例会被传递给Proxy.newProxyInstance
方法创建代理对象。
下面举例说明:
1.1 定义接口
public interface HelloWorld {
public void sayHelloWorld();
}
1.2 代理类
public class HelloWorldImpl implements HelloWorld {
@Override
public void sayHelloWorld() {
System.out.println("Hello World");
}
}
1.3 InvocationHandler 实现类
/**
* 使用JDK实现动态代理,要求被代理的对象必须实现一个接口
*/
public class JdkProxyExample implements InvocationHandler {
// 真实对象
private Object target = null;
// 绑定真实对象并返回代理对象
public Object bind(Object target) {
this.target = target;
// 第三个参数类型是InvocationHandler,代理对象被调用时,会先执行Handler接口的invoke方法
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
/**
* 代理方法逻辑
* @param proxy 代理对象
* @param method 当前调度方法
* @param args 当前方法参数
* @return 代理结果返回
* @throws Throwable 异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("进入代理逻辑方法");
System.out.println("在调度真实对象之前的服务");
Object obj = method.invoke(target, args);
System.out.println("在调度真实对象之后的服务");
return obj;
}
public static void main(String[] args) {
JdkProxyExample jdk = new JdkProxyExample();
HelloWorld proxy = (HelloWorld) jdk.bind(new HelloWorldImpl());
proxy.sayHelloWorld();
}
}
执行结果如下:
进入代理逻辑方法
在调度真实对象之前的服务
Hello World
在调度真实对象之后的服务
2、BaseJdbcLogger 及其子类
BaseJdbcLogger
及其子类通过使用动态代理,实现了执行 SQL 操作时,无感输出 JDBC 调试日志的效果。
2.1 BaseJdbcLogger
2.1.1 数据结构
protected static final Set SET_METHODS = new HashSet(); // 记录了PreparedStatement接口中定义的常用的set*()方法
protected static final Set EXECUTE_METHODS = new HashSet(); // 记录了Statement接口和PreparedStatement接口中与执行SQL语句相关的方法
private Map
-
SET_METHODS
:记录了PreparedStatement接口中定义的常用的set*()方法。 -
EXECUTE_METHODS
:记录了Statement接口和PreparedStatement接口中与执行SQL语句相关的方法。 -
columnMap
:记录了PreparedStatement.set*()方法设置的键值对。 -
columnNames
:记录了PreparedStatement.set*()方法设置的key值。 -
columnValues
:记录了PreparedStatement.set*()方法设置的value值。 -
statementLog
:用于输出日志的Log对象。 -
queryStack
: 记录了SQL的层数,用于格式化输出SQL。
在类加载时,会对 SET_METHODS
、EXECUTE_METHODS
进行初始化,源码如下:
static {
SET_METHODS.add("setString");
SET_METHODS.add("setNString");
SET_METHODS.add("setInt");
SET_METHODS.add("setByte");
SET_METHODS.add("setShort");
SET_METHODS.add("setLong");
SET_METHODS.add("setDouble");
SET_METHODS.add("setFloat");
SET_METHODS.add("setTimestamp");
SET_METHODS.add("setDate");
SET_METHODS.add("setTime");
SET_METHODS.add("setArray");
SET_METHODS.add("setBigDecimal");
SET_METHODS.add("setAsciiStream");
SET_METHODS.add("setBinaryStream");
SET_METHODS.add("setBlob");
SET_METHODS.add("setBoolean");
SET_METHODS.add("setBytes");
SET_METHODS.add("setCharacterStream");
SET_METHODS.add("setNCharacterStream");
SET_METHODS.add("setClob");
SET_METHODS.add("setNClob");
SET_METHODS.add("setObject");
SET_METHODS.add("setNull");
EXECUTE_METHODS.add("execute");
EXECUTE_METHODS.add("executeUpdate");
EXECUTE_METHODS.add("executeQuery");
EXECUTE_METHODS.add("addBatch");
}
2.1.2 构造函数
public BaseJdbcLogger(Log log, int queryStack) {
this.statementLog = log;
if (queryStack == 0) {
this.queryStack = 1;
} else {
this.queryStack = queryStack;
}
}
初始化 Log 对象,指定 SQL 层次。
2.1.3 方法功能
BaseJdbcLogger
里大部分方法比较简单,要特别关注的是 #getParameterValueString()
和 #prefix()
方法。
String getParameterValueString()
【功能】获取参数值字符串,其形式为 value1(type1), value2(type2), ..., null, ...
,用于输出实际执行 SQL 时绑定参数的信息。
【源码与注解】
protected String getParameterValueString() {
List typeLists = new ArrayList(columnValues.size());
// 遍历所有绑定传入的参数值,若为空则字符串为 "null",否则为 "value(type)"
for (Object value : columnValues) {
if (value == null) {
typeLists.add("null");
} else {
typeLists.add(value + "(" + value.getClass().getSimpleName() + ")");
}
}
// eg: [null, 10(Integer), test(String)]
// 将 List 中的值转化为字符串
final String parameters = typeLists.toString();
// 去掉前后中括号
return parameters.substring(1, parameters.length() - 1);
}
String prefix(boolean isInput)
【功能】根据判断输入还是输出阶段输出的日志,决定日志前缀是形如 "==> " 还是 "<== "。
【源码与注解】
protected void debug(String text, boolean input) {
if (statementLog.isDebugEnabled()) {
statementLog.debug(prefix(input) + text);
}
}
protected void trace(String text, boolean input) {
if (statementLog.isTraceEnabled()) {
statementLog.trace(prefix(input) + text);
}
}
private String prefix(boolean isInput) {
char[] buffer = new char[queryStack * 2 + 2];
Arrays.fill(buffer, '=');
buffer[queryStack * 2 + 1] = ' '; // 前缀最后一个字符为空格,与实际的日志内容隔开
if (isInput) {
buffer[queryStack * 2] = '>'; // 根据输入还是输出决定前缀的第一个字符为 ‘<’ 还是倒数第二个字符为 ‘>’
} else {
buffer[0] = '<';
}
return new String(buffer);
}
String removeBreakingWhitespace(String original)
【功能】将多行字符串整合成一行,并且取出多余的空格,主要是将 SQL 格式化,方便日志输出。
【源码与注解】
protected String removeBreakingWhitespace(String original) {
// 根据字符串中的 \t\n\r\f 解析处理,分成多段
StringTokenizer whitespaceStripper = new StringTokenizer(original);
StringBuilder builder = new StringBuilder();
// 将每一段拼接起来,并用空格分隔
while (whitespaceStripper.hasMoreTokens()) {
builder.append(whitespaceStripper.nextToken());
builder.append(" ");
}
return builder.toString();
}
【测试案例】
public class Test extends BaseJdbcLogger {
public Test(Log log, int queryStack) {
super(log, queryStack);
}
public static void main(String[] args) {
String sql = "select id, role_name as roleName, note " +
"from t_role\r\n" +
"where role_name like concat('%', #{roleName}, '%')";
Test test = new Test(null, 0);
System.out.println(test.removeBreakingWhitespace(sql));
}
}
执行结果如下:
2.2 ConnectionLogger
【功能】ConnectionLogger
继承了 BaseJdbcLogger 并实现了 InvocationHandler 接口,封装了真正的 Connection 对象,并为其生成代理对象。
【构造方法和成员】
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
private Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
// other code
}
【生成代理对象方法】
// 创建一个内嵌Log对象的代理Connection对象
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);
}
【代理逻辑】
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
// 如果调用的是从Object继承的方法,则直接调用,不做任何其他处理
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// 如果调用的是preparedStatement()方法、prepareCall()方法或createStatement()方法,
// 则在创建相应的statement对象后,为其创建代理对象并返回该代理对象
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
// 调用底层封装的Connection对象的prepareStatement()方法,得到PreparedStatement对象
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
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 {
// 其他方法则直接调用底层Connection对象的相应方法
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 返回真实的对象
public Connection getConnection() {
return connection;
}
}
【解析】
- (1)
ConnectionLogger
封装了 Connection 对象,在构造函数中,调用父类构造器用传入的Log 对象和 SQL 层次进行初始化。 - (2)
#newInstance()
方法负责暴露给调用者生成代理对象。 - (3)当执行生成的代理 Connection 对象的方法时,会调用
#invoke()
方法,在该方法中,对执行的方法进行拦截,若是 Object 类中声明的方法,直接反射调用,不做特殊处理,如果是 prepareStatement、prepareCall、createStatement 方法,为方法调用返回的 PreparedStatement、Statement 对象生成响应的代理对象再返回,因为 prepareStatement、prepareStatement 方法的入参是 SQL,所以这里打印 SQL 的日志,开发者测试时就可以从日志中看到实际执行的 SQL 是什么,特别是使用动态 SQL 时,很难看出实际执行的 SQL 是什么。
【测试案例】
调用代码(部分)
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtil.openSqlSession("mybatis-config-properties.xml");
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role = roleMapper.getRole(1L);
System.out.println("Role = " + role);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
mapper 配置(部分)
在 ConnectionLogger
里的 #invoke()
方法里打上断点,运行观察
可以看到,执行 SQL 时会调用
Connection.prepareStatement()
创建 PreparedStatement 对象,并打印一行日志输出取出多余换行空格后的 SQL。
2.3 PreparedStatementLogger
【功能】PreparedStatementLogger
继承了 BaseJdbcLogger 并实现了 InvocationHandler 接口,封装了真正的 PreparedStatement 对象,并提供了 #newInstance()
方法生成代理对象,会被 ConnectionLogger 所调用。
【代理逻辑】
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// (1) 如果调用 PreparedStatement 的执行方法
if (EXECUTE_METHODS.contains(method.getName())) {
// (1.1) 输出绑定参数信息
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo();
// (1.2 )执行查询SQL,返回值是一个ResultSet对象,封装生成代理对象ResultSetLogger返回
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
// (1.3) 执行其他方法不用封装代理对象返回
return method.invoke(statement, params);
}
// (2) 如果执行的是设置参数的方法,将传入参数保存到父类成员中,方便执行时输出参数日志
} else if (SET_METHODS.contains(method.getName())) {
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
// (3) getResultSet() 一般跟 execute() 方法配合使用,用来获取SQL执行的结果集
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
// (4) getUpdateCount() 一般跟 execute() 方法配合使用,用来获取SQL执行的更新计数
} else if ("getUpdateCount".equals(method.getName())) {
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
【解析】
(1) 如果调用 PreparedStatement 的执行方法
在 BaseJdbcLogger
中,存储了四个执行方法的名称,分别为:execute
、executeUpdate
、executeQuery
、addBatch
。
-
execute
是一个通用的执行 SQL 的方法,返回值为布尔值,如果为 true,表示返回一个结果集(常见于查询语句),如果为 false,则返回表示没有结果或者是更新计数; -
executeUpdate
可以执行 INSERT、UPDATE 或 DELETE 语句,返回值是一个整数表示影响行数; -
executeQuery
通常用来执行 SELECT 语句,返回值是一个结果集; -
addBatch
是往 Statement 对象中添加要批量执行的 SQL;
(1.1)在执行上述这些方法时,通常都是会绑定参数的,所以如果检查到执行上述的方法,会输出绑定参数的日志。
(1.2)如果执行 executeQuery
肯定返回一个 ResultSet,封装成代理对象返回。
(1.3)其他执行方法直接反射调用。
(2)如果执行的是设置参数的方法,将传入参数保存到父类成员中,方便执行时输出参数日志。
(3)getResultSet() 一般跟 execute() 方法配合使用,用来获取SQL执行的结果集,这里生成结果集的代理对象返回。
(4)getUpdateCount() 一般跟 execute() 方法配合使用,用来获取SQL执行的更新计数。
【测试案例】
还是上面的代码,在 PreparedStatementLogger
中打上断点,执行情况如下图所示:
可以看到执行的方法是
execute()
,并输出了 SQL 绑定参数的日志。
2.4 ResultLogger
【功能】ResultLogger
继承了 BaseJdbcLogger 并实现了 InvocationHandler 接口,封装了真正的 ResultSet 对象,并提供了 #newInstance()
方法生成代理对象,会被 PreparedStatementLogger 和 StatementLogger 所调用。
【数据结构与构造】
private static Set BLOB_TYPES = new HashSet(); // 不输出字段值类型,主要是超大数据的类型
private boolean first = true; // 控制输出结果集字段名的日志时,只在第一次打印日志
private int rows = 0; // 统计结果集中有多少行
private ResultSet rs; // 封装的真实ResultSet对象
private Set 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;
}
ResultLogger
具备打印结果集中每行数据信息日志的功能,需要将日志级别设定为 TRACE
,需要在 log4j.properties 文件中添加以下配置:
log4j.logger.ssm=TRACE
其中,后缀 ssm 是映射配置文件中
ResultLogger
限制了一些可能携带大量数据的字段的数据的输出,BLOB_TYPES
就存储了受限制的类型,并在加载类时静态初始化,其他字段的含义看注释即可。
【代理逻辑】
@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);
// 调用 ResultSet.next(),遍历结果及中的多行结果
if ("next".equals(method.getName())) {
if (((Boolean) o)) {
rows++; // 统计行数加一
// 需要开启TRACE的日志级别才能输出每一行的字段信息和对应的值信息
if (isTraceEnabled()) {
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);
}
}
当 ResultSet.next()
被调用时,类似迭代器会遍历结果集中的多行结果,假如返回 false,表示没有行了,则打印统计行数的日志;假如返回 true,则获取行信息,打印其字段信息和字段值,对应 #printColumnHeaders()
和 #printColumnValues()
方法,还是上面的测试案例,加上 TRACE 的配置之后,输出的日志如下:
2.5 StatementLogger
跟 PreparementLogger
类似,不赘述。