都是程序員呜魄,設計Java異常的時候要優(yōu)雅

導語

異常處理是程序開發(fā)中必不可少操作之一,但如何正確優(yōu)雅的對異常進行處理確是一門學問构韵,筆者根據自己的開發(fā)經驗來談一談我是如何對異常進行處理的周蹭。

由于本文只作一些經驗之談,不涉及到基礎知識部分疲恢,如果讀者對異常的概念還很模糊凶朗,請先查看基礎知識。

如何選擇異常類型

異常的類別

正如我們所知道的显拳,java中的異常的超類是java.lang.Throwable(后文省略為Throwable),它有兩個比較重要的子類,java.lang.Exception(后文省略為Exception)和java.lang.Error(后文省略為Error)棚愤,其中Error由JVM虛擬機進行管理,如我們所熟知的OutOfMemoryError異常等,所以我們本文不關注Error異常杂数,那么我們細說一下Exception異常宛畦。

Exception異常有個比較重要的子類,叫做RuntimeException揍移。我們將RuntimeException或其他繼承自RuntimeException的子類稱為非受檢異常(unchecked Exception)次和,其他繼承自Exception異常的子類稱為受檢異常(checked Exception)。本文重點來關注一下受檢異常和非受檢異常這兩種異常那伐。

如何選擇異常

從筆者的開發(fā)經驗來看踏施,如果在一個應用中,需要開發(fā)一個方法(如某個功能的service方法)罕邀,這個方法如果中間可能出現異常畅形,那么你需要考慮這個異常出現之后是否調用者可以處理,并且你是否希望調用者進行處理诉探,如果調用者可以處理束亏,并且你也希望調用者進行處理,那么就要拋出受檢異常阵具,提醒調用者在使用你的方法時,考慮到如果拋出異常時如果進行處理定铜,相似的阳液,如果在寫某個方法時,你認為這是個偶然異常揣炕,理論上說帘皿,你覺得運行時可能會碰到什么問題,而這些問題也許不是必然發(fā)生的畸陡,也不需要調用者顯示的通過異常來判斷業(yè)務流程操作的鹰溜,那么這時就可以使用一個RuntimeException這樣的非受檢異常.

好了虽填,估計我上邊說的這段話,你讀了很多遍也依然覺得晦澀了曹动。
那么斋日,請跟著我的思路,在慢慢領會一下墓陈。

什么時候才需要拋異常

首先我們需要了解一個問題恶守,什么時候才需要拋異常?異常的設計是方便給開發(fā)者使用的贡必,但不是亂用的兔港,筆者對于什么時候拋異常這個問題也問了很多朋友,能給出準確答案的確實不多仔拟。其實這個問題很簡單衫樊,如果你覺得某些”問題”解決不了了,那么你就可以拋出異常了利花。比如科侈,你在寫一個service,其中在寫到某段代碼處,你發(fā)現可能會產生問題,那么就請拋出異常吧晋被,相信我兑徘,你此時拋出異常將是一個最佳時機。

應該拋出怎樣的異常

了解完了什么時候才需要拋出異常后羡洛,我們再思考一個問題挂脑,真的當我們拋出異常時,我們應該選用怎樣的異常呢欲侮?究竟是受檢異常還是非受檢異常呢(RuntimeException)呢崭闲?我來舉例說明一下這個問題,先從受檢異常說起,比如說有這樣一個業(yè)務邏輯威蕉,需要從某文件中讀取某個數據刁俭,這個讀取操作可能是由于文件被刪除等其他問題導致無法獲取從而出現讀取錯誤,那么就要從redis或mysql數據庫中再去獲取此數據,參考如下代碼韧涨,getKey(Integer)為入口程序.


image.png

