詳細(xì)了解MyBatis的異常處理機(jī)制
前言
作為一款成熟的ORM框架,MyBatis有自己一套成熟的異常處理體系。MyBatis的異常體系,有如下幾個(gè)關(guān)鍵角色。
- PersistenceException。繼承于RuntimeException(直接繼承于**IbatisException),是MyBatis各個(gè)功能模塊的異常的父類(lèi),所以MyBatis**中使用的異常都是運(yùn)行時(shí)異常;
- ExceptionFactory。MyBatis中根據(jù)異常上下文創(chuàng)建PersistenceException的工廠類(lèi),配合ErrorContext使用;
- ErrorContext。MyBatis異常處理的靈魂,是一個(gè)和線(xiàn)程綁定的全局異常上下文,在打印異常信息時(shí),能夠反映出異常存在于哪個(gè)映射文件中,是做什么操作時(shí)引發(fā)的異常以及發(fā)生異常的SQL信息等。
正文
一. MyBatis異常體系說(shuō)明
MyBatis框架自定義了一個(gè)異常基類(lèi),叫做PersistenceException,UML圖如下所示。

MyBatis各個(gè)功能模塊自定義的異常均繼承于PersistenceException,部分異常類(lèi)UML圖如下所示。

異常的拋出策略遵循如下原則。
- 優(yōu)先基于邏輯判斷的方式拋出異常。在每個(gè)功能模塊中,會(huì)優(yōu)先對(duì)非法條件或場(chǎng)景進(jìn)行判斷校驗(yàn),如果校驗(yàn)不通過(guò),則拋出功能模塊對(duì)應(yīng)的自定義異常;
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));
}
}- 所有底層異常統(tǒng)一封裝為MyBatis的自定義異常。比如初始化日志打印器時(shí)的各種反射相關(guān)異常,獲取數(shù)據(jù)庫(kù)連接時(shí)的各種數(shù)據(jù)庫(kù)連接池相關(guān)異常,與數(shù)據(jù)庫(kù)交互時(shí)的各種SQL異常等,均會(huì)被MyBatis統(tǒng)一封裝為各個(gè)功能模塊自定義的異常類(lèi)型,然后向上拋出;
public static Log getLog(String logger) {
try {
// 運(yùn)行時(shí)異常,校驗(yàn)異常和Error均可能會(huì)發(fā)生
return logConstructor.newInstance(logger);
} catch (Throwable t) {
// 捕獲到的Throwable統(tǒng)一封裝為自定義的LogException
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}- 在能夠處理自定義異常的地方精確捕獲異常。在能夠明確下層會(huì)拋出哪種異常并且當(dāng)前能夠處理這種異常的情況下,通過(guò)try-catch精確的捕獲異常。
@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() 方法會(huì)拋出SQLException,下面是調(diào)用getResult() 方法時(shí)的兩種不同處理策略。
// 能明確下層會(huì)拋出哪種異常且能夠處理這種異常的情況
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;
}// 不能明確下層會(huì)拋出哪種異?;蛘弋?dāng)前不能夠處理這種異常的情況
@Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
TypeHandler<?> handler = resolveTypeHandler(rs, columnName);
return handler.getResult(rs, columnName);
}總之就是突出一個(gè)能處理絕不放過(guò),不能處理絕不逞強(qiáng)。
二. ErrorContext
我們使用MyBatis操作數(shù)據(jù)庫(kù)時(shí),如果在映射文件中寫(xiě)了一條錯(cuò)誤的SQL,此時(shí)運(yùn)行程序,會(huì)得到如下報(bào)錯(cuò)信息。
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) ......
通過(guò)上述的異常信息,我們清晰的知道了錯(cuò)誤發(fā)生在哪個(gè)映射文件,錯(cuò)誤與哪個(gè)對(duì)象有關(guān),錯(cuò)誤是在進(jìn)行什么操作時(shí)發(fā)生,錯(cuò)誤相關(guān)的SQL語(yǔ)句信息,錯(cuò)誤詳細(xì)的堆棧信息。
MyBatis之所以能夠在異常發(fā)生時(shí)打印出上述的完備的異常信息,就是基于ErrorContext,下面對(duì)ErrorContext的實(shí)現(xiàn)原理和工作機(jī)制進(jìn)行分析。
MyBatis將ErrorContext實(shí)現(xiàn)成了線(xiàn)程綁定的單例模式,在ErrorContext中有一個(gè)靜態(tài)字段LOCAL,用于存儲(chǔ)每個(gè)線(xiàn)程的ErrorContext,同時(shí)還提供了instance() 方法用于每個(gè)線(xiàn)程獲取ErrorContext,相關(guān)字段和方法如下所示。
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;
}
......
}也就是每個(gè)線(xiàn)程在使用MyBatis的過(guò)程中,隨時(shí)可以通過(guò)ErrorContext的instance() 方法拿到當(dāng)前線(xiàn)程綁定的ErrorContext。
ErrorContext有如下幾個(gè)字段,用于存儲(chǔ)MyBatis執(zhí)行過(guò)程中的關(guān)鍵信息,如下所示。
public class ErrorContext {
......
// 用于暫存ErrorContext
private ErrorContext stored;
// 保存當(dāng)前操作的映射文件
private String resource;
// 保存當(dāng)前的行為
private String activity;
// 保存當(dāng)前操作的對(duì)象
// 比如保存當(dāng)前的MappedStatement的id
private String object;
// 保存當(dāng)前的異常信息
private String message;
// 保存當(dāng)前執(zhí)行的SQL
private String sql;
// 保存異常
private Throwable cause;
......
}下面以一條錯(cuò)誤的SQL執(zhí)行全過(guò)程,演示ErrorContext的完整工作機(jī)制。
已知,MyBatis中,我們通過(guò)映射接口執(zhí)行SQL語(yǔ)句,流程如下。

