如何優雅的設計java異常

個人公衆號:MarkerHub,網站:markerhub.comjava

更多精選文章請點擊:Java筆記大全.mdmysql

小Hub領讀:

做者以增刪改查收貨地址爲實例,詳細說明了如何去設計一個好的異常處理,包括使用Guava中的Preconditions、hibernate的hibernate-validator,還有如何異常和處理異常的邏輯,文章有點長,看完仍是收穫挺大!git


導語

異常處理是程序開發中必不可少操做之一,但如何正確優雅的對異常進行處理確是一門學問,筆者根據本身的開發經驗來談一談我是如何對異常進行處理的。github

因爲本文只做一些經驗之談,不涉及到基礎知識部分,若是讀者對異常的概念還很模糊,請先查看基礎知識。web

如何選擇異常類型

異常的類別

正如咱們所知道的,java 中的異常的超類是 java.lang.Throwable(後文省略爲 Throwable), 它有兩個比較重要的子類, java.lang.Exception(後文省略爲 Exception) 和 java.lang.Error(後文省略爲 Error),其中 Error 由 JVM 虛擬機進行管理, 如咱們所熟知的 OutOfMemoryError 異常等,因此咱們本文不關注 Error 異常,那麼咱們細說一下 Exception 異常。redis

Exception 異常有個比較重要的子類,叫作 RuntimeException。咱們將 RuntimeException 或其餘繼承自 RuntimeException 的子類稱爲非受檢異常 (unchecked Exception),其餘繼承自 Exception 異常的子類稱爲受檢異常 (checked Exception)。本文重點來關注一下受檢異常和非受檢異常這兩種異常。spring

如何選擇異常

從筆者的開發經驗來看,若是在一個應用中,須要開發一個方法 (如某個功能的 service 方法),這個方法若是中間可能出現異常,那麼你須要考慮這個異常出現以後是否調用者能夠處理,而且你是否但願調用者進行處理,若是調用者能夠處理,而且你也但願調用者進行處理,那麼就要拋出受檢異常,提醒調用者在使用你的方法時,考慮到若是拋出異常時若是進行處理。sql

類似的,若是在寫某個方法時,你認爲這是個偶然異常,理論上說,你以爲運行時可能會碰到什麼問題,而這些問題也許不是必然發生的,也不須要調用者顯示的經過異常來判斷業務流程操做的,那麼這時就可使用一個 RuntimeException 這樣的非受檢異常.數據庫

好了,估計我上邊說的這段話,你讀了不少遍也依然以爲晦澀了。編程

那麼,請跟着個人思路,在慢慢領會一下。

何時才須要拋異常

首先咱們須要瞭解一個問題,何時才須要拋異常?異常的設計是方便給開發者使用的,但不是亂用的,筆者對於何時拋異常這個問題也問了不少朋友,能給出準確答案的確實很少。其實這個問題很簡單,若是你以爲某些」 問題」 解決不了了,那麼你就能夠拋出異常了。

好比,你在寫一個 service, 其中在寫到某段代碼處, 你發現可能會產生問題,那麼就請拋出異常吧,相信我,你此時拋出異常將是一個最佳時機。

應該拋出怎樣的異常

瞭解完了何時才須要拋出異常後,咱們再思考一個問題,真的當咱們拋出異常時,咱們應該選用怎樣的異常呢?到底是受檢異常仍是非受檢異常呢 (RuntimeException) 呢?

我來舉例說明一下這個問題,先從受檢異常提及, 好比說有這樣一個業務邏輯,須要從某文件中讀取某個數據,這個讀取操做多是因爲文件被刪除等其餘問題致使沒法獲取從而出現讀取錯誤,那麼就要從 redis 或 mysql 數據庫中再去獲取此數據, 參考以下代碼,getKey(Integer) 爲入口程序.

public String getKey(Integer key){
    String  value;
    try {
        InputStream inputStream = getFiles("/file/nofile");
        //接下來從流中讀取key的value指
        value = ...;
    } catch (Exception e) {
        //若是拋出異常將從mysql或者redis進行取之
        value = ...;
    }
}

public InputStream getFiles(String path) throws Exception {
    File file = new File(path);
    InputStream inputStream = null;
    try {
        inputStream = new BufferedInputStream(new FileInputStream(file));
    } catch (FileNotFoundException e) {
        throw new Exception("I/O讀取錯誤",e.getCause());
    }
    return inputStream;
}