ok牍戚,看了以上代碼以后,你也許心中有一些想法虑粥,原來受檢異橙缧ⅲ可以控制義務邏輯,對娩贷,沒錯第晰,通過受檢異常真的可以控制業(yè)務邏輯,但是切記不要這樣使用,我們應該合理的拋出異常茁瘦,因為程序本身才是流程品抽,異常的作用僅僅是當你進行不下去的時候找到的一個借口而已,它并不能當成控制程序流程的入口或出口甜熔,如果這樣使用的話圆恤,是在將異常的作用擴大化,這樣將會導致代碼復雜程度的增加纺非,耦合性會提高哑了,代碼可讀性降低等問題。那么就一定不要使用這樣的異常嗎烧颖?其實也不是弱左,在真的有這樣的需求的時候,我們可以這樣使用炕淮,只是切記拆火,不要把它真的當成控制流程的工具或手段。那么究竟什么時候才要拋出這樣的異常呢涂圆?要考慮们镜,如果調用者調用出錯后,一定要讓調用者對此錯誤進行處理才可以润歉,滿足這樣的要求時模狭,我們才會考慮使用受檢異常。

接下來踩衩,我們來看一下非受檢異常呢(RuntimeException)嚼鹉,對于RuntimeException這種異常,我們其實很多見驱富,比如java.lang.NullPointerException/java.lang.IllegalArgumentException等锚赤,那么這種異常我們時候拋出呢?當我們在寫某個方法的時候褐鸥,可能會偶然遇到某個錯誤线脚,我們認為這個問題時運行時可能為發(fā)生的,并且理論上講叫榕,沒有這個問題的話浑侥,程序將會正常執(zhí)行的時候,它不強制要求調用者一定要捕獲這個異常晰绎,此時拋出RuntimeException異常,舉個例子锭吨,當傳來一個路徑的時候,需要返回一個路徑對應的File對象:


image.png

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

應該選用哪種異常

通過以上的描述和舉例锄弱,可以總結出一個結論考蕾,RuntimeException異常和受檢異常之間的區(qū)別就是:是否強制要求調用者必須處理此異常,如果強制要求調用者必須進行處理会宪,那么就使用受檢異常肖卧,否則就選擇非受檢異常(RuntimeException)。一般來講掸鹅,如果沒有特殊的要求塞帐,我們建議使用RuntimeException異常。

場景介紹和技術選型

架構描述

正如我們所知巍沙,傳統(tǒng)的項目都是以MVC框架為基礎進行開發(fā)的葵姥,本文主要從使用restful風格接口的設計來體驗一下異常處理的優(yōu)雅。

我們把關注點放在restful的api層(和web中的controller層類似)和service層句携,研究一下在service中如何拋出異常榔幸,然后api層如何進行捕獲并且轉化異常。

使用的技術是:spring-boot,jpa(hibernate),mysql,如果對這些技術不是太熟悉矮嫉,讀者需要自行閱讀相關材料削咆。

業(yè)務場景描述

選擇一個比較簡單的業(yè)務場景,以電商中的收貨地址管理為例蠢笋,用戶在移動端進行購買商品時拨齐,需要進行收貨地址管理,在項目中昨寞,提供一些給移動端進行訪問的api接口瞻惋,如:添加收貨地址,刪除收貨地址编矾,更改收貨地址熟史,默認收貨地址設置,收貨地址列表查詢窄俏,單個收貨地址查詢等接口蹂匹。

構建約束條件

ok,這個是設置好的一個很基本的業(yè)務場景凹蜈,當然限寞,無論什么樣的api操作,其中都包含一些規(guī)則:

添加收貨地址:
入參:

  • 用戶id
  • 收貨地址實體信息

約束:

  • 用戶id不能為空仰坦,且此用戶確實是存在 的
  • 收貨地址的必要字段不能為 空
  • 如果用戶還沒有收貨地址履植,當此收貨地址創(chuàng)建時設置成默認收貨地址 —

刪除收貨地址:
入參:

  • 用戶id
  • 收貨地址id

約束:

  • 用戶id不能為空,且此用戶確實是存在的
  • 收貨地址不能為空悄晃,且此收貨地址確實是存在的
  • 判斷此收貨地址是否是用戶的收貨地址
  • 判斷此收貨地址是否為默認收貨地址玫霎,如果是默認收貨地址凿滤,那么不能進行刪除