首先在BaseExecutor中會(huì)記錄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;
}在上述方法中記錄了resource為com/mybatis/learn/dao/BookMapper.xml,雖然也記錄了activity和object,但是這兩個(gè)值會(huì)在后續(xù)流程節(jié)點(diǎn)被覆蓋。
繼續(xù)往下執(zhí)行,會(huì)在BaseStatementHandler的prepare() 方法中記錄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);
}
}繼續(xù)往下執(zhí)行,會(huì)在DefaultParameterHandler的setParameters() 方法中記錄activity和object,如下所示。
@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) {
......
}
}繼續(xù)往下執(zhí)行,就會(huì)在PreparedStatementHandler的query() 方法中真正的通過(guò)PreparedStatement操作數(shù)據(jù)庫(kù),如下所示。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException {
// 這里是JDBC里的PreparedStatement
PreparedStatement ps = (PreparedStatement) statement;
// 由于之前故意將SQL寫(xiě)錯(cuò)所以這里會(huì)報(bào)錯(cuò)
ps.execute();
return resultSetHandler.handleResultSets(ps);
}由于之前故意在映射文件中將SQL寫(xiě)錯(cuò),所以在PreparedStatementHandler的query() 方法中通過(guò)PreparedStatement操作數(shù)據(jù)庫(kù)時(shí),會(huì)拋出SQLSyntaxErrorException,該異常會(huì)一路往外拋,最終在DefaultSqlSession的selectList() 方法中被捕獲,如下所示。
@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后,會(huì)通過(guò)ExceptionFactory的wrapException() 方法創(chuàng)建PersistenceException,如下所示。
public static RuntimeException wrapException(String message, Exception e) {
// 先記錄message和cause到ErrorContext中
// 然后通過(guò)ErrorContext的toString()方法組裝異常詳細(xì)信息
// 最后基于異常詳細(xì)信息和異常創(chuàng)建PersistenceException
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}在創(chuàng)建PersistenceException時(shí),會(huì)先把ErrorContext的message和cause豐富上,此時(shí)ErrorContext的所有字段已經(jīng)完成賦值,然后會(huì)通過(guò)ErrorContext的toString() 方法組裝得到異常的詳細(xì)信息,最后基于異常詳細(xì)信息和異常創(chuàng)建PersistenceException。我們看到的異常的詳細(xì)打印信息,就是在ErrorContext的toString() 方法中拼接的,下面看一下其實(shí)現(xiàn)。
@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();
}最后,一次數(shù)據(jù)庫(kù)操作結(jié)束時(shí),無(wú)論操作是否成功,都需要對(duì)ErrorContext進(jìn)行初始化,在DefaultSqlSession的selectList() 方法的finally代碼塊中,會(huì)調(diào)用到ErrorContext的reset() 方法來(lái)初始化ErrorContext,如下所示。
public ErrorContext reset() {
resource = null;
activity = null;
object = null;
message = null;
sql = null;
cause = null;
// 防止內(nèi)存泄漏
LOCAL.remove();
return this;
}至此,一次數(shù)據(jù)庫(kù)操作中,ErrorContext的使命就完成了。
總結(jié)
其實(shí)可以發(fā)現(xiàn),MyBatis的異常使用中,也沒(méi)有嚴(yán)格遵循異常規(guī)約,甚至某些地方還明目張膽的觸犯異常規(guī)約,但是其實(shí)也不妨礙MyBatis的強(qiáng)大。
MyBatis的異常體系,總結(jié)如下。
- 所有異常都是運(yùn)行時(shí)異常;
- 優(yōu)先基于邏輯判斷的方式拋出異常;
- 所有底層異常統(tǒng)一封裝為MyBatis的自定義異常;
- 能處理絕不放過(guò),不能處理絕不逞強(qiáng)。
此外,MyBatis自己基于ErrorContext實(shí)現(xiàn)了一套全局異常處理機(jī)制,使得MyBatis在異常發(fā)生時(shí),能夠打印盡可能詳細(xì)的異常信息,這里給出一個(gè)完整的作用流程圖。

