分析mybatis如何实现打印sql语句
  使⽤mybatis查询数据库时,若⽇志级别为debug时,⾃动打印sql语句,参数值以及结果集数⽬,类似这样
==>  Preparing: select id, description from demo where id = ?
==>  Parameters: 1(Integer)
<==  Total: 1
  若是使⽤jdbc,打印类似⽇志,原有的jdbc逻辑⾥,我们需要插⼊⽇志打印逻辑
1 String sql = "select id, description from demo where id = ?";
2System.out.println("==>  Preparing: " + sql);
3 PreparedStatement psmt = conn.prepareStatement(sql);
4System.out.println("==>  Parameters: 1(Integer)");
5 psmt.setInt(1,1);
6 ResultSet rs = uteQuery();
7int count = 0;
()) {
9    count++;
10 }
11System.out.println("<==  Total:" + count);
  这样做是因为我们⽆法改变jdbc代码,不能让数据库连接获取PreparedStatement对象时,告诉它你把传给你的sql语句打印出来吧。这时候就在想如果prepareStatement有⼀个⽇志打印的功能就好
了,还要可以传⼊⽇志对象和⽇志格式参数就更好了,可惜它没有这样的功能。
  我们只能额外在获取PreparedStatement对象时,PreparedStatement对象设置参数时和ResultSet处理返回结果集时这些逻辑之上加上⽇志打印逻辑,这是很让⼈沮丧的代码。其实很容易想到,这
种受限于原有功能实现,需要增强其能⼒,正是代理模式适⽤的场景,我们只需要创建对应的代理类
去重写我们想要增强的⽅法就好。
  回到mybatis。mybatis查询数据库时也是使⽤的jdbc,mybatis作为⼀种持久层框架,使⽤了动态代理来增强jdbc原有逻辑,代码在org.apache.ibatis.logging.jdbc包下,下⾯从getMapper来逐步分
析mybatis如何实现打印sql语句。
  mybatis有两种接⼝调⽤⽅式,⼀种是基于默认⽅法传⼊statementID,另⼀种是基于Mapper接⼝,其实两种⽅式是⼀样的,⼀会就可以知道,先介绍getMapper。
  getMapper是mybatis顶层API SqlSession的⼀个⽅法,默认实现
public class DefaultSqlSession implements SqlSession {
  // mybatis配置信息对象
private final Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
}
Configuration 保存解析后的配置信息,继续往下⾛
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
Mapper(type, sqlSession);
}
}
MapperRegistry 保存着Mapper接⼝与对应代理⼯⼚的映射关系
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) (type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
wInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
}
MapperProxyFactory是⼀个⽣成Mapper代理类的⼯⼚,使⽤动态代理去⽣成具体的mapper接⼝代理类
public class MapperProxyFactory<T> {
  // 构造器中初始化
private final Class<T> mapperInterface;
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) ClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
电脑往虚拟机里传文件}
  分析MapperProxy定义的代理⾏为,调⽤mapper接⼝⾥⾯的⽅法时,都会⾛这⾥,这⾥完成了mapper接⼝⽅法与l中sql语句的绑定,相关参数已在MapperMethod构造器中初始化,这⾥逻辑较为复杂,简单来说,就public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.DeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
  // 这⾥关联mapper中接⼝⽅法与l中的sql语句
final MapperMethod mapperMethod = cachedMapperMethod(method);
ute(sqlSession, args);
}
}
  下⼀步就是MapperMethod具体的执⾏逻辑了,这⾥内容较多,主要是对执⾏sql的类型进⾏判断,简单截取select部分
case SELECT:
if (urnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (urnsMany()) {
result = executeForMany(sqlSession, args);
} else if (urnsMap()) {
result = executeForMap(sqlSession, args);
} else if (urnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = vertArgsToSqlCommandParam(args);
result = sqlSession.Name(), param);
}
break;
  看到这⾥就可以发现原来当我们使⽤getMapper⽣成的代理类型时,调⽤内部⾃定义⽅法,仍然是基于mybatis默认⽅法的。不过这好像和打印sql语句没啥关系,重点在类似sqlSession.Name(), param)⽅法,依旧去查看其默认实现DefaultSqlSession,可以发现是调⽤内部的执⾏器去执⾏查询⽅法的