更改收貨地址:
入參:

  • 用戶id
  • 收貨地址id

約束:

  • 用戶id不能為空,且此用戶確實是存在的
  • 收貨地址不能為空庶近,且此收貨地址確實是存在的
  • 判斷此收貨地址是否是用戶的收貨地址

默認地址設置:

入參:

  • 用戶id
  • 收貨地址id

約束:

  • 用戶id不能為空翁脆,且此用戶確實是存在的
  • 收貨地址不能為空,且此收貨地址確實是存在的
  • 判斷此收貨地址是否是用戶的收貨地址

收貨地址列表查詢:

入參:

  • 用戶id

約束:

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

單個收貨地址查詢:

入參:

  • 用戶id
  • 收貨地址id

約束:

  • 用戶id不能為空反番,且此用戶確實是存在的
  • 收貨地址不能為空,且此收貨地址確實是存在的
  • 判斷此收貨地址是否是用戶的收貨地址

約束判斷和技術選型

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

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

添加收貨地址中需要對用戶id和收貨地址實體信息就行校驗,那么對于非空的判斷蛾洛,我們如何進行工具的選擇呢养铸?傳統(tǒng)的判斷如下:


image.png

上邊的例子,如果只判斷uid為空還好轧膘,如果再去判斷address這個實體中的某些必要屬性是否為空钞螟,在字段很多的情況下,這無非是災難性的谎碍。

那我們應該怎么進行這些入參的判斷呢鳞滨,給大家介紹兩個知識點:

  1. Guava中的Preconditions類實現了很多入參方法的判斷
  2. jsr 303的validation規(guī)范(目前實現比較全的是hibernate實現的hibernate-validator)

如果使用了這兩種推薦技術,那么入參的判斷會變得簡單很多蟆淀。推薦大家多使用這些成熟的技術和jar工具包拯啦,他可以減少很多不必要的工作量。我們只需要把重心放到業(yè)務邏輯上熔任。而不會因為這些入參的判斷耽誤更多的時間褒链。

如何優(yōu)雅的設計java異常

domain介紹

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

Address domain如下:


image.png

User domain如下:


image.png

ok,上邊是一個模型關系,用戶-收貨地址的關系是1-n的關系惦费。上邊的@Data是使用了一個叫做lombok的工具兵迅,它自動生成了Setter和Getter等方法,用起來非常方便薪贫,感興趣的讀者可以自行了解一下恍箭。

dao介紹

數據連接層,我們使用了spring-data-jpa這個框架瞧省,它要求我們只需要繼承框架提供的接口扯夭,并且按照約定對方法進行取名鳍贾,就可以完成我們想要的數據庫操作。

用戶數據庫操作如下:


image.png

收貨地址操作如下:


image.png

正如讀者所看到的交洗,我們的DAO只需要繼承JpaRepository,它就已經幫我們完成了基本的CURD等操作贾漏,如果想了解更多關于spring-data的這個項目,請參考一下spring的官方文檔藕筋,它比不方案我們對異常的研究。

Service異常設計

ok梳码,終于到了我們的重點了隐圾,我們要完成service一些的部分操作:添加收貨地址,刪除收貨地址掰茶,獲取收貨地址列表.

首先看我的service接口定義:


image.png

我們來關注一下實現:

添加收貨地址
首先再來看一下之前整理的約束條件:

入參:

  • 用戶id
  • 收貨地址實體信息

約束:

  • 用戶id不能為空暇藏,且此用戶確實是存在的
  • 收貨地址的必要字段不能為空
  • 如果用戶還沒有收貨地址,當此收貨地址創(chuàng)建時設置成默認收貨地址

先看以下代碼實現:


image.png

其中濒蒋,已經完成了上述所描述的三點約束條件盐碱,當三點約束條件都滿足時,才可以進行正常的業(yè)務邏輯沪伙,否則將拋出異常(一般在此處建議拋出運行時異常-RuntimeException)瓮顽。

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