複製代碼

ok,看了以上代碼之後,你也許心中有一些想法,原來受檢異常能夠控制義務邏輯,對,沒錯,經過受檢異常真的能夠控制業務邏輯,可是切記不要這樣使用,咱們應該合理的拋出異常,由於程序自己纔是流程,異常的做用僅僅是當你進行不下去的時候找到的一個藉口而已,它並不能當成控制程序流程的入口或出口,若是這樣使用的話,是在將異常的做用擴大化,這樣將會致使代碼複雜程度的增長,耦合性會提升,代碼可讀性下降等問題。

那麼就必定不要使用這樣的異常嗎?其實也不是,在真的有這樣的需求的時候,咱們能夠這樣使用,只是切記,不要把它真的當成控制流程的工具或手段。那麼究竟何時纔要拋出這樣的異常呢?要考慮,若是調用者調用出錯後,必定要讓調用者對此錯誤進行處理才能夠,知足這樣的要求時,咱們纔會考慮使用受檢異常。

接下來,咱們來看一下非受檢異常呢 (RuntimeException),對於 RuntimeException 這種異常,咱們其實不少見,好比 java.lang.NullPointerException/java.lang.IllegalArgumentException 等,那麼這種異常咱們時候拋出呢?

當咱們在寫某個方法的時候,可能會偶然遇到某個錯誤,咱們認爲這個問題時運行時可能爲發生的,而且理論上講,沒有這個問題的話,程序將會正常執行的時候,它不強制要求調用者必定要捕獲這個異常,此時拋出 RuntimeException 異常。

舉個例子,當傳來一個路徑的時候,須要返回一個路徑對應的 File 對象:

public void test() {
    myTest.getFiles("");
}

public File getFiles(String path) {
    if(null == path || "".equals(path)){
        throw  new NullPointerException("路徑不能爲空!");
    }
    File file = new File(path);

    return file;
}


複製代碼

上述例子代表,若是調用者調用 getFiles(String) 的時候若是 path 是空,那麼就拋出空指針異常 (它是 RuntimeException 的子類), 調用者不用顯示的進行 try…catch… 操做進行強制處理. 這就要求調用者在調用這樣的方法時先進行驗證,避免發生 RuntimeException. 以下:

public void test() {
    String path = "/a/b.png";
    if(null != path && !"".equals(path)){
        myTest.getFiles("");
    }
}

public File getFiles(String path) {
    if(null == path || "".equals(path)){
        throw  new NullPointerException("路徑不能爲空!");
    }
    File file = new File(path);

    return file;
}


複製代碼

應該選用哪一種異常

經過以上的描述和舉例,能夠總結出一個結論,**RuntimeException 異常和受檢異常之間的區別就是: 是否強制要求調用者必須處理此異常,若是強制要求調用者必須進行處理,那麼就使用受檢異常,不然就選擇非受檢異常 (RuntimeException)。**通常來說,若是沒有特殊的要求,咱們建議使用 RuntimeException 異常。

場景介紹和技術選型

架構描述

正如咱們所知,傳統的項目都是以 MVC 框架爲基礎進行開發的,本文主要從使用 restful 風格接口的設計來體驗一下異常處理的優雅。

咱們把關注點放在 restful 的 api 層 (和 web 中的 controller 層相似) 和 service 層,研究一下在 service 中如何拋出異常,而後 api 層如何進行捕獲而且轉化異常。

使用的技術是: spring-boot,jpa(hibernate),mysql, 若是對這些技術不是太熟悉,讀者須要自行閱讀相關材料。

業務場景描述

選擇一個比較簡單的業務場景,以電商中的收貨地址管理爲例,用戶在移動端進行購買商品時,須要進行收貨地址管理,在項目中,提供一些給移動端進行訪問的 api 接口,如: 添加收貨地址,刪除收貨地址,更改收貨地址,默認收貨地址設置,收貨地址列表查詢,單個收貨地址查詢等接口。

構建約束條件

ok,這個是設置好的一個很基本的業務場景,固然,不管什麼樣的 api 操做,其中都包含一些規則:

添加收貨地址:
入參:

  • 用戶 id

  • 收貨地址實體信息

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在 的

  • 收貨地址的必要字段不能爲 空

  • 若是用戶尚未收貨地址,當此收貨地址建立時設置成默認收貨地址 —

