准备
在阅读源码前,需要先clone源码 地址:https://github.com/mybatis/my...
Mybatis框架使用大量常见的设计模式,学习Mybatis源码我们主要学习以下几点:
- 学习大佬们的编码思想及规范
- 学习一些传承下来的设计模式
- 实践java基础理论
带着问题阅读源码
- 问题1:源码中用了哪些设计模式?为什么要用这些设计模式?
- 问题2.Mybatis打开调试模式之后,能打印sql语句等信息,这是怎么实现的?实现过程中使用了什么设计模式?假如有多个日志实现,加载顺序是什么?
带着这些个问题 我们去源码中找寻答案
首先我们先来看看源码包功能模块图
Mybatis框架提供了一个顶层接口SqlSession
,核心处理层,基础支撑层 这里使用了一个常见的设计模式:外观模式/门面模式
今天我们主要讲解基础支撑层
中日志模块
内部是如何处理的.
源码-logging
打开源码项目,可以看到有20个目录,这里我们先不深入去看,把目光聚焦到logging目录
那么有那么多日志实现,他的加载顺序是什么呢.到这里我们会想到刚刚看到的LogFactory类,大叫都知道工厂方法就是用来获取实例的既然要获取实例那么他的处理也肯定在工厂内部处理完毕了(这里使用了简单工厂模式) 那么我们打开LogFactory类来看看他的加载顺序到底是怎么实现的:
import java.lang.reflect.Constructor;
public final class LogFactory {
public static final String MARKER = "MYBATIS";
private static Constructor extends Log> logConstructor; //定义logConstructor 他继承于Constructor
static { //加载顺序在这里.初始化时必定按照这个顺序执行,可以发现下面这一排排就是我们刚刚看到的各个适配器
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// 在这里私有化构造器 就不能通过new来创建实例了
}
public static Log getLog(Class> clazz) {//第一步.如果我们需要一个Log 那么我们就调用这个方法,传入需要的类即可
return getLog(clazz.getName());
}
public static Log getLog(String logger) {//第二步.根据类的名字获取Log实现.如果获取不到会报错,这个logConstructor就是我们上面的,他内部在初始化时已经通过static的静态代码块进行了处理,所以会处理为对应的日志实现,
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {//每个静态同步方法都是去调用setImplementation
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {//将传入的代码块执行,注意这里是.run 不是.start
if (logConstructor == null) {//如果还没有实现才执行
try {
runnable.run();
} catch (Throwable t) {//如果加载这个实现出现了异常,如:ClassNotFoundException 这个时候直接忽略,等待下一个实现调用
// ignore
}
}
}
//在获取到一个实现成功后 会调用这个方法来初始化Log
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.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
回顾一下问题:源码中用了哪些设计模式?
到目前我们看到Mybatis使用了 1.外观模式/门面模式 2.适配器模式 3简单工厂模式
到这里我们已经拿到了Log的实例不管他是通过log4j还是slf4j的实现,那他是怎么实现对日志进行增强的呢 怎么打印出来的SQL语句和参数以及结果计数的?
如果想要输出这些信息,那么首先肯定要获取到这些信息.
下面我们就去看这块,看看他是怎么获取和存贮这些信息的.
public abstract class BaseJdbcLogger {
protected static final Set SET_METHODS;//set方法容器
protected static final Set EXECUTE_METHODS = new HashSet<>();//执行方法容器
private final Map
到这我们知道了他是怎么存贮信息的获取的方式就是在调用时通过存贮的方法和set方法列表来查询,那么他是什么时候被调用呢?仔细看看这个图我们发现了端倪 在BaseJdbcLogger下面还有
- ConnectionLogger
- PreparedStatementLogger
- ResultSetLogger
- StatementLogger
这些不就是我们很眼熟的流程吗 获取数据库连接--> 预编译SQL--> 执行SQL--> 获取结果
我们随便打开一个 看看他们是怎么在自己的一亩三分地干活的
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {//继承于BaseJdbcLogger 实现InvocationHandler 看到InvocationHandler 就会想到->代理模式
private final Connection 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);
}
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {//判断方法名是不是这两中的一个 短语或 有一个就为true
if (isDebugEnabled()) {//原来开启debug 就会打出日志是在这里呀
debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);// 通过connection创建了PreparedStatement
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); //把stmt 传过去给PreparedStatementLogger的newInstance 创建了一个stmt的代理返回
return stmt;//注意这里返回的不是真实stmt 而是代理过后的stmt
} else if ("createStatement".equals(method.getName())) {//如果是创建Statement
Statement stmt = (Statement) method.invoke(connection, params); //拿到连接去创建一个Statement
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);//把Statement传进StatementLogger 获取Statement的代理对象来返回
return stmt;//注意这里返回的不是真实stmt 而是代理过后的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
* @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);
}
/**
* return the wrapped connection.
*
* @return the connection
*/
public Connection getConnection() {
return connection;
}
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
private final PreparedStatement statement; //还是一样 这里是被代理的实际对象
private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.statement = stmt;
}
@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())) {//还记得那几个容器吗 用来存储各种方法的 现在他来了
//开启DEBUG 我就把参数打印出来
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);//获取到一个resultset
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);//把resultset 也代理了来返回
} else {
return method.invoke(statement, params);
}
} 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);
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} 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);
}
}
/**
* Creates a logging version of a PreparedStatement.
*
* @param stmt - the statement
* @param statementLog - the statement log
* @param queryStack - the query stack
* @return - the proxy
*/
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);
}
/**
* Return the wrapped prepared statement.
*
* @return the PreparedStatement
*/
public PreparedStatement getPreparedStatement() {
return statement;
}
}
最后是ResultSetLogger
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {//ResultSetLogger的增强方法
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++;//只要还有下一条next,就记数加1 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);//开启debug情况下 打印出总数计数
}
}
clearColumnInfo();
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
到这里我们基本已经摸完了这块逻辑 现在复盘一下问题
- 问题1.源码中用了哪些设计模式?为什么要用这些设计模式?
门面/外观模式 适配器模式 代理模式 简单工厂模式. 为什么要使用这些模式 太长了我就不写了,大家自行总结 - 问题2.Mybatis打开调试模式之后,能打印sql语句等信息,这是怎么实现的?实现过程中使用了什么设计模式?假如有多个日志实现,加载顺序是什么?
在调试模式能打印日志,是mybatis框架在实际执行时对对象进行了代理,进行了增强.他使用了代理设计模式.他实现日志的顺序是:
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
加群让我们一起学习吧
关注公众号:java宝典