myBatis源码解析-日志篇(1)
上半年在进行知识储备,下半年争取写一点好的博客来记录自己源码之路。在学习源码的路上也掌握了一些设计模式,可所谓一举两得。本次打算写Mybatis的源码解读。
准备工作
1. 下载mybatis源码
下载地址:https://github.com/mybatis/mybatis-3
2. 下载mybatis-parent源码
下载地址:https://github.com/mybatis/parent
3. 编译
进入mybatis-paren所在文件夹
mvn clean install
进入mybatis所在文件夹
mvn clean mvn install -Dmaven.test.skip=true
4. 用IDEA或Eclipse打开mybatis即可
源码分析-日志模块
1. 日志基础包
package org.apache.ibatis.logging; // mybatis自定义接口,提供四种级别 error->debug->trace->warn 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); }
如上,mybatis提供了日志的四种级别,error->debug->trace->warn
2. 从源码可以看到,mybatis提供了如jdk14,log4j,log4j2等日志实现,分析常用的如log4j2源码
// log4j的适配器 public class Log4j2Impl implements Log { // 真正提供日志能力的log4j的日志类 private Log log; // 构造方法,导入真正的实现类 public Log4j2Impl(String clazz) { Logger logger = LogManager.getLogger(clazz); if (logger instanceof AbstractLogger) { log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger); } else { log = new Log4j2LoggerImpl(logger); } } public boolean isDebugEnabled() { return log.isDebugEnabled(); } public boolean isTraceEnabled() { return log.isTraceEnabled(); } // 调用真正的实现方法 public void error(String s, Throwable e) { log.error(s, e); } // 调用真正的实现方法 public void error(String s) { log.error(s); } // 调用真正的实现方法 public void debug(String s) { log.debug(s); } // 调用真正的实现方法 public void trace(String s) { log.trace(s); } // 调用真正的实现方法 public void warn(String s) { log.warn(s); } }
综上,可以看到,mybatis并没有提供真正的日志实现接口,只是定义了一套自己的日志接口,其实现交给真正的具体日志类(如log4j,log4j2)。此处用到了适配器模式。比如log4j2,mybatis提供自定义接口,提供log4j2适配器继承自定义接口,在log4j2适配器里调用log4j2的真实方法来实现自己的接口。
3.各日志默认调用流程
看源码包中有很多日志实现,那具体的默认调用流程是怎样。查看org.apache.ibatis.logging包中的LogFactory类。看名字就知道是用到了工厂模式。
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers
*/
public static final String MARKER = "MYBATIS";
// 被选定的第三方日志组件适配器的构造方法
private static Constructor<? extends Log> logConstructor;
// 自动扫描日志实现,并且第三方日志插件加载优先级如下
// 类加载时会默认实现静态方法,实现顺序为slf4j->commonsLoging->log4j2->log4j->jdklog
static {
// 调用tryImplementation方法
tryImplementation(new Runnable() {
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4J2Logging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4JLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useJdkLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useNoLogging();
}
});
}
......
LogFactory有默认的日志加载顺序,写在静态代码块中。默认实现顺序为slf4j->commonsLoging->log4j2->log4j->jdklog。接着分析tryImplementation方法
// 此方法调用的是线程的run方法,仔细看,是直接run,不是start,所以不是多线程 private static void tryImplementation(Runnable runnable) { // 判断全局的日志构造方法是否为空,若为空,则调用线程的run方法 if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // 对没有具体实现的日志类,直接忽略,不抛异常, // ignore } } }
查看具体的run方法,此处继续分析log4j2的run方法。
public static synchronized void useLog4J2Logging() { setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class); } // 使用到了java的反射机制 private static void setImplementation(Class<? extends Log> implClass) { try { // 利用反射获取log4j2的真实构造方法 Constructor<? extends Log> candidate = implClass.getConstructor(new Class[] { String.class }); // 获取log4j2的实例 Log log = candidate.newInstance(new Object[] { LogFactory.class.getName() }); log.debug("Logging initialized using '" + implClass + "' adapter."); // 赋值给全局变量logConstructor logConstructor = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } }
以log4j2为例,分析了具体的日志实现。但我们使用mybatis时通常会在执行sql的时候,对sql进行打印输出。所以接下来查看mybatis如何使用这些日志接口。
4. 分析org.apache.ibatis.logging.jdbc下的类
在源码分析前,回顾下jdbc连接数据库方式。
........ Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 Connection conn=DriverManager.getConnection(URL, USER, PASSWORD); //3.通过数据库的连接操作数据库,实现增删改查(使用Statement类) Statement st=conn.createStatement(); ResultSet rs=st.executeQuery("select * from user"); .........
加载数据库忽略,此处有Connection,Statement,ResultSet等参数,查看org.apache.ibatis.logging.jdbc下的类,都有对应的log类,先分析BaseJdbcLogger类。
public abstract class BaseJdbcLogger { // PrepareedStatement下的所有set方法 protected static final Set<String> SET_METHODS = new HashSet<String>(); protected static final Set<String> EXECUTE_METHODS = new HashSet<String>(); // PrepareecdStatement 设置的key value pair private Map<Object, Object> columnMap = new HashMap<Object, Object>(); // PreparedStatement 设置的column private List<Object> columnNames = new ArrayList<Object>(); // PrepareedStatement 设置的value private List<Object> columnValues = new ArrayList<Object>(); protected Log statementLog; protected int queryStack; /* * Default constructor */ public BaseJdbcLogger(Log log, int queryStack) { this.statementLog = log; if (queryStack == 0) queryStack = 1; this.queryStack = queryStack; } // 初始化默认的一些set方法 static { SET_METHODS.add("setString"); 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("setClob"); SET_METHODS.add("setObject"); SET_METHODS.add("setNull"); EXECUTE_METHODS.add("execute"); EXECUTE_METHODS.add("executeUpdate"); EXECUTE_METHODS.add("executeQuery"); EXECUTE_METHODS.add("addBatch"); } // setColumn方会记录设置的column和对应的value protected void setColumn(Object key, Object value) { columnMap.put(key, value); columnNames.add(key); columnValues.add(value); }
再来分析ConnectionLogger类
// 实现InvocationHandler就知道是个代理类 public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { // 真正的connection private Connection connection; private ConnectionLogger(Connection conn, Log statementLog, int queryStack) { super(statementLog, queryStack); this.connection = conn; } // 增强 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方法 if ("prepareStatement".equals(method.getName())) { // 打印sql参数 if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } // 调用connection真实方法 PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); // 将生成的PreparedStatement也构建成代理对象 stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("prepareCall".equals(method.getName())) { // 若是prepareCall方法 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())) { // 若是createStatement方法 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); } } /* * Creates a logging version of a connection * * @param conn - the original connection * @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); } /* * return the wrapped connection * * @return the connection */ public Connection getConnection() { return connection; } }
后续如PreparedStatementLogger,ResultSetLogger等也是一样,都是封装了PreparedStatement或ResultSet,在执行真实语句前后进行日志打印,打印执行的Sql语句,此处用到了代理模式。熟悉AOP的可能对此有了解。