刪除收貨地址:
入參:

  • 用戶 id

  • 收貨地址 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址不能爲空,且此收貨地址確實是存在的

  • 判斷此收貨地址是不是用戶的收貨地址

  • 判斷此收貨地址是否爲默認收貨地址,若是是默認收貨地址,那麼不能進行刪除

更改收貨地址:
入參:

  • 用戶 id

  • 收貨地址 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址不能爲空,且此收貨地址確實是存在的

  • 判斷此收貨地址是不是用戶的收貨地址

默認地址設置:
入參:

  • 用戶 id

  • 收貨地址 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址不能爲空,且此收貨地址確實是存在的

  • 判斷此收貨地址是不是用戶的收貨地址

收貨地址列表查詢:
入參:

  • 用戶 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

單個收貨地址查詢:
入參:

  • 用戶 id

  • 收貨地址 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址不能爲空,且此收貨地址確實是存在的

  • 判斷此收貨地址是不是用戶的收貨地址

約束判斷和技術選型

對於上述列出的約束條件和功能列表,我選擇幾個比較典型的異常處理場景進行分析: 添加收貨地址,刪除收貨地址,獲取收貨地址列表。

那麼應該有哪些必要的知識儲備呢,讓咱們看一下收貨地址這個功能:

添加收貨地址中須要對用戶 id 和收貨地址實體信息就行校驗,那麼對於非空的判斷,咱們如何進行工具的選擇呢?傳統的判斷以下:

/**
 * 添加地址
 * @param uid
 * @param address
 * @return
 */
public Address addAddress(Integer uid,Address address){
    if(null != uid){
        //進行處理..
    }
    return null;
}


複製代碼

上邊的例子,若是隻判斷 uid 爲空還好,若是再去判斷 address 這個實體中的某些必要屬性是否爲空,在字段不少的狀況下,這無非是災難性的。

那咱們應該怎麼進行這些入參的判斷呢,給你們介紹兩個知識點:

  • Guava 中的 Preconditions 類實現了不少入參方法的判斷

  • jsr 303 的 validation 規範 (目前實現比較全的是 hibernate 實現的 hibernate-validator)

若是使用了這兩種推薦技術,那麼入參的判斷會變得簡單不少。**推薦你們多使用這些成熟的技術和 jar 工具包,他能夠減小不少沒必要要的工做量。****咱們只須要把重心放到業務邏輯上。**而不會由於這些入參的判斷耽誤更多的時間。

如何優雅的設計jav異常

domain 介紹

根據項目場景來看,須要兩個 domain 模型,一個是用戶實體,一個是地址實體.

Address domain 以下:

@Entity
@Data
public class Address {
    @Id
    @GeneratedValue
    private Integer id;
    private String province;//省
    private String city;//市
    private String county;//區
    private Boolean isDefault;//是不是默認地址

    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn()
    private User user;
}


複製代碼

User domain 以下:

@Entity
@Data
public class User {
    @Id
   @GeneratedValue
   private Integer id;
   private String name;//姓名

    @OneToMany(cascade= CascadeType.ALL,mappedBy="user",fetch = FetchType.LAZY)
        private Set<Address> addresses;
}


複製代碼

ok, 上邊是一個模型關係,用戶 - 收貨地址的關係是 1-n 的關係。上邊的 @Data 是使用了一個叫作 lombok 的工具,它自動生成了 Setter 和 Getter 等方法,用起來很是方便,感興趣的讀者能夠自行了解一下。

dao 介紹

數據鏈接層,咱們使用了 spring-data-jpa 這個框架,它要求咱們只須要繼承框架提供的接口,而且按照約定對方法進行取名,就能夠完成咱們想要的數據庫操做。

用戶數據庫操做以下:

@Repository
public interface IUserDao extends JpaRepository<User,Integer> {

}


複製代碼

收貨地址操做以下:

@Repository
public interface IAddressDao extends JpaRepository<Address,Integer> {

}


複製代碼

正如讀者所看到的,咱們的 DAO 只須要繼承 JpaRepository, 它就已經幫咱們完成了基本的 CURD 等操做,若是想了解更多關於 spring-data 的這個項目,請參考一下 spring 的官方文檔,它比不方案咱們對異常的研究。