程序员培训
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = MappedStatement(statement);
    // mybatis存在3种执⾏器,批处理、缓存和标准执⾏器
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
  这⾥的执⾏器是在构造⽅法内赋值的,默认情况下使⽤的是SimpleExecutor,这⾥省略⽗类query⽅法中的缓存相关代码,重点是⼦类去实现的doQuery⽅法
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
mysql语句的执行顺序StatementHandler handler = wStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, Timeout());
process和procedurehandler.parameterize(stmt);
return stmt;
}
  这⾥获取Connection对象时,猜测mybatis⼀定是对Connection进⾏了增强,不然⽆法在获取Statement 之前打印sql语句
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = Connection();
if (statementLog.isDebugEnabled()) {
wInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = ClassLoader();
return (Connection) wProxyInstance(cl, new Class[]{Connection.class}, handler);
}
  代码很清楚的,如果你的当前系统⽀持打印debug⽇志,那么就动态帮你⽣成⼀个连接对象,这⾥传⼊的代理⾏为是这个类本⾝,那么只需要分析其的invoke⽅法就好了,这⾥依旧只分析⼀部分必要的
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
if (Object.class.DeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".Name())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = wInstance(stmt, statementLog, queryStack);
return stmt;
}
}
  在这⾥到了打印的sql信息,还可以发现下⾯的PreparedStatementLogger继续增强,逻辑都是⼀样的,就不分析了。有⼀点需要注意的,这⾥是mapper接⼝的动态代理类,所以这⾥的⽇志级别是受接⼝所在包的⽇志级别控制的,只需要配置mapper接⼝的⽇志级别是debug,就可以看见打印的sql语句了。
  到这⾥,已经知道了,sql信息是如何打印出来的了,可是,还有⼀个问题需要解决,这⾥的⽇志对象是怎么来的,mybatis本⾝是没有⽇志打印能⼒的。
  mybatis本⾝并没有内嵌⽇志框架,⽽是考虑了⽤户本⾝的⽇志框架的选择。简⽽⾔之,就是⽤户⽤啥⽇志框架,它就⽤什么框架,当⽤户存在多种选择时,它也有⾃⼰的偏好设计(可以指定)。这样做,就需要mybatis兼容市⾯上常见的⽇志框架,同时⾃⼰也要有⼀套⽇志接⼝,mybatis定义⽇志输出级别控制,兼容⽇志框架提供的具体实现类。这是不是⼜和⼀种设计模式很像了,适配器模式,转换不同接⼝,实现统⼀调⽤,具体的代码在org.apache.ibatis.logging包下。
  org.apache.ibatis.logging.Log 是mybatis⾃⼰定义的⽇志接⼝,org.apache.ibatis.logging.LogFactory 是mybatis⽤于加载合适的⽇志实现类,其下的众多包,均是⽇志框架的适配器类,主要做⽇志级别的转换和具体log对象的创建,那么这些适配器类怎么加载的,LogFactory有⼀个静态代码块去尝试加载合适的⽇志框架,然后创建正确的log对象。
public final class LogFactory {
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(new Runnable() {贝尔摩德的口头禅
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
.
..
}
public static Log getLog(String logger) {
try {
wInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
}
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = Constructor(String.class);
Log log = wInstance(Name());
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);
}
}
}
background菜鸟教程View Code
  这⾥会按顺序去尝试加载不同的⽇志框架,若当前系统中存在对应的⽇志框架,才可以加载成功,这样logConstructor就有值了,下⾯则不会再尝试加载,在getLog⾥⾯则是实例化具体的⽇志对象。
  这样就完成了mybatis如何打印sql语句的整体流程解析,主要有两点,创建Log对象和通过动态代理给jdbc增加⽇志打印能⼒。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。