Mybatis
使用ErrorContext
来做异常的统一处理
大体上就是使用一个ThreadLocal
来存储当前线程的异常信息,当异常发生时可以根据这些信息快速地定位问题,一目了然
ErrorContext
先贴下源码
public class ErrorContext {
private static final String LINE_SEPARATOR = System.lineSeparator();
private static final ThreadLocal LOCAL = ThreadLocal.withInitial(ErrorContext::new);
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
return LOCAL.get();
}
public ErrorContext store() {
ErrorContext newContext = new ErrorContext();
newContext.stored = this;
LOCAL.set(newContext);
return LOCAL.get();
}
public ErrorContext recall() {
if (stored != null) {
LOCAL.set(stored);
stored = null;
}
return LOCAL.get();
}
public ErrorContext resource(String resource) {
this.resource = resource;
return this;
}
public ErrorContext activity(String activity) {
this.activity = activity;
return this;
}
public ErrorContext object(String object) {
this.object = object;
return this;
}
public ErrorContext message(String message) {
this.message = message;
return this;
}
public ErrorContext sql(String sql) {
this.sql = sql;
return this;
}
public ErrorContext cause(Throwable cause) {
this.cause = cause;
return this;
}
public ErrorContext reset() {
resource = null;
activity = null;
object = null;
message = null;
sql = null;
cause = null;
LOCAL.remove();
return this;
}
@Override
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);
}
// sql
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();
}
}
其实都很易读,精髓就是那个ThreadLocal
类型的类变量LOCAL
.
看下其方法
ThreadLocal
一个私有的构造方法,是一个单例模式的应用,通过ThreadLocal
实现
ThreadLocal
是一块线程私有的领地,可以随时从线程上下文中拿到这份变量的副本,是多线程下安全访问共享变量的一种解决方案。
1.4
引入,但其返回的都是Object类型对象,需要强转;在1.5
时添加泛型后得到了极大的增强
关于ThreadLocal
其大名鼎鼎的就是可能会造成内存泄漏,详细可参考ThreadLocal内存泄漏真因探究
而ErrorContext
就是一种很好的ThreadLocal
应用实例。
Method
- 使用
resource
/ activity
/ object
/ message
/ sql
/ cause
这些方法都是会使用到ErrorContext
的地方,也就是会创建ThreadLocal
如果你跟着调用链往上追溯,都会是下面三个点
- 加载配置文件创建 SqlSessionFactory
- 创建
SqlSession
-
SqlSession
SQL执行
- 清理
清理方法是reset
,看下调用
-
DefaultSqlSession
SQL执行 -
DefaultSqlSessionFactory
创建 SqlSession -
SqlSessionFactoryBuilder
创建 SqlSessionFactory
与使用对应,再看下具体调用方式
@Override
public Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
Cursor cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
registerCursor(cursor);
return cursor;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
其他的都是一样,通过 try-catch-finally
的方式确保ThreadLocal
的释放
- 存储和恢复
除了以上方法,我们注意到还有两个特殊方法store
和recall
ErrorContext
还有一个属性用于存储自身
private ErrorContext stored;
store
public ErrorContext store() {
ErrorContext newContext = new ErrorContext();
newContext.stored = this;
LOCAL.set(newContext);
return LOCAL.get();
}
将当前对象保存起来,同时新建一个ErrorContext
返回
recall
public ErrorContext recall() {
if (stored != null) {
LOCAL.set(stored);
stored = null;
}
return LOCAL.get();
}
很明显,恢复之前存储起来的ErrorContext
再来看下调用
protected void generateKeys(Object parameter) {
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
ErrorContext.instance().store();
keyGenerator.processBefore(executor, mappedStatement, null, parameter);
ErrorContext.instance().recall();
}
发现是在KeyGenerator # processBefore
方法前后调用的,很明显是怕此方法污染了异常信息,具体原因需要再深入看下这个方法。
可参考mybatis 源码分析 KeyGenerator 详解
思考
这里对比一下另一种写法(应该是低版本的实现,其实无所谓什么版本,只要能触发我们思考就值得讲一讲)
public ErrorContext store() {
stored = this;
LOCAL.set(new ErrorContext());
return LOCAL.get();
}
这种写法会有什么问题吗?
首先我们明确几点
-
ErrorContext
是一种单例 -
stored
是一个实例变量
那么上述写法就做了如下三件事
第一步 [email protected] = A@ErrorContext
第二步 新建 ErrorContex
对象 B@ErrorContext
第三步 将 B@ErrorContext
放入 ThreadLocal
这里请注意一下,[email protected]
是空的
那么下次调用recall
方法来恢复的时候还能拿到stored
下来的对象吗?
是拿不到的。
参考链接
ThreadLocal内存泄漏真因探究
Mybatis源码研究之ErrorContext
mybatis 源码分析 KeyGenerator 详解