以上就是詳細(xì)了解MyBatis的異常處理機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于MyBatis 異常處理機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)文件和base64流的相互轉(zhuǎn)換功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)文件和base64流的相互轉(zhuǎn)換功能,涉及Java文件讀取及base64 轉(zhuǎn)換相關(guān)操作技巧,需要的朋友可以參考下2018-05-05
SpringBoot中整合JodConverter實(shí)現(xiàn)文件在線(xiàn)預(yù)覽功能
Spring Boot JodConverter是一個(gè)基于Spring Boot框架的文檔轉(zhuǎn)換工具,它使用JodConverter庫(kù)來(lái)實(shí)現(xiàn)文檔格式之間的轉(zhuǎn)換,本文主要介紹了SpringBoot中整合JodConverter實(shí)現(xiàn)文件在線(xiàn)預(yù)覽功能,需要的朋友可以參考下2024-04-04
SpringBoot入門(mén)原理及優(yōu)勢(shì)分析
本篇文章主要來(lái)為大家介紹SpringBoot入門(mén)原理及優(yōu)勢(shì)分析,有需要的朋友可以借鑒參考下,希望可以有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-09-09
Java將時(shí)間按月份分段的實(shí)現(xiàn)思路與方法
這篇文章主要給大家介紹了關(guān)于Java將時(shí)間按月份分段的實(shí)現(xiàn)思路與方法,通過(guò)文中介紹的方法可以將時(shí)間分成我們想要的時(shí)間段,文中給出了詳細(xì)的實(shí)例代碼,需要的朋友可以參考下2021-07-07
MyBatis-Plus處理更新操作時(shí)的空值問(wèn)題及解決過(guò)程
這篇文章主要介紹了MyBatis-Plus處理更新操作時(shí)的空值問(wèn)題及解決過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-06-06
Java初學(xué)者之五子棋游戲?qū)崿F(xiàn)教程
這篇文章主要為大家詳細(xì)介紹了Java初學(xué)者之五子棋游戲?qū)崿F(xiàn)教程,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10

