我的公眾號:MarkerHub,網站:https://markerhub.com更多精選文章請點擊:Java筆記大全.md
小Hub領讀:
作者以增刪改查收貨地址為實例,詳細説明了如何去設計一個好的異常處理,包括使用Guava中的Preconditions、hibernate的hibernate-validator,還有如何異常和處理異常的邏輯,文章有點長,看完還是收穫挺大!
- 來源:lrwinx
- https://lrwinx.github.io/
導語
異常處理是程序開發中必不可少操作之一,但如何正確優雅的對異常進行處理確是一門學問,筆者根據自己的開發經驗來談一談我是如何對異常進行處理的。
由於本文只作一些經驗之談,不涉及到基礎知識部分,如果讀者對異常的概念還很模糊,請先查看基礎知識。
如何選擇異常類型
異常的類別
正如我們所知道的,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)。本文重點來關注一下受檢異常和非受檢異常這兩種異常。
如何選擇異常
從筆者的開發經驗來看,如果在一個應用中,需要開發一個方法 (如某個功能的 service 方法),這個方法如果中間可能出現異常,那麼你需要考慮這個異常出現之後是否調用者可以處理,並且你是否希望調用者進行處理,如果調用者可以處理,並且你也希望調用者進行處理,那麼就要拋出受檢異常,提醒調用者在使用你的方法時,考慮到如果拋出異常時如果進行處理。
相似的,如果在寫某個方法時,你認為這是個偶然異常,理論上説,你覺得運行時可能會碰到什麼問題,而這些問題也許不是必然發生的,也不需要調用者顯示的通過異常來判斷業務流程操作的,那麼這時就可以使用一個 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)。
介紹以下以上我所用到的技術:
1、Preconfitions.checkNotNull(T t) 這個是使用 Guava 中的 com.google.common.base.Preconditions 進行判斷的,因為 service 中用到的驗證較多,所以建議將 Preconfitions 改成靜態導入的方式:
import static com.google.common.base.Preconditions.checkNotNull;
當然 Guava 的 github 中的説明也建議我們這樣使用。
2、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編程思想可以在線看了!