1. 程式人生 > >Spring揭祕讀書筆記 八 資料訪問異常體系

Spring揭祕讀書筆記 八 資料訪問異常體系

這篇部落格 來自spring揭祕一書的第十三章

為什麼要有訪問異常都有一個體系,這個我們得從DAO模式說起。

DAO模式

任何一個系統,不管是一個最簡單的小系統,還是大規模的系統,都得跟資料打交道,說白了都得時常進行存取資料的操作。我們暫且不論資料本身,資料儲存的方式就已經是各有不同了。
最簡單的,把資料儲存到關係型資料庫中。這裡面至少就有MySQL,Oracle等等
我還可以把資料儲存到文字檔案裡。
還可以把資料儲存到csv檔案中(關於csv,大家百度之)
還有LDAP(Lightweight Directory Access Protocol)輕量目錄訪問協議


為了統一和簡化系統訪問這不同儲存方式的資料,就提出了DAO模式。
換句話說,DAO層乾的就是遮蔽因為不同的資料儲存方式而帶來存取差異。
我再說的簡單點,在我們的系統裡,一般會有一個總的DAO類,是一個介面。
然後就是有MySQLDaoImpl,OracleDaoImpl等等。



舉個例子吧,我們看一個訪問顧客的例子
public interface IUserDao{
    public User findUserByPK(Integer id);     
    public void updateUser(User user);
}
在服務層的程式碼裡,我們只有宣告一個IUserDao型的例項變數,並讓spring給我們注入即可
public class UserService{
    private IUserDao userDao;
 
    public IUserDao getUserDao(){
        return userDao;
    }
 
    public void setUserDao(IUserDao userDao){
        this.userDao = userDao;
    }
     
    public void disableUser(Integer userId){
        User user = this.userDao.findUserByPK(userId);
        userDao.updateUser(user);
    }
}
假定我們的資料儲存在關係型資料庫中
那麼我就給IUserDao來一個jdbc的實現
public class JDBCUserDao implements IUserDao{
 
    @Override
    public User findUserByPK(Integer id){
        // TODO Auto-generated method stub
        return null;
    }
 
    @Override
    public void updateUser(User user){
        // TODO Auto-generated method stub
     }
}
如果以後資料需要從關係型資料庫中遷移到文字檔案中(我知道這個假設很扯淡,就是為了說明如果後來資料的儲存方式發生了改變),那麼我們再實現一個TextUserDao即可
public class TextUserDao implements IUserDao{
 
    @Override
    public User findUserByPK(Integer id){
        // TODO Auto-generated method stub
        return null;
    }
 
    @Override
    public void updateUser(User user){
        // TODO Auto-generated method stub
     }
}
我們需要改動的地方,就是再spring的配置檔案裡,把UserService的配置變一下即可。

看到了吧,DAO的優勢就在於可以遮蔽由不同的資料訪問機制的差異而導致的儲存差異。

Exception處理的問題

這樣就OK了嗎,似乎是OK了。
這麼吧,我們把JDBCUserDao補充完整。
import java.sql.Connection;
 
import javax.sql.DataSource;
 
public class JDBCUserDao implements IUserDao{
    //省略datasource的getset
    private DataSource dataSource ;
 
    @Override
    public User findUserByPK(Integer id){
        Connection conn = null;
        try{
            conn = getDataSource().getConnection();
            //....
            User user = new User();
            //........
            return user;
        } 
        catch (SQLException e){
            //是丟擲異常,還是在當前位置處理。。。
        }
        finally{
            releaseConnection(conn);
        }
        return null;
    }
 
    //省略updateuser與releaseConnection
}
問題就在於try catch裡,如果捕獲到異常
是丟擲呢,還是就直接處理呢?
如果是直接就處理了,那麼service就收到一個null,它什麼都不知道,那這怎麼整?
那就只能拋給service層了。
不過,JDBCUserDao類的findUserByPK方法的簽名就得改一改了。
public User findUserByPK(Integer id) throws SQLException
同樣的IUserDao的簽名也得改一次
public User findUserByPK(Integer id) throws SQLException;