Service 異常設計

ok,終於到了咱們的重點了,咱們要完成 service 一些的部分操做: 添加收貨地址,刪除收貨地址,獲取收貨地址列表.

首先看個人 service 接口定義:

public interface IAddressService {

/**
 * 建立收貨地址
 * @param uid
 * @param address
 * @return
 */
Address createAddress(Integer uid,Address address);

/**
 * 刪除收貨地址
 * @param uid
 * @param aid
 */
void deleteAddress(Integer uid,Integer aid);

/**
 * 查詢用戶的全部收貨地址
 * @param uid
 * @return
 */
List<Address> listAddresses(Integer uid);
}


複製代碼

咱們來關注一下實現:

添加收貨地址

首先再來看一下以前整理的約束條件:

入參:

  • 用戶 id

  • 收貨地址實體信息

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址的必要字段不能爲空

  • 若是用戶尚未收貨地址,當此收貨地址建立時設置成默認收貨地址

先看如下代碼實現:

 @Override
public Address createAddress(Integer uid, Address address) {
    //============ 如下爲約束條件   ==============
    //1.用戶id不能爲空,且此用戶確實是存在的
    Preconditions.checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new RuntimeException("找不到當前用戶!");
    }
    //2.收貨地址的必要字段不能爲空
    BeanValidators.validateWithException(validator, address);
    //3.若是用戶尚未收貨地址,當此收貨地址建立時設置成默認收貨地址
    if(ObjectUtils.isEmpty(user.getAddresses())){
        address.setIsDefault(true);
    }

    //============ 如下爲正常執行的業務邏輯   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}


複製代碼

其中,已經完成了上述所描述的三點約束條件,當三點約束條件都知足時,才能夠進行正常的業務邏輯,不然將拋出異常 (通常在此處建議拋出運行時異常 - RuntimeException)。

介紹如下以上我所用到的技術:

一、Preconfitions.checkNotNull(T t) 這個是使用 Guava 中的 com.google.common.base.Preconditions 進行判斷的,由於 service 中用到的驗證較多,因此建議將 Preconfitions 改爲靜態導入的方式:

import static com.google.common.base.Preconditions.checkNotNull; 


複製代碼

固然 Guava 的 github 中的說明也建議咱們這樣使用。

二、BeanValidators.validateWithException(validator, address);
這個使用了 hibernate 實現的 jsr 303 規範來作的,須要傳入一個 validator 和一個須要驗證的實體, 那麼 validator 是如何獲取的呢, 以下:

@Configuration
public class BeanConfigs {

@Bean
public javax.validation.Validator getValidator(){
    return new LocalValidatorFactoryBean();
}
}


複製代碼

他將獲取一個 Validator 對象,而後咱們在 service 中進行注入即可以使用了:

 @Autowired     
private Validator validator ;


複製代碼

那麼 BeanValidators 這個類是如何實現的?其實實現方式很簡單,只要去判斷 jsr 303 的標註註解就 ok 了。

那麼 jsr 303 的註解寫在哪裏了呢?固然是寫在 address 實體類中了:

@Entity
@Setter
@Getter
public class Address {
@Id
    @GeneratedValue
    private Integer id;
    @NotNull
private String province;//省
@NotNull
private String city;//市
@NotNull
private String county;//區
private Boolean isDefault = false;//是不是默認地址

@ManyToOne(cascade={CascadeType.ALL})
@JoinColumn()
private User user;
}


複製代碼

寫好你須要的約束條件來進行判斷,若是合理的話,才能夠進行業務操做,從而對數據庫進行操做。

這塊的驗證是必須的,一個最主要的緣由是: 這樣的驗證能夠避免髒數據的插入。

若是讀者有正式上線的經驗的話,就能夠理解這樣的一個事情,**任何的代碼錯誤均可以容忍和修改,可是若是出現了髒數據問題,那麼它有多是一個毀滅性的災難。**程序的問題能夠修改,可是髒數據的出現有可能沒法恢復。因此這就是爲何在 service 中必定要判斷好約束條件,再進行業務邏輯操做的緣由了。

此處的判斷爲業務邏輯判斷,是從業務角度來進行篩選判斷的,除此以外,有可能在不少場景中都會有不一樣的業務條件約束,只須要按照要求來作就好。

