詳細(xì)了解MyBatis的異常處理機(jī)制
前言
作為一款成熟的ORM框架,MyBatis有自己一套成熟的異常處理體系。MyBatis的異常體系,有如下幾個關(guān)鍵角色。
- PersistenceException。繼承于RuntimeException(直接繼承于**IbatisException),是MyBatis各個功能模塊的異常的父類,所以MyBatis**中使用的異常都是運行時異常;
- ExceptionFactory。MyBatis中根據(jù)異常上下文創(chuàng)建PersistenceException的工廠類,配合ErrorContext使用;
- ErrorContext。MyBatis異常處理的靈魂,是一個和線程綁定的全局異常上下文,在打印異常信息時,能夠反映出異常存在于哪個映射文件中,是做什么操作時引發(fā)的異常以及發(fā)生異常的SQL信息等。
正文
一. MyBatis異常體系說明
MyBatis框架自定義了一個異常基類,叫做PersistenceException,UML圖如下所示。
MyBatis各個功能模塊自定義的異常均繼承于PersistenceException,部分異常類UML圖如下所示。
異常的拋出策略遵循如下原則。
- 優(yōu)先基于邏輯判斷的方式拋出異常。在每個功能模塊中,會優(yōu)先對非法條件或場景進(jìn)行判斷校驗,如果校驗不通過,則拋出功能模塊對應(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的自定義異常。比如初始化日志打印器時的各種反射相關(guān)異常,獲取數(shù)據(jù)庫連接時的各種數(shù)據(jù)庫連接池相關(guān)異常,與數(shù)據(jù)庫交互時的各種SQL異常等,均會被MyBatis統(tǒng)一封裝為各個功能模塊自定義的異常類型,然后向上拋出;
public static Log getLog(String logger) { try { // 運行時異常,校驗異常和Error均可能會發(fā)生 return logConstructor.newInstance(logger); } catch (Throwable t) { // 捕獲到的Throwable統(tǒng)一封裝為自定義的LogException throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t); } }
- 在能夠處理自定義異常的地方精確捕獲異常。在能夠明確下層會拋出哪種異常并且當(dāng)前能夠處理這種異常的情況下,通過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() 方法會拋出SQLException,下面是調(diào)用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; }
// 不能明確下層會拋出哪種異?;蛘弋?dāng)前不能夠處理這種異常的情況 @Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { TypeHandler<?> handler = resolveTypeHandler(rs, columnName); return handler.getResult(rs, columnName); }
總之就是突出一個能處理絕不放過,不能處理絕不逞強(qiáng)。
二. ErrorContext
我們使用MyBatis操作數(shù)據(jù)庫時,如果在映射文件中寫了一條錯誤的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) ......
通過上述的異常信息,我們清晰的知道了錯誤發(fā)生在哪個映射文件,錯誤與哪個對象有關(guān),錯誤是在進(jìn)行什么操作時發(fā)生,錯誤相關(guān)的SQL語句信息,錯誤詳細(xì)的堆棧信息。
MyBatis之所以能夠在異常發(fā)生時打印出上述的完備的異常信息,就是基于ErrorContext,下面對ErrorContext的實現(xiàn)原理和工作機(jī)制進(jìn)行分析。
MyBatis將ErrorContext實現(xiàn)成了線程綁定的單例模式,在ErrorContext中有一個靜態(tài)字段LOCAL,用于存儲每個線程的ErrorContext,同時還提供了instance() 方法用于每個線程獲取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; } ...... }
也就是每個線程在使用MyBatis的過程中,隨時可以通過ErrorContext的instance() 方法拿到當(dāng)前線程綁定的ErrorContext。
ErrorContext有如下幾個字段,用于存儲MyBatis執(zhí)行過程中的關(guān)鍵信息,如下所示。
public class ErrorContext { ...... // 用于暫存ErrorContext private ErrorContext stored; // 保存當(dāng)前操作的映射文件 private String resource; // 保存當(dāng)前的行為 private String activity; // 保存當(dāng)前操作的對象 // 比如保存當(dāng)前的MappedStatement的id private String object; // 保存當(dāng)前的異常信息 private String message; // 保存當(dāng)前執(zhí)行的SQL private String sql; // 保存異常 private Throwable cause; ...... }
下面以一條錯誤的SQL執(zhí)行全過程,演示ErrorContext的完整工作機(jī)制。
已知,MyBatis中,我們通過映射接口執(zhí)行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; }
在上述方法中記錄了resource為com/mybatis/learn/dao/BookMapper.xml,雖然也記錄了activity和object,但是這兩個值會在后續(xù)流程節(jié)點被覆蓋。
繼續(xù)往下執(zhí)行,會在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í)行,會在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í)行,就會在PreparedStatementHandler的query() 方法中真正的通過PreparedStatement操作數(shù)據(jù)庫,如下所示。
@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寫錯,所以在PreparedStatementHandler的query() 方法中通過PreparedStatement操作數(shù)據(jù)庫時,會拋出SQLSyntaxErrorException,該異常會一路往外拋,最終在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后,會通過ExceptionFactory的wrapException() 方法創(chuàng)建PersistenceException,如下所示。
public static RuntimeException wrapException(String message, Exception e) { // 先記錄message和cause到ErrorContext中 // 然后通過ErrorContext的toString()方法組裝異常詳細(xì)信息 // 最后基于異常詳細(xì)信息和異常創(chuàng)建PersistenceException return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e); }
在創(chuàng)建PersistenceException時,會先把ErrorContext的message和cause豐富上,此時ErrorContext的所有字段已經(jīng)完成賦值,然后會通過ErrorContext的toString() 方法組裝得到異常的詳細(xì)信息,最后基于異常詳細(xì)信息和異常創(chuàng)建PersistenceException。我們看到的異常的詳細(xì)打印信息,就是在ErrorContext的toString() 方法中拼接的,下面看一下其實現(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ù)庫操作結(jié)束時,無論操作是否成功,都需要對ErrorContext進(jìn)行初始化,在DefaultSqlSession的selectList() 方法的finally代碼塊中,會調(diào)用到ErrorContext的reset() 方法來初始化ErrorContext,如下所示。
public ErrorContext reset() { resource = null; activity = null; object = null; message = null; sql = null; cause = null; // 防止內(nèi)存泄漏 LOCAL.remove(); return this; }
至此,一次數(shù)據(jù)庫操作中,ErrorContext的使命就完成了。
總結(jié)
其實可以發(fā)現(xiàn),MyBatis的異常使用中,也沒有嚴(yán)格遵循異常規(guī)約,甚至某些地方還明目張膽的觸犯異常規(guī)約,但是其實也不妨礙MyBatis的強(qiáng)大。
MyBatis的異常體系,總結(jié)如下。
- 所有異常都是運行時異常;
- 優(yōu)先基于邏輯判斷的方式拋出異常;
- 所有底層異常統(tǒng)一封裝為MyBatis的自定義異常;
- 能處理絕不放過,不能處理絕不逞強(qiáng)。
此外,MyBatis自己基于ErrorContext實現(xiàn)了一套全局異常處理機(jī)制,使得MyBatis在異常發(fā)生時,能夠打印盡可能詳細(xì)的異常信息,這里給出一個完整的作用流程圖。
以上就是詳細(xì)了解MyBatis的異常處理機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于MyBatis 異常處理機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
metershpere實現(xiàn)調(diào)用自定義jar包中的方法
在MeterSphere接口測試中,面對多層循環(huán)邏輯和邏輯判斷等復(fù)雜情況,直接編寫測試用例往往顯得混亂不便,本文介紹了一個簡化這一過程的方法:首先使用IDEA創(chuàng)建Maven工程,編寫所需邏輯并生成jar包;然后在MeterSphere中上傳此jar包2024-10-10java 獲取當(dāng)前函數(shù)名的實現(xiàn)代碼
以下是對使用java獲取當(dāng)前函數(shù)名的實現(xiàn)代碼進(jìn)行了介紹。需要的朋友可以過來參考下2013-08-08Java并發(fā)編程中的volatile關(guān)鍵字詳解
這篇文章主要介紹了Java并發(fā)編程中的volatile關(guān)鍵字詳解,volatile?用于保證我們某個變量的可見性,使其一直存放在主存中,不被移動到某個線程的私有工作內(nèi)存中,需要的朋友可以參考下2023-08-08你必須得會的SpringBoot全局統(tǒng)一處理異常詳解
程序在運行的過程中,不可避免會產(chǎn)生各種各樣的錯誤,這個時候就需要進(jìn)行異常處理,本文主要為大家介紹了SpringBoot實現(xiàn)全局統(tǒng)一處理異常的方法,需要的可以參考一下2023-06-06