1. Preconfitions.checkNotNull(T t)這個是使用Guava中的com.google.common.base.Preconditions進行判斷的,因為service中用到的驗證較多围橡,所以建議將Preconfitions改成靜態(tài)導入的方式:

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

當然Guava的github中的說明也建議我們這樣使用暖混。

2. BeanValidators.validateWithException(validator, address);

這個使用了hibernate實現的jsr 303規(guī)范來做的,需要傳入一個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;//區(qū)

private Boolean isDefault = false;//是否是默認地址

 

@ManyToOne(cascade={CascadeType.ALL})

@JoinColumn(name="uid")

private User user;

}

寫好你需要的約束條件來進行判斷,如果合理的話塞赂,才可以進行業(yè)務操作泪勒,從而對數據庫進行操作。

這塊的驗證是必須的减途,一個最主要的原因是:這樣的驗證可以避免臟數據的插入酣藻。如果讀者有正式上線的經驗的話,就可以理解這樣的一個事情鳍置,任何的代碼錯誤都可以容忍和修改辽剧,但是如果出現了臟數據問題,那么它有可能是一個毀滅性的災難税产。程序的問題可以修改怕轿,但是臟數據的出現有可能無法恢復偷崩。所以這就是為什么在service中一定要判斷好約束條件,再進行業(yè)務邏輯操作的原因了撞羽。

此處的判斷為業(yè)務邏輯判斷阐斜,是從業(yè)務角度來進行篩選判斷的,除此之外诀紊,有可能在很多場景中都會有不同的業(yè)務條件約束谒出,只需要按照要求來做就好。

對于約束條件的總結如下:

  1. 基本判斷約束(null值等基本判斷)
  2. 實體屬性約束(滿足jsr 303等基礎判斷)
  3. 業(yè)務條件約束(需求提出的不同的業(yè)務約束)

當這個三點都滿足時邻奠,才可以進行下一步操作

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

只拋出RuntimeException就算是優(yōu)雅的拋出異常嗎?當然不是,對于service中的拋出異常,筆者認為大致有兩種拋出的方法:

  1. 拋出帶狀態(tài)碼RumtimeException異常
  2. 拋出指定類型的RuntimeException異常

相對這兩種異常的方式進行結束腐晾,第一種異常指的是我所有的異常都拋RuntimeException異常芥颈,但是需要帶一個狀態(tài)碼,調用者可以根據狀態(tài)碼再去查詢究竟service拋出了一個什么樣的異常。

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

一般來講子姜,如果系統(tǒng)沒有別的特殊需求的時候,在開發(fā)設計中楼入,建議使用第二種方式哥捕。但是比如說像基礎判斷的異常,就可以完全使用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.如果用戶還沒有收貨地址,當此收貨地址創(chuàng)建時設置成默認收貨地址

    if(ObjectUtils.isEmpty(user.getAddresses())){

        address.setIsDefault(true);

    }

 

    //============ 以下為正常執(zhí)行的業(yè)務邏輯   ==============

    address.setUser(user);

    Address result = addressDao.save(address);

    return result;

}

這樣的service就看起來穩(wěn)定性和理解性就比較強了衫画。

刪除收貨地址:

入參:

  • 用戶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();

    }

 

    //============ 以下為正常執(zhí)行的業(yè)務邏輯   ==============

    addressDao.delete(address);

}

設計了相關的四個異常類:NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException.根據不同的業(yè)務需求拋出不同的異常缨历。

獲取收貨地址列表:

入參:

  • 用戶id

約束:

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

代碼如下:

@Override

public List<Address> listAddresses(Integer uid) {

    //============ 以下為約束條件   ==============

    //1.用戶id不能為空糙麦,且此用戶確實是存在的

    checkNotNull(uid);

    User user = userDao.findOne(uid);

    if(null == user){

        throw new NotFindUserException();

    }

 

    //============ 以下為正常執(zhí)行的業(yè)務邏輯   ==============

    User result = userDao.findOne(uid);

    return result.getAddresses();

}

