在日志模块的上篇中,我们详细拆解了 MyBatis 是如何整合第三方日志框架,实现了完善的日志功能的。那么在本节中,我们再来具体分析下:为了实现“将日志功能优雅地嵌入到核心流程中,实现无侵入式地日志打印”这一目标,MyBatis 内部做了怎样的设计。
日志打印功能点
为了便于分析,我们先来回顾一下原生 JDBC 的执行流程。直接上代码:
/**
* @author ZhangShenao
* @date 2023/5/29 2:07 PM
* Description 原生JDBC的使用方式
*/
public class JdbcDemo {
public static void main(String[] args) throws Exception {
//1. 注册数据库驱动/创建数据源DataSource
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 创建数据库连接Connection
Connection conn = DriverManager.getConnection("xxx");
//3. 创建执行语句Statement
String sql = " select * from `user` ";
PreparedStatement stmt = conn.prepareStatement(sql);
//4. 执行SQL语句,获取结果集ResultSet
ResultSet resultSet = stmt.executeQuery();
//5. 解析ResultSet,获取业务对象
List<User> users = new ArrayList<>();
while (resultSet.next()) {
long id = resultSet.getLong("id");
String groupName = resultSet.getString("name");
users.add(new User(id, groupName));
}
System.out.println("Users: " + users);
//6. 释放资源对象
resultSet.close();
stmt.close();
conn.close();
}
@Data
@AllArgsConstructor
@ToString
private static class User {
private long id;
private String name;
}
}
可以看到,一次典型的 JDBC 操作,会经历如下几个核心流程:
- 注册数据库驱动/创建数据源
DataSource
; - 创建数据库连接
Connection
; - 创建预编译执行语句
PreparedStatement
; - 执行 SQL 语句,获取结果集
ResultSet
; - 解析
ResultSet
,获取业务对象; - 释放资源。
在上述步骤中,可以认为最核心的需要打印日志的功能点为:
1. 创建 PrepareStatement
时:打印待执行的 SQL 语句;
2. 访问数据库时:打印实际参数的类型和值;
3. 查询出结果集后:打印结果行数及结果值。
Proxy Pattern 代理模式
需要打印日志的功能点已经明确了,接下来就是分析下怎么实现。总不能在每处直接logger.info()
吧?
在业务执行的主流程外,需要额外织入一些通用的增强逻辑,以实现对现有功能的扩展。这是典型的 Proxy Pattern 代理模式的适用场景。
按照惯例,我们来回顾一下代理模式的 UML 结构图:
(图片来源:https://refactoring.guru/design-patterns/proxy)
对照代理模式,我们可以理所当然地想到:可以通过创建一个动态代理类,在 MyBatis 的核心执行流程之外,额外增加日志打印的功能。那么 MyBatis 具体是如何实现的呢?
MyBatis 日志增强器
我们来看下 MyBatis 日志增强器的类结构图:
**看到 InvocationHandler,大家肯定第一时间就能想到动态代理!**没错,这些日志增强器都是通过 JDK 原生动态代理的方式创建的代理类。下面具体介绍下每个类的功能:
BaseJdbcLogger
BaseJdbcLogger
是所有日志增强器的抽象父类,它用于记录 JDBC 那些需要增强的方法,并保存运行期间的 SQL 参数信息:
/**
* 所有日志增强器的抽象父类,用于记录JDBC那些需要增强的方法,并保存运行期间的SQL参数信息
*/
public abstract class BaseJdbcLogger {
//记录需要被增强的方法
protected static final Set<String> SET_METHODS;
protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
//记录运行期间的SQL参数相关信息
private final Map<Object, Object> columnMap = new HashMap<>();
private final List<Object> columnNames = new ArrayList<>();
private final List<Object> columnValues = new ArrayList<>();
//...省略非必要代码
//在初始化时,记录所有需要被日志增强的JDBC方法
static {
//记录PreparedStatement中的setXXX()方法
SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
.filter(method -> method.getName().startsWith("set")).filter(method -> method.getParameterCount() > 1)
.map(Method::getName).collect(Collectors.toSet());
//记录executeXXX()方法
EXECUTE_METHODS.add("execute");
EXECUTE_METHODS.add("executeUpdate");
EXECUTE_METHODS.add("executeQuery");
EXECUTE_METHODS.add("addBatch");
}
//...省略非必要代码
//通过Log完成日志打印
protected boolean isDebugEnabled() {
return statementLog.isDebugEnabled();
}
protected boolean isTraceEnabled() {
return statementLog.isTraceEnabled();
}
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);
}
}
//...省略非必要代码
}
ConnectionLogger
ConnectionLogger
:数据库连接的日志增强器,用于打印 PreparedStatement
相关参数,并通过动态代理方式,创建 StatementLogger
和 PreparedStatementLogger
两个日志增强器。
/**
* 数据库连接的日志增强器
*/
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
//底层维护JDCB Connection数据库连接对象
private final Connection connection;
//...省略非必要代码
//方法拦截实现
@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);
}
//针对prepareStatement相关方法,创建PreparedStatementLogger日志增强器
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
}
//针对createStatement相关方法,创建StatementLogger日志增强器
if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
return StatementLogger.newInstance(stmt, statementLog, queryStack);
} else {
//Connection自身的方法,正常执行
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//创建动态代理对象
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);
}
//...省略非必要代码
}
PreparedStatementLogger
PreparedStatementLogger
和 StatementLogger
这两个增强器的功能类似,这里以更常用的 PreparedStatementLogger
为例,其主要功能为:
- 打印 JDBC
PreparedStatement
中的动态参数信息; - 拦截
setXXX()
方法,记录封装的参数; - 创建
ResultSetLogger
日志增强器,使得对于结果集的操作具备日志打印的功能。
/**
* PreparedStatement日志增强器
*/
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
//底层维护JDBC PreparedStatement对象
private final PreparedStatement statement;
//...省略非必要代码
//方法拦截实现
@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);
}
//拦截executeXXX()方法,打印参数信息
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo();
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);
}
}
//拦截setXXX()方法,记录动态参数
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);
} else if ("getResultSet".equals(method.getName())) {
//拦截getResultSet()方法,返回ResultSetLogger增强器
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else if ("getUpdateCount".equals(method.getName())) {
//拦截getUpdateCount()方法,打印update操作影响的记录行数
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
//PreparedStatement中的普通方法,直接调用
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//创建动态代理对象
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
ClassLoader cl = PreparedStatement.class.getClassLoader();
return (PreparedStatement) Proxy.newProxyInstance(cl,
new Class[] { PreparedStatement.class, CallableStatement.class }, handler);
}
//...省略非必要代码
}
ResultSetLogger
最后一个日志增强器是 ResultSetLogger
,它是结果集日志增强器,主要用于打印结果集的总记录数和每条记录的结果。
/**
* 结果集日志增强器
*/
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
//底层维护JDBC ResultSet对象
private final ResultSet rs;
//...省略非必要代码
//方法拦截实现
@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);
}
Object o = method.invoke(rs, params);
if ("next".equals(method.getName())) {
if ((Boolean) o) {
rows++;
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);
}
}
//打印结果列头信息
private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
StringJoiner row = new StringJoiner(", ", " Columns: ", "");
for (int i = 1; i <= columnCount; i++) {
if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
blobColumns.add(i);
}
row.add(rsmd.getColumnLabel(i));
}
trace(row.toString(), false);
}
//打印结果列值
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);
}
//创建代理对象
public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
ClassLoader cl = ResultSet.class.getClassLoader();
return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
}
//...省略非必要代码
}
日志功能优雅嵌入
有了上面介绍的几个日志增强器,打印日志的功能是如何优雅地嵌入到 MyBatis 的核心执行流程中的呢?
在MyBatis 有个关键的组件 Executor
,它是 MyBatis 的核心执行器接口,对于数据库的插入、查询等操作最终都是通过该接口来完成的。后面我们会有专门的篇幅来详细介绍 Executor
的内部实现,这里只需要看一下它创建数据库连接的方法:org.apache.ibatis.executor.BaseExecutor#getConnection()
:
//创建数据库连接
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
//创建ConnectionLogger日志增强器,获取打印日志的功能
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
}
return connection;
}
可以看到,这里创建的实际上是 ConnectionLogger
这个日志增强器。这样一来,通过 BaseExecutor -> ConnectionLogger -> PreparedStatementLogger -> ResultSetLogger
的执行链路,类似多米诺骨牌方式,完成了日常增强器的创建过程。
小结
在日志模块中,我们首先对 MyBatis 的日志功能进行了需求分析,接下来探讨了 MyBatis 对第三方日志框架的整合方式,进而看到了 MyBatis 如何对 JDBC 原生的组件进行日志功能增强,最后了解了把日志功能优雅嵌入到核心执行流程的小技巧。日志这个功能虽然简单,但是 MyBatis 内部的实现用到了很多经典的设计模式,如适配器模式、动态代理模式等等,代码简洁且优雅,非常值得我们学习和借鉴。