MyBatis 还有一个很有意思的点在于异常日志的输出。不知道大家有没有发现,使用 MyBatis 时定位问题非常容易,我们只需要查看一下控制台的异常日志就能一目了然地知道问题出现在了哪里。这归功于一个简单的类 ErrorContext。我们首先来一点一点认识一个这个类。
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
MyBatis 异常涵盖的信息总结为一点就是:异常是由谁在做什么的时候在哪个资源文件中发生的,执行的 SQL 是哪个,以及 java 详细的异常信息。这六个私有变量分别存储这些信息:
1、resource:存储异常存在于哪个资源文件中。
如:### The error may exist in mapper/AuthorMapper.xml
2、activity:存储异常是做什么操作时发生的。
如:### The error occurred while setting parameters
3、object:存储哪个对象操作时发生异常。
如:### The error may involve defaultParameterMap
4、message:存储异常的概览信息。
如:### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘id2’ in ‘field list’
5、sql:存储发生日常的 SQL 语句。
如:### SQL: select id2, name, sex, phone from author where name = ?
6、cause:存储详细的 Java 异常日志。
如:### Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘id2’ in ‘field list’ at
org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) at
org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150) at
org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141) at
org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:139) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:76)
对于这六个成员变量的 “set” 方法,命名同相应成员变量,均是对成员变量做赋值操作并返回存储完相应信息后当前 ErrorContext 的实例。例如 resource 变量对应的 resource() 方法:
public ErrorContext resource(String resource) {
this.resource = resource;
return this;
}
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
我们知道,ThreadLocal 是本地线程存储,它的作用是为变量在每个线程中创建一个副本,每个线程内部都可以使用该副本,线程之间互不影响。
ErrorContext 可以看作是线程内部的单例模式:
1、使用 ThreadLocal 来管理 ErrorContext:
保证了在多线程环境中,每个线程内部可以共用一份 ErrorContext,但多个线程持有的 ErrorContext 互不影响,保证了异常日志的正确输出。
提供私有的构造方法:
private ErrorContext() {
}
提供获取线程内实例的静态公有的接口:
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
在同一线程中,ErrorContext 通过该接口提供给外界一个获取其唯一实例的方式。在调用 instance() 时,首先从 LOCAL 中获取,如果获取到了 ErrorContext 实例,则直接返回该实例;若未获取到,则会调用其构造方法创建一个,并将其存入 LOCAL。
stored 变量充当一个中介,在调用 store() 方法时将当前 ErrorContext 保存下来,在调用 recall() 方法时将该 ErrorContext 实例传递给 LOCAL。
public ErrorContext store() {
stored = this;
LOCAL.set(new ErrorContext());
return LOCAL.get();
}
public ErrorContext recall() {
if (stored != null) {
LOCAL.set(stored);
stored = null;
}
return LOCAL.get();
}
reset() 顾名思义,用来重置变量,为变量赋 null 值,以便 gc 的执行,并清空 LOCAL:
public ErrorContext reset() {
resource = null;
activity = null;
object = null;
message = null;
sql = null;
cause = null;
LOCAL.remove();
return this;
}
toString() 方法则是用来拼接异常信息,最终打印用的:
public String toString() {
StringBuilder description = new StringBuilder();
// message
if (this.message != null) {
description.append(LINE_SEPARATOR);
description.append("### ");
description.append(this.message);
}
// resource
if (resource != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may exist in ");
description.append(resource);
}
// object
if (object != null) {
description.append(LINE_SEPARATOR);
description.append("### The error may involve ");
description.append(object);
}
// activity
if (activity != null) {
description.append(LINE_SEPARATOR);
description.append("### The error occurred while ");
description.append(activity);
}
// activity
if (sql != null) {
description.append(LINE_SEPARATOR);
description.append("### SQL: ");
description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
}
// cause
if (cause != null) {
description.append(LINE_SEPARATOR);
description.append("### Cause: ");
description.append(cause.toString());
}
return description.toString();
}
MyBatis 使用 ErrorContext 的一般方法是:在方法最开始存储相应的执行信息,资源文件位置、SQL 语句等,对应 ErrorContext 中六个成员变量,其实我们已经在分析 Mapper 方法调用的时候已经见过它了。为了使程序出错,我们故意写错 sql,来看一下 ErrorContext 是如何工作的。
我们依旧使用 debug 源码的方式,入口代码为:
List<Author> list = mapper.selectByName("Sylvia");
错误的 sql:id2 字段不存在
<select id="selectByName" resultMap="AuthorMap" >
select
id2, name, sex, phone
from author
where name = #{name}
</select>
这次我们只跟踪 ErrorContext 都在哪里出现:
第一处:org.apache.ibatis.executor.BaseExecutor 的 query(…) 方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//...
}
在这里,ErrorContext 存储了 resource、activity 和 object,此时的 ErrorContext.instance() 值为:
### The error may exist in mapper/AuthorMapper.xml
### The error may involve com.zhaoxueer.learn.dao.AuthorMapper.selectByName
### The error occurred while executing a query
第二处:org.apache.ibatis.executor.statement.BaseStatementHandler 的 prepare() 方法
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
//...
}
在这里,ErrorContext 又存储了 sql,此时的 ErrorContext.instance() 值为:
### The error may exist in mapper/AuthorMapper.xml
### The error may involve com.zhaoxueer.learn.dao.AuthorMapper.selectByName
### The error occurred while executing a query
### SQL: select id2, name, sex, phone from author where name = ?
第三处:org.apache.ibatis.exceptions.ExceptionFactory
接着在 com.mysql.cj.jdbc.ConnectionImpl 类中执行时抛出了异常,我们跟踪一下抛出的异常来继续查看 ErrorContext 还在哪里出现。
首先,mysql 的异常抛给了 org.apache.ibatis.logging.jdbc.PreparedStatementLogger:
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
//...
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
最后回到了 org.apache.ibatis.session.defaults.DefaultSqlSession:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//...
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在 catch 块中,调用了 ExceptionFactory.wrapException(…) 方法,并向其传递了参数:"Error querying database. Cause: " + e 和 e,其中 e 的值在这里为:java.sql.SQLSyntaxErrorException: Unknown column ‘id2’ in ‘field list’。ExceptionFactory 是将一路填充下来的 ErrorContext 最终抛出去的工厂,通过它来统一管理 ErrorContext 异常抛出。我们进入 ExceptionFactory:
public class ExceptionFactory {
private ExceptionFactory() {
// Prevent Instantiation
}
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
}
我们看到在 wrapException(…) 方法中,ErrorContext 又存入了 message 和 cause,此时的 ErrorContext.instance() 值为:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'id2' in 'field list'
### The error may exist in mapper/AuthorMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select id2, name, sex, phone from author where name = ?
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'id2' in 'field list'
最后抛出完整的异常信息,即文章开头的异常日志图片,并在 DefaultSqlSession 类的 selectList(…) 方法的 finally 块中调用 reset() 方法清空 ErrorContext。
分析到这里,我们就可以看出来,ErrorContext 的使用是非常棒的一点。我们知道 Java 的异常抛出往往是一大篇内容,要想定位问题并没那么容易,因此 MyBatis 没有直接使用 Java 默认的异常抛出是有足够理由的。通过 ErrorContext 这样一个简单的类,最后抛出给开发者的异常简短且一目了然,一看到异常我们就可以知道是哪里疏忽了,就算是接手别人的代码,你同样可以直接根据抛出异常中资源文件位置来定位问题。