前言
使用Spring-Jdbc的情況下,在有些場景中,我們需要根據資料庫報的例外型別的不同,來撰寫我們的業務代碼,比如說,我們有這樣一段邏輯,如果我們新插入的記錄,存在唯一約束沖突,就會回傳給客戶端描述:記錄已存在,請勿重復操作
代碼一般是這么寫的:
@Resource
private JdbcTemplate jdbcTemplate;
public String testAdd(){
try {
jdbcTemplate.execute("INSERT INTO user_info (user_id, user_name, email, nick_name, status, address) VALUES (80002, '張三豐', '[email protected]', '張真人', 1, '武當山');");
return "OK";
}catch (DuplicateKeyException e){
return "記錄已存在,請勿重復操作";
}
}
測驗一下:

如上圖提示,并且無論什么更換什么資料庫(Spring-Jdbc支持的),代碼都不用改動
那么Spring-Jdbc是在使用不同資料庫時,Spring如何幫我們實作對例外的抽象的呢?
代碼實作
我們來正向看下代碼:
首先入口JdbcTemplate.execute方法:
public void execute(final String sql) throws DataAccessException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Executing SQL statement [" + sql + "]");
}
...
//實際執行入口
this.execute(new ExecuteStatementCallback(), true);
}
內部方法execute
@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;
Object var12;
try {
stmt = con.createStatement();
this.applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
this.handleWarnings(stmt);
var12 = result;
} catch (SQLException var10) {
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
//SQL出現例外后,在這里進行例外轉換
throw this.translateException("StatementCallback", sql, var10);
} finally {
if (closeResources) {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
}
return var12;
}
例外轉換方法translateException
protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
//獲取例外轉換器,然后根據資料庫回傳碼相關資訊執行轉換操作
//轉換不成功,也有兜底例外UncategorizedSQLException
DataAccessException dae = this.getExceptionTranslator().translate(task, sql, ex);
return (DataAccessException)(dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
}
獲取轉換器方法getExceptionTranslator
public SQLExceptionTranslator getExceptionTranslator() {
//獲取轉換器屬性,如果為空,則生成一個
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator != null) {
return exceptionTranslator;
} else {
synchronized(this) {
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator == null) {
DataSource dataSource = this.getDataSource();
//shouldIgnoreXml是一個標記,就是不通過xml加載bean,默認false
if (shouldIgnoreXml) {
exceptionTranslator = new SQLExceptionSubclassTranslator();
} else if (dataSource != null) {
//如果DataSource不為空,則生成轉換器SQLErrorCodeSQLExceptionTranslator
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
} else {
// 其他情況,生成SQLStateSQLExceptionTranslator轉換器
exceptionTranslator = new SQLStateSQLExceptionTranslator();
}
this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator;
}
return (SQLExceptionTranslator)exceptionTranslator;
}
}
}
轉換方法:
因為默認的轉換器是SQLErrorCodeSQLExceptionTranslator,所以這里呼叫SQLErrorCodeSQLExceptionTranslator的doTranslate方法

