Mybatis随笔(四) 异常处理ErrorContext

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.

看下其方法


ErrorContext - Method

ThreadLocal

一个私有的构造方法,是一个单例模式的应用,通过ThreadLocal实现

ThreadLocal是一块线程私有的领地,可以随时从线程上下文中拿到这份变量的副本,是多线程下安全访问共享变量的一种解决方案。

1.4引入,但其返回的都是Object类型对象,需要强转;在1.5时添加泛型后得到了极大的增强

关于ThreadLocal其大名鼎鼎的就是可能会造成内存泄漏,详细可参考ThreadLocal内存泄漏真因探究

ErrorContext就是一种很好的ThreadLocal应用实例。

Method

  1. 使用

resource / activity / object / message / sql / cause 这些方法都是会使用到ErrorContext的地方,也就是会创建ThreadLocal

use - usage

如果你跟着调用链往上追溯,都会是下面三个点

  • 加载配置文件创建 SqlSessionFactory
  • 创建 SqlSession
  • SqlSession SQL执行
  1. 清理

清理方法是reset,看下调用

reset - useage
  • 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的释放

  1. 存储和恢复

除了以上方法,我们注意到还有两个特殊方法storerecall
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 详解

你可能感兴趣的:(Mybatis随笔(四) 异常处理ErrorContext)