這樣一來就帶來了兩個問題
1 我們引入DAO的目的就是為了統一,不管使用什麼樣的資料儲存機制,客戶端(也就是service層)應該是不變的,現在因為使用關係型資料庫,需要丟擲特定的SQLException,那麼你讓service層怎麼辦?加上處理SQLException的邏輯?如果我不用關係型資料庫了,我使用文字方式儲存資料,那麼就肯定不會丟擲SQLException。你又讓Service層怎麼辦?
2 如果我使用了文字方式來儲存資料,findUserByPK方法需要丟擲一個TextExcetpin(這個異常是我杜撰的),那怎麼辦?再改簽名嗎?
public User findUserByPK(Integer id) throws SQLException,TextExcetpin;

如果儲存機制再改變,我們又繼續新增丟擲的異常型別,這個設計也太糟糕了吧。


就目前來看,我們的dao似乎建造了一個空中樓閣。


包裝Exception

問題來了,不要怕,解決它就是了。
上面的問題的核心是,不同的dao的實現會丟擲不同的異常,如果對每一種異常都處理一下,我們的dao層的設計也就沒有意義了。
那麼我們把不同的異常,包裝成一個統一的異常不就OK了?
包裝成什麼異常呢?checked exception還是unchecked exception呢?


這裡我簡要介紹一下這兩種異常。
Exception的子類有兩類
一類是 類似NullPointerException這種,繼承自RuntimeException(RuntimeException又繼承自Exception),在出現RuntimeException的地方,我們不需要try catch,方法的簽名處也不需要宣告throws,這類exception我們稱之為unchecked exception
另一類是 類似ClassNotFound,SQLExceptin這種,直接繼承自Exception。這種異常必須得try catch,如果不及時捕獲的話,在方法的簽名處就得宣告 throws。這類exception我們稱之為checked exception。



我們現在的情況是,在dao裡面的exception需要丟擲,但是丟擲後,我們需要service層怎麼處理?最好的辦法就是不做處理,僅僅讓他知道就ok。
那麼我們選擇unchecked exception
這麼一來JDBCUserDao的核心程式碼就是下面的樣子了:
public User findUserByPK(Integer id){
	try{
	 conn = getDataSource().getConnection();
	 //....
	  User user = new User();
	   Statement stmt = conn.createStatement();
	   stmt.execute("");
	   //........
	  return user;
	} 
	catch (SQLException e){
	   throw new RuntimeException(e);
	}
}
關鍵問題是,因為RuntimeException不需要在方法的簽名處宣告throws
我們的方法簽名又迴歸大一統了。
public User findUserByPK(Integer id)
另外還有一個小問題,就拿關係型資料庫來說吧,mysql與oracle的錯誤包裝方式不一樣,有的資料庫提供商採用SQLException的ErrorCode作為具體的錯誤資訊標準,有的資料庫提供商則通過SQLException的SqlState來返回相信的錯誤資訊。如果直接像上面的那樣子直接丟擲一個RuntimeException,那麼客戶端要想知道錯誤資訊還不知道去errorcode看還是去sqlstate看呢。
怎麼辦?我們使用分類轉譯的方式,改成下面的樣子
catch (SQLException e){
    //是丟擲異常,還是在當前位置處理。。。
    if(isMysqlVendor()){
        //按照mysql資料庫的規則分析錯誤資訊然後丟擲
        throw new RuntimeException(e);
    }
    if(isOracleVendor()){
        //按照oracle資料庫的規則分析錯誤資訊並丟擲
        throw new RuntimeException(e);
    }
    throw new RuntimeException(e);
}
或者我們可以吧上面的兩個if判斷裝到一個工具類裡面去。


還有一個問題,上面的處理方式,所有的異常其實就一個RuntimeException。還不夠細緻。
比如,資料庫連線不上、ldap伺服器連線失敗,他們被認為是資源獲取失敗;而主鍵衝突或者是其它的資源衝突,他們被認為是資料訪問一致性衝突。針對這些情況,可以為RuntimeException為基準,為獲取資源失敗這種情況分配一個RuntimeException子型別,稱其為ResourceFailerException,而資料一致性衝突對應另外一個子型別DataIntegrityViolationException,其它的分類異常可以加以類推,所以我們需要的只是一套unchecked exception型別的面向資料訪問領域的異常層次型別。


不需要重新發明輪子

spring已經為我們做好了下面的異常體系:




DataAccessException位於org.springframework.dao中

上面各種exception的具體職責,我就不細說了,大家百度之。



參照資料

http://my.oschina.net/u/218421/blog/38478