java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis 异常处理机制

详细了解MyBatis的异常处理机制

作者:半夏之沫

本文将对MyBatis的异常体系以及异常使用进行学习,MyBatis版本是3.5.6,作为一款成熟的ORM框架,MyBatis有自己一套成熟的异常处理体系,,需要的朋友可以参考下

前言

作为一款成熟的ORM框架,MyBatis有自己一套成熟的异常处理体系。MyBatis的异常体系,有如下几个关键角色。

正文

一. MyBatis异常体系说明

MyBatis框架自定义了一个异常基类,叫做PersistenceExceptionUML图如下所示。

MyBatis各个功能模块自定义的异常均继承于PersistenceException,部分异常类UML图如下所示。

异常的抛出策略遵循如下原则。

private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (!StatementType.CALLABLE.equals(ms.getStatementType())
        && void.class.equals(ms.getResultMaps().get(0).getType())) {
        throw new BindingException("method " + command.getName()
                                   + " needs either a @ResultMap annotation, a @ResultType annotation,"
                                   + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
    }
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
        RowBounds rowBounds = method.extractRowBounds(args);
        sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
        sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
}
public static Log getLog(String logger) {
    try {
        // 运行时异常,校验异常和Error均可能会发生
        return logConstructor.newInstance(logger);
    } catch (Throwable t) {
        // 捕获到的Throwable统一封装为自定义的LogException
        throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
}
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
    try {
        return getNullableResult(rs, columnName);
    } catch (Exception e) {
        throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
    }
}

上述getResult() 方法会抛出SQLException,下面是调用getResult() 方法时的两种不同处理策略。

// 能明确下层会抛出哪种异常且能够处理这种异常的情况
Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType, List<ResultMapping> constructorMappings,
                                       List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) {
    boolean foundValues = false;
    for (ResultMapping constructorMapping : constructorMappings) {
        final Class<?> parameterType = constructorMapping.getJavaType();
        final String column = constructorMapping.getColumn();
        final Object value;
        try {
            if (constructorMapping.getNestedQueryId() != null) {
                value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
            } else if (constructorMapping.getNestedResultMapId() != null) {
                final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
                value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping));
            } else {
                final TypeHandler<?> typeHandler = constructorMapping.getTypeHandler();
                value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
            }
        } catch (ResultMapException | SQLException e) {
            // 精确的捕获ResultMapException和SQLException
            throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
        }
        constructorArgTypes.add(parameterType);
        constructorArgs.add(value);
        foundValues = value != null || foundValues;
    }
    return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}
// 不能明确下层会抛出哪种异常或者当前不能够处理这种异常的情况
@Override
public Object getNullableResult(ResultSet rs, String columnName)
    throws SQLException {
    TypeHandler<?> handler = resolveTypeHandler(rs, columnName);
    return handler.getResult(rs, columnName);
}

总之就是突出一个能处理绝不放过,不能处理绝不逞强

二. ErrorContext

我们使用MyBatis操作数据库时,如果在映射文件中写了一条错误的SQL,此时运行程序,会得到如下报错信息。

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'book b' at line 4
### The error may exist in com/mybatis/learn/dao/BookMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT             b.id, b.b_name, b.b_price         FROMM             book b
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'book b' at line 4
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
	......
Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'book b' at line 4
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	......

通过上述的异常信息,我们清晰的知道了错误发生在哪个映射文件错误与哪个对象有关错误是在进行什么操作时发生错误相关的SQL语句信息错误详细的堆栈信息

MyBatis之所以能够在异常发生时打印出上述的完备的异常信息,就是基于ErrorContext,下面对ErrorContext的实现原理和工作机制进行分析。

MyBatisErrorContext实现成了线程绑定的单例模式,在ErrorContext中有一个静态字段LOCAL,用于存储每个线程的ErrorContext,同时还提供了instance() 方法用于每个线程获取ErrorContext,相关字段和方法如下所示。

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);
    ......
    private ErrorContext() {
    }
    public static ErrorContext instance() {
        return LOCAL.get();
    }
    ......
}

上述代码可以等效于如下代码。

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    ......
    private ErrorContext() {
    }
    public static ErrorContext instance() {
        ErrorContext context = LOCAL.get();
        if (context == null) {
            context = new ErrorContext();
            LOCAL.set(context);
        }
        return context;
    }
    ......
}

也就是每个线程在使用MyBatis的过程中,随时可以通过ErrorContextinstance() 方法拿到当前线程绑定的ErrorContext