api異常設計

大致有兩種拋出的方法:

  • 拋出帶狀態(tài)碼RumtimeException異常
  • 拋出指定類型的RuntimeException異常

這個是在設計service層異常時提到的戈二,通過對service層的介紹,我們在service層拋出異常時選擇了第二種拋出的方式喳资,不同的是,在api層拋出異常我們需要使用這兩種方式進行拋出:要指定api異常的類型腾供,并且要指定相關的狀態(tài)碼仆邓,然后才將異常拋出,這種異常設計的核心是讓調用api的使用者更能清楚的了解發(fā)生異常的詳細信息伴鳖,除了拋出異常外节值,我們還需要將狀態(tài)碼對應的異常詳細信息以及異常有可能發(fā)生的問題制作成一個對應的表展示給用戶,方便用戶的查詢榜聂。(如github提供的api文檔搞疗,微信提供的api文檔等),還有一個好處:如果用戶需要自定義提示消息,可以根據返回的狀態(tài)碼進行提示的修改须肆。

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層非常輕钝凶,基本上做成一個轉發(fā)的功能就好(接口參數仪芒,傳遞給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接口發(fā)生異常時,用戶也可以收到正常的數據格式了,比如當沒有用戶(uid為2)時誉结,卻為這個用戶添加收貨地址,postman(Google plugin 用于模擬http請求)之后的數據:

{

  "errorCode": 10003,

  "tip": "找不到該用戶"

}

歡迎大家加入粉絲群:963944895鹅士,群內免費分享Spring框架、Mybatis框架SpringBoot框架惩坑、SpringMVC框架掉盅、SpringCloud微服務、Dubbo框架以舒、Redis緩存趾痘、RabbitMq消息、JVM調優(yōu)蔓钟、Tomcat容器永票、MySQL數據庫教學視頻及架構學習思維導圖

總結

本文只從如何設計異常作為重點來講解,涉及到的api傳輸和service的處理滥沫,還有待優(yōu)化瓦侮,比如api接口訪問需要使用https進行加密,api接口需要OAuth2.0授權或api接口需要簽名認證等問題佣谐,文中都未曾提到,本文的重心在于異常如何處理方妖,所以讀者只需關注涉及到異常相關的問題和處理方式就可以了狭魂。希望本篇文章對你理解異常有所幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末党觅,一起剝皮案震驚了整個濱河市雌澄,隨后出現的幾起案子,更是在濱河造成了極大的恐慌杯瞻,老刑警劉巖镐牺,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異魁莉,居然都是意外死亡睬涧,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門旗唁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畦浓,“玉大人,你說我怎么就攤上這事检疫⊙惹耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵屎媳,是天一觀的道長夺溢。 經常有香客問我论巍,道長,這世上最難降的妖魔是什么风响? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任嘉汰,我火速辦了婚禮,結果婚禮上钞诡,老公的妹妹穿的比我還像新娘郑现。我一直安慰自己,他們只是感情好荧降,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布接箫。 她就那樣靜靜地躺著,像睡著了一般朵诫。 火紅的嫁衣襯著肌膚如雪辛友。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天剪返,我揣著相機與錄音废累,去河邊找鬼。 笑死脱盲,一個胖子當著我的面吹牛邑滨,可吹牛的內容都是我干的。 我是一名探鬼主播钱反,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼掖看,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了面哥?” 一聲冷哼從身側響起哎壳,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尚卫,沒想到半個月后归榕,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡吱涉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年刹泄,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怎爵。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡循签,死狀恐怖,靈堂內的尸體忽然破棺而出疙咸,到底是詐尸還是另有隱情县匠,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站乞旦,受9級特大地震影響贼穆,放射性物質發(fā)生泄漏。R本人自食惡果不足惜兰粉,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一故痊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧玖姑,春花似錦愕秫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至闪彼,卻和暖如春甜孤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背畏腕。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工缴川, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人描馅。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓把夸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铭污。 傳聞我的和親對象是個殘疾皇子恋日,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容