Spring系列之不同數據庫異常如何抽象的?


前言

使用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, '張三豐', 'xxx@126.com', '張真人', 1, '武當山');");
        return "OK";
    }catch (DuplicateKeyException e){
        return "記錄已存在,請勿重復操作";
    }
}

測試一下:
file
如上圖提示,並且無論什么更換什么數據庫(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 {
	...
    } catch (SQLException var10) {
        ....
	//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方法
file
類圖調用關系如上,實際先調用的是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());
	...
	//這里用1062唯一性約束沖突,所以走到這里的邏輯,從而返回DuplicateKeyException
	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" class="org.springframework.jdbc.support.SQLErrorCodes">
		<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 標准里

處理流程圖

file

用到了哪些設計模式?

組合模式

file

通過上圖大家有沒有發現三個實現類之間的關系—組合關系,組合關系在父類AbstractFallbackSQLExceptionTranslator中變成了遞歸調用,這里充滿了智慧(Composite設計模式)。

單例模式

在SQLErrorCodesFactory(單例模式)

策略模式

根據數據庫的不同,獲取不同的errorcodes集合

總結:

在學習的過程中,我們不但要關注其實現的方式,還要關注我們能從里面學到什么?比如說從這個異常抽象中,能學到幾種設計模式,以及使用的場景,這些都是可以運用到以后的工作中。
下一篇,我們繼續。
也歡迎加我,一起交流和成長。

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM