Mybatis源码研究之ErrorContext

Mybatis可以说是本人由.NET转Java后读的第一份源代码。而且因为所供职的公司属于传统的小型企业,所以相比较于Spring,接触到Mybatis的问题更多,这也导致对于MyBatis的研究投入更多的精力。

1. 概述

诚如标题,今天的关注重心是ErrorContext,其实对于这个类,笔者在一开始的时候感觉非常好奇——这玩意是干啥的? 而随着慢慢对Java理解的深入,尤其是在看了大众点评开源的CAT设计思路之后,再联想其到这个ErrorContext——这不就是个简易版实现吗? 同样都是使用了 ThreadLocal<T>,这种将执行上下文信息的收集独立出来并集中到一处的做法非常值得借鉴和学习。

在大致理解了思路之后,我们再来看看ErrorContext的名字,这个类的名字完美的表明了自己的职责——记录本次执行过程中相关上下文信息,待发生Error时候其他组件就可以从本类实例中获取到相关的上下文信息,这对于排错是非常有帮助的。

2. 内部细节

通观ErrorContext内部实现,我们发现其对于toString()方法的实现细节让人非常眼熟。

通过IDE提供的的“查找引用”功能,我们可以发现改ErrorContext.toString()方法只在 org.apache.ibatis.exceptions.ExceptionFactory类中得到调用, 也正是因为使用了ThreadLocal<T>, 我们就能直接取到之前执行本SQL的线程上的信息, 也就很方便的构建出异常发生时的上下文,快速排错

2.1 实例

我们故意构建一个异常,来看看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();
  }

3. 清理工作

对于ThreadLocal<T>有过了解的读者应该都知道关于ThreadLocal<T>的一个警告:“一定要确保在执行完毕后清空ThreadLocal,避免产生意料之外的问题。”,接下来我们来看看Mybatis是如何确保ThreadLocal<T>在执行完毕后被清空的(通过调用ErrorContext.reset()实现)。

首先让我们来看看 ErrorContext.reset() 方法调用的位置,集中在三个类:

  1. SqlSessionFactoryBuilder
  2. DefaultSqlSession
  3. DefaultSqlSessionFactory
    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()的使用大概分两种:

  1. 启动时。读取Mybatis的XML配置信息,配置读取完毕就重置(调用reset())。也就是下面的org.apache.ibatis.builderorg.apache.ibatis.builder.xml package。
  2. 运行时。也就是下面的org.apache.ibatis.executor package。Mybatis会在每次操作后使用try-finally机制来确保ThreadLocal被重置,而在catch中我们依然是可以使用ExceptionFactory.wrapException()来获取到ErrorContext实例里存储的信息。
    在这里插入图片描述

所以,Mybatis采用 try-catch-finally 的机制, 在可能执行出错时候获取到ErrorContext实例里存储的信息来协助使用者快速排错,而最终又保证了清理工作能如期执行。

4. store()和 recall()

在探究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()源码为例:

  1. 因为ErrorContext中的stored字段为实例字段,所以在ErrorContext.store()方法执行时候当前的线程下的ErrorContext实例将被自身的stored字段引用。
  2. 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 可以执行成功-->
	<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()处打上断点,得到如下截图:

  • 调用selectKey之前:
    调用selectKey之前
  • 调用selectKey之后:
    调用selectKey之后
  • 执行实际的Insert,触发异常:
    执行实际的Insert,触发异常

以上可以看到,在执行Insert语句时候,依然是执行selectKey时的那个ErrorContext,并没有如预期的那样恢复到前一个ErrorContext实例。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值