對於約束條件的總結以下:

  • 基本判斷約束 (null 值等基本判斷)

  • 實體屬性約束 (知足 jsr 303 等基礎判斷)

  • 業務條件約束 (需求提出的不一樣的業務約束)

當這個三點都知足時,才能夠進行下一步操做

ok, 基本介紹瞭如何作一個基礎的判斷,那麼再回到異常的設計問題上,上述代碼已經很清楚的描述如何在適當的位置合理的判斷一個異常了,那麼如何合理的拋出異常呢?

只拋出 RuntimeException 就算是優雅的拋出異常嗎?固然不是,對於 service 中的拋出異常,筆者認爲大體有兩種拋出的方法:

  • 拋出帶狀態碼 RumtimeException 異常

  • 拋出指定類型的 RuntimeException 異常

相對這兩種異常的方式進行結束,第一種異常指的是我全部的異常都拋 RuntimeException 異常,可是須要帶一個狀態碼,調用者能夠根據狀態碼再去查詢究竟 service 拋出了一個什麼樣的異常。

第二種異常是指在 service 中拋出什麼樣的異常就自定義一個指定的異常錯誤,而後在進行拋出異常。

通常來說,若是系統沒有別的特殊需求的時候,在開發設計中,建議使用第二種方式。可是好比說像基礎判斷的異常,就能夠徹底使用 guava 給咱們提供的類庫進行操做。jsr 303 異常也可使用本身封裝好的異常判斷類進行操做,由於這兩種異常都是屬於基礎判斷,不須要爲它們指定特殊的異常。可是對於第三點義務條件約束判斷拋出的異常,就須要拋出指定類型的異常了。

對於

throw new RuntimeException("找不到當前用戶!");


複製代碼

定義一個特定的異常類來進行這個義務異常的判斷:

public class NotFindUserException extends RuntimeException {
public NotFindUserException() {
    super("找不到此用戶");
}

public NotFindUserException(String message) {
    super(message);
}
}


複製代碼

而後將此處改成:

throw new NotFindUserException("找不到當前用戶!");
or

throw new NotFindUserException();


複製代碼

ok, 經過以上對 service 層的修改,代碼更改以下:

@Override
public Address createAddress(Integer uid, Address address) {
    //============ 如下爲約束條件   ==============
    //1.用戶id不能爲空,且此用戶確實是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException("找不到當前用戶!");
    }
    //2.收貨地址的必要字段不能爲空
    BeanValidators.validateWithException(validator, address);
    //3.若是用戶尚未收貨地址,當此收貨地址建立時設置成默認收貨地址
    if(ObjectUtils.isEmpty(user.getAddresses())){
        address.setIsDefault(true);
    }

    //============ 如下爲正常執行的業務邏輯   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}


複製代碼

這樣的 service 就看起來穩定性和理解性就比較強了。

刪除收貨地址:

入參:

  • 用戶 id

  • 收貨地址 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

  • 收貨地址不能爲空,且此收貨地址確實是存在的

  • 判斷此收貨地址是不是用戶的收貨地址

  • 判斷此收貨地址是否爲默認收貨地址,若是是默認收貨地址,那麼不能進行刪除

它與上述添加收貨地址相似,故再也不贅述,delete 的 service 設計以下:

@Override
public void deleteAddress(Integer uid, Integer aid) {
    //============ 如下爲約束條件   ==============
    //1.用戶id不能爲空,且此用戶確實是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException();
    }
    //2.收貨地址不能爲空,且此收貨地址確實是存在的
    checkNotNull(aid);
    Address address = addressDao.findOne(aid);
    if(null == address){
        throw new NotFindAddressException();
    }
    //3.判斷此收貨地址是不是用戶的收貨地址
    if(!address.getUser().equals(user)){
        throw new NotMatchUserAddressException();
    }
    //4.判斷此收貨地址是否爲默認收貨地址,若是是默認收貨地址,那麼不能進行刪除
    if(address.getIsDefault()){
       throw  new DefaultAddressNotDeleteException();
    }

    //============ 如下爲正常執行的業務邏輯   ==============
    addressDao.delete(address);
}


複製代碼

設計了相關的四個異常類:
NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException.
根據不一樣的業務需求拋出不一樣的異常。

獲取收貨地址列表:
入參:

  • 用戶 id

