Mybatis可以说是本人由.NET转Java后读的第一份源代码。而且因为所供职的公司属于传统的小型企业,所以相比较于Spring,接触到Mybatis的问题更多,这也导致对于MyBatis的研究投入更多的精力。
诚如标题,今天的关注重心是ErrorContext
,其实对于这个类,笔者在一开始的时候感觉非常好奇——这玩意是干啥的? 而随着慢慢对Java理解的深入,尤其是在看了大众点评开源的CAT设计思路之后,再联想其到这个ErrorContext
——这不就是个简易版实现吗? 同样都是使用了 ThreadLocal
,这种将执行上下文信息的收集独立出来并集中到一处的做法非常值得借鉴和学习。
在大致理解了思路之后,我们再来看看ErrorContext
的名字,这个类的名字完美的表明了自己的职责——记录本次执行过程中相关上下文信息,待发生Error时候其他组件就可以从本类实例中获取到相关的上下文信息,这对于排错是非常有帮助的。
通观ErrorContext
内部实现,我们发现其对于toString()
方法的实现细节让人非常眼熟。
通过IDE提供的的“查找引用”功能,我们可以发现改ErrorContext.toString()
方法只在 org.apache.ibatis.exceptions.ExceptionFactory
类中得到调用, 也正是因为使用了ThreadLocal
, 我们就能直接取到之前执行本SQL的线程上的信息, 也就很方便的构建出异常发生时的上下文,快速排错。
我们故意构建一个异常,来看看ErrorContext.toString()
方法的表现:
再对照ErrorContext.toString()
的实现(内容过多,所以直接贴源码了):
// ErrorContext.java
@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);
}
// 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();
}
对于ThreadLocal
有过了解的读者应该都知道关于ThreadLocal
的一个警告:“一定要确保在执行完毕后清空ThreadLocal,避免产生意料之外的问题。”,接下来我们来看看Mybatis是如何确保ThreadLocal
在执行完毕后被清空的(通过调用ErrorContext.reset()
实现)。
首先让我们来看看 ErrorContext.reset()
方法调用的位置,集中在三个类:
我们来看看平时的Mybatis操作:
// DefaultSqlSessionFactory作为SqlSessionFactory接口的实现类,用于构建SqlSession实例。
// DefaultSqlSession作为SqlSession接口的实现类。这个应该算得上框架使用者最常接触到的。
// 以上是作为运行时的应用
// 而SqlSessionFactoryBuilder则是负责在启动时候的解析 Mybatis xml配置文件。
SqlSessionFactory sqlSessionFactory = xxx;
SqlSession sqlSession = sqlSessionFactory.openSession();
T oneResult = session.<T> selectOne(sqlId, param);
Console.log(oneResult);
正如上文已经提及到的,Mybatis对ErrorContext.reset()
的使用大概分两种:
reset()
)。也就是下面的org.apache.ibatis.builder
和org.apache.ibatis.builder.xml
package。org.apache.ibatis.executor
package。Mybatis会在每次操作后使用try-finally机制来确保ThreadLocal被重置,而在catch中我们依然是可以使用ExceptionFactory.wrapException()
来获取到ErrorContext
实例里存储的信息。所以,Mybatis采用 try-catch-finally 的机制, 在可能执行出错时候获取到ErrorContext
实例里存储的信息来协助使用者快速排错,而最终又保证了清理工作能如期执行。
在探究Mybatis的ErrorContext
类时,我们会发现这样的两个方法store()
和recall()
。其实只是从名字我们也能大致猜出其含义,以及它们肯定是成组被调用的,但这样的位置是哪里呢?。
依然是通过IDE提供的“查找引用”功能,我们发现这组方法的调用位置居然也是只有一处,正是如下图所示的源码位置:
// BaseStatementHandler.java 正是唯一调用store()和recall()的位置, 这也看出这两个的使用是成组的.
protected void generateKeys(Object parameter) {
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
ErrorContext.instance().store();
keyGenerator.processBefore(executor, mappedStatement, null, parameter);
ErrorContext.instance().recall();
}
如果对KeyGenerator
接口有所了解的读者,应该知道该接口除了processBefore()
方法之外,还有另外一个名为processAfter()
,这样就不可避免地产生了一个疑问:“为什么Mybatis没有选择为processAfter()也进行一次类似地操作?”。注意以下只是笔者的猜想,不负任何责任:因为processBefore()执行先于主体数据库执行,如果不进行这个成组操作,之后的主体操作出现的异常信息可能被前者所污染,导致排错困难。
笔者一直存在一些疑问,以下面的ErrorContext
源码和上面的BaseStatementHandler.generateKeys()
源码为例:
ErrorContext
中的stored
字段为实例字段,所以在ErrorContext.store()
方法执行时候当前的线程下的ErrorContext
实例将被自身的stored
字段引用。ErrorContext.store()
方法执行后当前线程有了一个全新的ErrorContext
实例,其stored字段的值为null。所以ErrorContext.recall()
方法是唤不回前一个ErrorContext
实例。这就有意思了,如果这两个方法真是笔者臆断的作用,那这逻辑上就说不通了?public class ErrorContext {
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext stored;
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();
}
}
下面我们以实际的测试样例来佐证一下:我们故意构建一个如下的Mybatis映射片段:
<insert id="xxx" parameterType="map">
<selectKey keyProperty="XXID" resultType="java.lang.String" order="BEFORE">
SELECT sys_guid() FROM DUAL
selectKey>
INSERT INTO xxy (
id
,hh
) VALUES (
#{ID}
,#{XXID}
)
insert>
我们在BaseStatementHandler.generateKeys()
源码,以及ExceptionFactory.wrapException()
处打上断点,得到如下截图:
以上可以看到,在执行Insert语句时候,依然是执行selectKey时的那个ErrorContext,并没有如预期的那样恢复到前一个ErrorContext实例。