類圖呼叫關系如上,實際先呼叫的是AbstractFallbackSQLExceptionTranslator.translate的方法
@Nullable
public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
Assert.notNull(ex, "Cannot translate a null SQLException");
//這里才真正呼叫SQLErrorCodeSQLExceptionTranslator.doTranslate方法
DataAccessException dae = this.doTranslate(task, sql, ex);
if (dae != null) {
return dae;
} else {
//如果沒有找到回應的例外,則呼叫其他轉換器,輸入遞回呼叫,這里后面說
SQLExceptionTranslator fallback = this.getFallbackTranslator();
return fallback != null ? fallback.translate(task, sql, ex) : null;
}
}
實際轉換類SQLErrorCodeSQLExceptionTranslator的方法:
//這里省略了一些無關代碼,只保留了核心代碼
//先獲取SQLErrorCodes集合,在根據回傳的SQLException中獲取的ErrorCode進行匹配,根據匹配結果進行回傳回應的例外
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
....
SQLErrorCodes sqlErrorCodes = this.getSqlErrorCodes();
String errorCode = Integer.toString(ex.getErrorCode());
...
if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
this.logTranslation(task, sql, sqlEx, false);
return new DuplicateKeyException(this.buildMessage(task, sql, sqlEx), sqlEx);
}
...
return null;
}
上面的SQLErrorCodes是一個錯誤碼集合,但是不是全部資料庫的所有錯誤碼集合,而是只取了相應資料庫的錯誤碼集合,怎么保證獲取的是當前使用的資料庫的錯誤碼,而不是其他資料庫的錯誤碼呢?當然Spring為我們實作了,在SQLErrorCodeSQLExceptionTranslator中:
public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {
private SingletonSupplier<SQLErrorCodes> sqlErrorCodes;
//默認構造方法,設定了如果轉換失敗,下一個轉換器是SQLExceptionSubclassTranslator
public SQLErrorCodeSQLExceptionTranslator() {
this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
}
//前面生成轉換器的時候,exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
//使用的是本構造方法,傳入了DataSource,其中有資料庫廠商資訊,本文中是MYSQL
public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
this();
this.setDataSource(dataSource);
}
//從錯誤碼工廠SQLErrorCodesFactory里,獲取和資料源對應的廠商的所有錯誤碼
public void setDataSource(DataSource dataSource) {
this.sqlErrorCodes = SingletonSupplier.of(() -> {
return SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource);
});
this.sqlErrorCodes.get();
}
}
錯誤碼工廠SQLErrorCodesFactory的resolveErrorCodes方法:
//既然是工廠,里面肯定有各種資料庫的錯誤碼,本文中使用的是MYSQL,我們看一下實作邏輯
@Nullable
public SQLErrorCodes resolveErrorCodes(DataSource dataSource) {
Assert.notNull(dataSource, "DataSource must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Looking up default SQLErrorCodes for DataSource [" + this.identify(dataSource) + "]");
}
//從快取中拿MYSQL對應的SQLErrorCodes
SQLErrorCodes sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
if (sec == null) {
synchronized(this.dataSourceCache) {
sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
if (sec == null) {
try {
String name = (String)JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName);
if (StringUtils.hasLength(name)) {
SQLErrorCodes var10000 = this.registerDatabase(dataSource, name);
return var10000;
}
} catch (MetaDataAccessException var6) {
logger.warn("Error while extracting database name", var6);
}
return null;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("SQLErrorCodes found in cache for DataSource [" + this.identify(dataSource) + "]");
}
return sec;
}
**快取dataSourceCache如何生成的? **
public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) {
//根據資料庫型別名稱(這里是MySQL),獲取錯誤碼串列
SQLErrorCodes sec = this.getErrorCodes(databaseName);
if (logger.isDebugEnabled()) {
logger.debug("Caching SQL error codes for DataSource [" + this.identify(dataSource) + "]: database product name is '" + databaseName + "'");
}
this.dataSourceCache.put(dataSource, sec);
return sec;
}
public SQLErrorCodes getErrorCodes(String databaseName) {
Assert.notNull(databaseName, "Database product name must not be null");
//從errorCodesMap根據key=MYSQL獲取SQLErrorCodes
SQLErrorCodes sec = (SQLErrorCodes)this.errorCodesMap.get(databaseName);
if (sec == null) {
Iterator var3 = this.errorCodesMap.values().iterator();
while(var3.hasNext()) {
SQLErrorCodes candidate = (SQLErrorCodes)var3.next();
if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) {
sec = candidate;
break;
}
}
}
if (sec != null) {
this.checkCustomTranslatorRegistry(databaseName, sec);
if (logger.isDebugEnabled()) {
logger.debug("SQL error codes for '" + databaseName + "' found");
}
return sec;
} else {
if (logger.isDebugEnabled()) {
logger.debug("SQL error codes for '" + databaseName + "' not found");
}
return new SQLErrorCodes();
}
}
//SQLErrorCodesFactory構造方法中,生成的errorCodesMap,map的內容來自org/springframework/jdbc/support/sql-error-codes.xml檔案
protected SQLErrorCodesFactory() {
Map errorCodes;
try {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
lbf.setBeanClassLoader(this.getClass().getClassLoader());
XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf);
Resource resource = this.loadResource("org/springframework/jdbc/support/sql-error-codes.xml");
if (resource != null && resource.exists()) {
bdr.loadBeanDefinitions(resource);
} else {
logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)");
}
resource = this.loadResource("sql-error-codes.xml");
if (resource != null && resource.exists()) {
bdr.loadBeanDefinitions(resource);
logger.debug("Found custom sql-error-codes.xml file at the root of the classpath");
}
errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false);
if (logger.isTraceEnabled()) {
logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet());
}
} catch (BeansException var5) {
logger.warn("Error loading SQL error codes from config file", var5);
errorCodes = Collections.emptyMap();
}
this.errorCodesMap = errorCodes;
}
sql-error-codes.xml檔案中配置了各個資料庫的主要的錯誤碼
這里列舉了MYSQL部分,當然還有其他部分,我們可以看到唯一性約束錯誤碼是1062,就可以翻譯成DuplicateKeyException例外了
<bean id="MySQL" >
<property name="databaseProductNames">
<list>
<value>MySQL</value>
<value>MariaDB</value>
</list>
</property>
<property name="badSqlGrammarCodes">
<value>1054,1064,1146</value>
</property>
<property name="duplicateKeyCodes">
<value>1062</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
</property>
<property name="dataAccessResourceFailureCodes">
<value>1</value>
</property>
<property name="cannotAcquireLockCodes">
<value>1205,3572</value>
</property>
<property name="deadlockLoserCodes">
<value>1213</value>
</property>
</bean>
你已經看到,比如上面的錯誤碼值列舉了一部分,如果出現了一個不在其中的錯誤碼肯定是匹配不到,Spring當然能想到這種情況了
/**
*@公-眾-號:程式員阿牛
*在AbstractFallbackSQLExceptionTranslator中,看到如果查找失敗會獲取下一個后續轉換器
*/
@Nullable
public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
Assert.notNull(ex, "Cannot translate a null SQLException");
DataAccessException dae = this.doTranslate(task, sql, ex);
if (dae != null) {
return dae;
} else {
SQLExceptionTranslator fallback = this.getFallbackTranslator();
return fallback != null ? fallback.translate(task, sql, ex) : null;
}
}
SQLErrorCodeSQLExceptionTranslator的后置轉換器是什么?
//構造方法中已經指定,SQLExceptionSubclassTranslator
public SQLErrorCodeSQLExceptionTranslator() {
this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
}
SQLExceptionSubclassTranslator的轉換方法邏輯如下:
/**
*@公-眾-號:程式員阿牛
*可以看出實際按照子型別別來判斷,回傳相應的錯誤類,如果匹配不到,則找到下一個處理器,這里的處理其我們可以根據構造方法青松找到*SQLStateSQLExceptionTranslator
*/
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
if (ex instanceof SQLTransientException) {
if (ex instanceof SQLTransientConnectionException) {
return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLTransactionRollbackException) {
return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLTimeoutException) {
return new QueryTimeoutException(this.buildMessage(task, sql, ex), ex);
}
} else if (ex instanceof SQLNonTransientException) {
if (ex instanceof SQLNonTransientConnectionException) {
return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLDataException) {
return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLIntegrityConstraintViolationException) {
return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLInvalidAuthorizationSpecException) {
return new PermissionDeniedDataAccessException(this.buildMessage(task, sql, ex), ex);
}
if (ex instanceof SQLSyntaxErrorException) {
return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
}
if (ex instanceof SQLFeatureNotSupportedException) {
return new InvalidDataAccessApiUsageException(this.buildMessage(task, sql, ex), ex);
}
} else if (ex instanceof SQLRecoverableException) {
return new RecoverableDataAccessException(this.buildMessage(task, sql, ex), ex);
}
return null;
}
SQLStateSQLExceptionTranslator的轉換方法:
/**
*@公-眾-號:程式員阿牛
*可以看出根據SQLState的前兩位來判斷例外,根據匹配結果回傳相應的例外資訊
*/
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
String sqlState = this.getSqlState(ex);
if (sqlState != null && sqlState.length() >= 2) {
String classCode = sqlState.substring(0, 2);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
}
if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
}
if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
}
if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
}
if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
}
if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {
return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
}
}
return ex.getClass().getName().contains("Timeout") ? new QueryTimeoutException(this.buildMessage(task, sql, ex), ex) : null;
}
為什么SQLState可以得出錯誤型別?
因為資料庫是根據 X/Open 和 SQL Access Group SQL CAE 規范 (1992) 所進行的定義,SQLERROR 回傳 SQLSTATE 值,SQLSTATE 值是包含五個字符的字串 ,五個字符包含數值或者大寫字母, 代表各種錯誤或者警告條件的代碼,SQLSTATE 有個層次化的模式:頭兩個字符標識條件的通常表示錯誤條件的類別, 后三個字符表示在該通用類中的子類,成功的狀態是由 00000 標識的,SQLSTATE 代碼在大多數地方都是定義在 SQL 標準里
處理流程圖

用到了哪些設計模式?
組合模式

通過上圖大家有沒有發現三個實作類之間的關系—組合關系,組合關系在父類AbstractFallbackSQLExceptionTranslator中變成了遞回呼叫,這里充滿了智慧(Composite設計模式),
單例模式
在SQLErrorCodesFactory(單例模式)
策略模式
根據資料庫的不同,獲取不同的errorcodes集合
---------------------END---------------------
關注:程式員阿牛,Spring系列更多文章,為你呈現
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297952.html
標籤:Java
上一篇:配置JDK方法