約束:

  • 用戶 id 不能爲空,且此用戶確實是存在的

代碼以下:

 @Override
public List<Address> listAddresses(Integer uid) {
    //============ 如下爲約束條件   ==============
    //1.用戶id不能爲空,且此用戶確實是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException();
    }

    //============ 如下爲正常執行的業務邏輯   ==============
    User result = userDao.findOne(uid);
    return result.getAddresses();
}


複製代碼

api 異常設計

大體有兩種拋出的方法:

  • 拋出帶狀態碼 RumtimeException 異常

  • 拋出指定類型的 RuntimeException 異常

這個是在設計 service 層異常時提到的,經過對 service 層的介紹,咱們在 service 層拋出異常時選擇了第二種拋出的方式,不一樣的是,在 api 層拋出異常咱們須要使用這兩種方式進行拋出: 要指定 api 異常的類型,而且要指定相關的狀態碼,而後纔將異常拋出,這種異常設計的核心是讓調用 api 的使用者更能清楚的瞭解發生異常的詳細信息。

除了拋出異常外,咱們還須要將狀態碼對應的異常詳細信息以及異常有可能發生的問題製做成一個對應的表展現給用戶,方便用戶的查詢。(如 github 提供的 api 文檔,微信提供的 api 文檔等), 還有一個好處: 若是用戶須要自定義提示消息,能夠根據返回的狀態碼進行提示的修改。

api 驗證約束

首先對於 api 的設計來講,須要存在一個 dto 對象,這個對象負責和調用者進行數據的溝通和傳遞,而後 dto->domain 在傳給 service 進行操做,這一點必定要注意。

第二點,除了說道的 service 須要進行基礎判斷 (null 判斷) 和 jsr 303 驗證之外,一樣的,api 層也須要進行相關的驗證,若是驗證不經過的話,直接返回給調用者,告知調用失敗,不該該帶着不合法的數據再進行對 service 的訪問。

那麼讀者可能會有些迷惑,不是 service 已經進行驗證了,爲何 api 層還須要進行驗證麼?這裏便設計到了一個概念: 編程中的墨菲定律,若是 api 層的數據驗證疏忽了,那麼有可能不合法數據就帶到了 service 層,進而講髒數據保存到了數據庫。

因此縝密編程的核心是: 永遠不要相信收到的數據是合法的。

api 異常設計

設計 api 層異常時,正如咱們上邊所說的,須要提供錯誤碼和錯誤信息,那麼能夠這樣設計,提供一個通用的 api 超類異常,其餘不一樣的 api 異常都繼承自這個超類:

public class ApiException extends RuntimeException {
protected Long errorCode ;
protected Object data ;

public ApiException(Long errorCode,String message,Object data,Throwable e){
    super(message,e);
    this.errorCode = errorCode ;
    this.data = data ;
}

public ApiException(Long errorCode,String message,Object data){
    this(errorCode,message,data,null);
}

public ApiException(Long errorCode,String message){
    this(errorCode,message,null,null);
}

public ApiException(String message,Throwable e){
    this(null,message,null,e);
}

public ApiException(){

}

public ApiException(Throwable e){
    super(e);
}

public Long getErrorCode() {
    return errorCode;
}

public void setErrorCode(Long errorCode) {
    this.errorCode = errorCode;
}

public Object getData() {
    return data;
}

public void setData(Object data) {
    this.data = data;
}
}


複製代碼

而後分別定義 api 層異常:
ApiDefaultAddressNotDeleteException,ApiNotFindAddressException,ApiNotFindUserException,ApiNotMatchUserAddressException
以默認地址不能刪除爲例:

public class ApiDefaultAddressNotDeleteException extends ApiException {

public ApiDefaultAddressNotDeleteException(String message) {
    super(AddressErrorCode.DefaultAddressNotDeleteErrorCode, message, null);
}
}


複製代碼

AddressErrorCode.DefaultAddressNotDeleteErrorCode
就是須要提供給調用者的錯誤碼。錯誤碼類以下:

public abstract class AddressErrorCode {
    public static final Long DefaultAddressNotDeleteErrorCode = 10001L;//默認地址不能刪除
    public static final Long NotFindAddressErrorCode = 10002L;//找不到此收貨地址
    public static final Long NotFindUserErrorCode = 10003L;//找不到此用戶
    public static final Long NotMatchUserAddressErrorCode = 10004L;//用戶與收貨地址不匹配
}