ErrorContext有如下几个字段,用于存储MyBatis执行过程中的关键信息,如下所示。

public class ErrorContext {
    ......
    // 用于暂存ErrorContext
    private ErrorContext stored;
    // 保存当前操作的映射文件
    private String resource;
    // 保存当前的行为
    private String activity;
    // 保存当前操作的对象
    // 比如保存当前的MappedStatement的id
    private String object;
    // 保存当前的异常信息
    private String message;
    // 保存当前执行的SQL
    private String sql;
    // 保存异常
    private Throwable cause;
    ......
}

下面以一条错误的SQL执行全过程,演示ErrorContext的完整工作机制。

已知,MyBatis中,我们通过映射接口执行SQL语句,流程如下。

首先在BaseExecutor中会记录resource,如下所示。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
		throws SQLException {
    // 在这里记录resource
    ErrorContext.instance()
        .resource(ms.getResource())
        .activity("executing a query")
        .object(ms.getId());
    ......
    List<E> list;
    ......
    return list;
}

在上述方法中记录了resourcecom/mybatis/learn/dao/BookMapper.xml,虽然也记录了activityobject,但是这两个值会在后续流程节点被覆盖。

继续往下执行,会在BaseStatementHandlerprepare() 方法中记录sql,如下所示。

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) 
    throws SQLException {
    // 在这里记录sql
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
        statement = instantiateStatement(connection);
        setStatementTimeout(statement, transactionTimeout);
        setFetchSize(statement);
        return statement;
    } catch (SQLException e) {
        closeStatement(statement);
        throw e;
    } catch (Exception e) {
        closeStatement(statement);
        throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}

继续往下执行,会在DefaultParameterHandlersetParameters() 方法中记录activityobject,如下所示。

@Override
public void setParameters(PreparedStatement ps) {
    // 在这里记录activity和object
    ErrorContext.instance()
        .activity("setting parameters")
        .object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        ......
    }
}

继续往下执行,就会在PreparedStatementHandlerquery() 方法中真正的通过PreparedStatement操作数据库,如下所示。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler)
    	throws SQLException {
    // 这里是JDBC里的PreparedStatement
    PreparedStatement ps = (PreparedStatement) statement;
    // 由于之前故意将SQL写错所以这里会报错
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}

由于之前故意在映射文件中将SQL写错,所以在PreparedStatementHandlerquery() 方法中通过PreparedStatement操作数据库时,会抛出SQLSyntaxErrorException,该异常会一路往外抛,最终在DefaultSqlSessionselectList() 方法中被捕获,如下所示。

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

捕获到SQLSyntaxErrorException后,会通过ExceptionFactorywrapException() 方法创建PersistenceException,如下所示。

public static RuntimeException wrapException(String message, Exception e) {
    // 先记录message和cause到ErrorContext中
    // 然后通过ErrorContext的toString()方法组装异常详细信息
    // 最后基于异常详细信息和异常创建PersistenceException
    return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}

在创建PersistenceException时,会先把ErrorContextmessagecause丰富上,此时ErrorContext的所有字段已经完成赋值,然后会通过ErrorContexttoString() 方法组装得到异常的详细信息,最后基于异常详细信息和异常创建PersistenceException。我们看到的异常的详细打印信息,就是在ErrorContexttoString() 方法中拼接的,下面看一下其实现。

@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();
}

最后,一次数据库操作结束时,无论操作是否成功,都需要对ErrorContext进行初始化,在DefaultSqlSessionselectList() 方法的finally代码块中,会调用到ErrorContextreset() 方法来初始化ErrorContext,如下所示。

public ErrorContext reset() {
    resource = null;
    activity = null;
    object = null;
    message = null;
    sql = null;
    cause = null;
    // 防止内存泄漏
    LOCAL.remove();
    return this;
}

至此,一次数据库操作中,ErrorContext的使命就完成了。

总结

其实可以发现,MyBatis的异常使用中,也没有严格遵循异常规约,甚至某些地方还明目张胆的触犯异常规约,但是其实也不妨碍MyBatis的强大。

MyBatis的异常体系,总结如下。

此外,MyBatis自己基于ErrorContext实现了一套全局异常处理机制,使得MyBatis在异常发生时,能够打印尽可能详细的异常信息,这里给出一个完整的作用流程图。

以上就是详细了解MyBatis的异常处理机制的详细内容,更多关于MyBatis 异常处理机制的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文