Mybatis是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。 Mybatis的源码可以说写的非常漂亮,模块之前划分的很清晰,里面大量采用了设计模式,其中的一些模块完全可以二次封装作为工具类来使用。本文开始每一篇都会对其中一个模块进行解读,分析其设计模式。
日志模块的代码在org.apache.ibatis.logging。Mybatis的日志功能的实现可以说非常优雅,主要有两大优势:
1、Mybatis的本身不提供日志功能,而是对接了常用的第三方日志如slf4j、log4j等。在实际使用中根据优先级依次加载 slf4J → commonsLoging → Log4J2 → Log4J → JdkLog。
2、在传统的jdbc代码中,我们如果想打印日志,需要入侵业务代码。而在Mybatis的实际使用中,我们却感受不到任何日志的存在。下面是一个传统的jdbc查询的核心代码,如果打印日志则要手动添加:
// 注册驱动...
Class.forName("com.mysql.jdbc.Driver");
// 获得一个连接
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 创建一个查询
Statement stmt = conn.createStatement();
String userName = "lwtxzwt";
// 拼接SQL
String sql = "SELECT * FROM t_user where user_name = '" + userName + "'";
ResultSet rs = stmt.executeQuery(sql);
// TODO log
// 从resultSet中获取数据并转化成bean
while (rs.next()) {
System.out.println("------------------------------");
TUser user = new TUser();
user.setId(rs.getInt("id"));
user.setUserName(rs.getString("user_name"));
// TODO log
}
// 关闭连接...
而在我们实际开发或者配置时,感觉不到这两块的存在,Mybatis又是如何优雅的实现呢?
这里首先采用了一个适配器模式,定义了一个通过接口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);
}
第三方日志各自实现这个接口:
class Slf4jLoggerImpl implements Log {
private final Logger log;
public Slf4jLoggerImpl(Logger logger) {
log = logger;
}
@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);
}
}
通过LogFactory里面的静态代码块,传入各个构造方法,通过判断构造方法是否为空,来实现根据优先级加载:
public final class LogFactory {
...
//被选定的第三方日志组件适配器的构造方法
private static Constructor extends Log> logConstructor;
//自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
...
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {//当构造方法不为空才执行方法
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
...
}
Mybatis通过动态代理,给Connection、Statement(PreparedStatement)、ResultSet附加了日志功能,其对应的实现是xxxLogger类。
ConnectionLogger的增强:
@Override
//对连接的增强
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
//如果是从Obeject继承的方法直接忽略
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句
//并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
}
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);//打印sql语句
}
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);
}
}
如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句,并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数。
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);
}
StatementLogger也类似,当调用executeQuery时,打印日志,并返回resultSet的代理对象。ResultSetLogger也同理。
因此当我们通过Mybatis获取Connection时(Mybatis对外不会有Connection概念,主要是通过SqlSession封装。),获取到的是代理对象,所以后续操作能够自动的打印出日志。