複製代碼

ok, 那麼 api 層的異常就已經設計完了,在此多說一句,AddressErrorCode 錯誤碼類存放了可能出現的錯誤碼,更合理的作法是把他放到配置文件中進行管理。

api 處理異常

api 層會調用 service 層,而後來處理 service 中出現的全部異常,首先,須要保證一點,必定要讓 api 層很是輕,基本上作成一個轉發的功能就好 (接口參數,傳遞給 service 參數,返回給調用者數據, 這三個基本功能),而後就要在傳遞給 service 參數的那個方法調用上進行異常處理。

此處僅以添加地址爲例:

 @Autowired
private IAddressService addressService;


/**
 * 添加收貨地址
 * @param addressDTO
 * @return
 */
@RequestMapping(method = RequestMethod.POST)
public AddressDTO add(@Valid @RequestBody AddressDTO addressDTO){
    Address address = new Address();
    BeanUtils.copyProperties(addressDTO,address);
    Address result;
    try {
        result = addressService.createAddress(addressDTO.getUid(), address);
    }catch (NotFindUserException e){
        throw new ApiNotFindUserException("找不到該用戶");
    }catch (Exception e){//未知錯誤
        throw new ApiException(e);
    }
    AddressDTO resultDTO = new AddressDTO();
    BeanUtils.copyProperties(result,resultDTO);
    resultDTO.setUid(result.getUser().getId());

    return resultDTO;
}


複製代碼

這裏的處理方案是調用 service 時,判斷異常的類型,而後將任何 service 異常都轉化成 api 異常,而後拋出 api 異常,這是經常使用的一種異常轉化方式。類似刪除收貨地址和獲取收貨地址也相似這樣處理,在此,不在贅述。

api 異常轉化

已經講解了如何拋出異常和何如將 service 異常轉化爲 api 異常,那麼轉化成 api 異常直接拋出是否就完成了異常處理呢?答案是否認的,當拋出 api 異常後,咱們須要把 api 異常返回的數據 (json or xml) 讓用戶看懂,那麼須要把 api 異常轉化成 dto 對象(ErrorDTO), 看以下代碼:

@ControllerAdvice(annotations = RestController.class)
class ApiExceptionHandlerAdvice {

/**
 * Handle exceptions thrown by handlers.
 */
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseEntity<ErrorDTO> exception(Exception exception,HttpServletResponse response) {
    ErrorDTO errorDTO = new ErrorDTO();
    if(exception instanceof ApiException){//api異常
        ApiException apiException = (ApiException)exception;
        errorDTO.setErrorCode(apiException.getErrorCode());
    }else{//未知異常
        errorDTO.setErrorCode(0L);
    }
    errorDTO.setTip(exception.getMessage());
    ResponseEntity<ErrorDTO> responseEntity = new ResponseEntity<>(errorDTO,HttpStatus.valueOf(response.getStatus()));
    return responseEntity;
}

@Setter
@Getter
class ErrorDTO{
    private Long errorCode;
    private String tip;
}
}


複製代碼

ok, 這樣就完成了 api 異常轉化成用戶能夠讀懂的 DTO 對象了,代碼中用到了 @ControllerAdvice,這是 spring MVC 提供的一個特殊的切面處理。

當調用 api 接口發生異常時,用戶也能夠收到正常的數據格式了, 好比當沒有用戶 (uid 爲 2) 時,卻爲這個用戶添加收貨地址, postman(Google plugin 用於模擬 http 請求)以後的數據:

{
  "errorCode": 10003,
  "tip": "找不到該用戶"
}


複製代碼

總結

本文只從如何設計異常做爲重點來說解,涉及到的 api 傳輸和 service 的處理,還有待優化,好比 api 接口訪問須要使用 https 進行加密,api 接口須要 OAuth2.0 受權或 api 接口須要簽名認證等問題,文中都不曾提到,本文的重心在於異常如何處理,因此讀者只需關注涉及到異常相關的問題和處理方式就能夠了。

推薦閱讀

Java筆記大全.md

太讚了,這個Java網站,什麼項目都有!https://markerhub.com

這個B站的UP主,講的java真不錯!

太讚了!最新版Java編程思想能夠在線看了!

相關文章
相關標籤/搜索