優(yōu)雅處理你的Java異常

作者:葉知泉

來源:https://my.oschina.net/c5ms/blog/1827907

本文介紹

本文僅按照業(yè)務系統(tǒng)開發(fā)角度描述異常的一些處理看法榜聂,不涉及java的異常基礎知識,可以自行查閱?《Java核心技術 卷I》?和?《java編程思想》?可以得到更多的基礎信息嗓蘑。

寫在前面的話

筆者文筆功力尚淺,言語多有不妥,請慷慨指正,必定感激不盡.?本文提出了幾個概念:?處理反饋须肆;業(yè)務異常;代碼錯誤桩皿。

請認真思考一下各中區(qū)別豌汇。

在開發(fā)業(yè)務系統(tǒng)中,我們目前絕大多數(shù)采用MVC模式,但是往往有人把service跟controller緊緊的耦合在一起,甚至直接使用Threadlocal來隱式傳值,并且復雜的邏輯幾乎只能使用service中存儲的全局對象來傳遞處理結果,包括異常。

這樣一來首先有違MVC模式,二來邏輯十分不清晰,難以維護泄隔。本文結合工作經(jīng)驗,給出一些異常使用建議,使用spring來實戰(zhàn)異常為我們帶來的好處拒贱。

常常,我們讀罷了各種java的書,異常的各種機制,特性都很清楚,但是始終還是不知道如何使用,甚至背下了概念,卻不知道如何致用。

我們開發(fā)的業(yè)務系統(tǒng),或者是產(chǎn)品,常常面臨著這樣的問題:

系統(tǒng)運行出錯,但是完全不知道錯誤發(fā)生的位置

我們找到了錯誤的位置,但是完全不知道是因為什么

系統(tǒng)明明出了錯誤,但是就是看不到錯誤堆棧信息

什么情況需要自定義異常

經(jīng)撤疰遥看到一些項目,在全局定義一個 AppException,然后所有地方都只拋出這個異常,并且把捕獲的異常case到這個AppException中.會有如下問題:

浪費log日志存儲空間,并且棧頂并不是最接近發(fā)生異常的代碼位置

只有一種異常類,無法精準區(qū)分開異常類型

異常類后期難以修改以增加其攜帶的信息

什么情況需要手動處理異常

我不會把書上的東西直接復制下來,這里說一下容易記住的,并且適合業(yè)務開發(fā)的柜思。

你有能力處理異常,并且你知道如何處理

你有責任處理異常

自定義業(yè)務異常

考慮如下場景: 系統(tǒng)提供一個API,用于修改用戶信息,服務器端采用json數(shù)據(jù)交互.首先我們定義ServiceException,用來表示業(yè)務邏輯受理失敗,它僅表示我們處理業(yè)務的時候發(fā)現(xiàn)無法繼續(xù)執(zhí)行下去岩调。

/**

* 業(yè)務受理失敗異常

*/

public?class?ServiceException?extends?RuntimeException?{

? ?//接收reason參數(shù)用來描述業(yè)務失敗原因.

?public?ServiceException(String?reason)?{?super(reason);?}

}

接下來看下Controller層

// UserController.java

/**

? * 修改用戶信息

? * @param userID 用戶ID

? * @param user 修改用戶信息表單數(shù)據(jù)

? */

?@PutMapping("{userID}")

?public?JSONResult?updateUser(@PathVariable("userID")?Integer?userID,?@RequestBody?UpdateUserForm?userForm)?{

? ?User?user?=?new?User();?//準備業(yè)務邏輯層使用的領域模型

? ?BeanUtils.copyProperties(userForm,?user);?//拷貝要修改的值

? ?user.setUserId(userID);?//設置主鍵到用戶數(shù)據(jù)中

? ?userService.updateUser(user);?//調(diào)用更新業(yè)務邏輯

? ?JSONResult?json?=?new?JSONResult();?//準備要響應的數(shù)據(jù)

? ?json.put("user",?user);?//把修改后的用戶數(shù)據(jù)還給頁面

? ?return?json;?// -- ?

?}

關于上述Controller寫法乍一看會有一些冗余,如果無法理解,請仔細研讀MVC設計模式. 先不管service,我們來考慮下. 一個業(yè)務系統(tǒng)不可能不對用戶提交的數(shù)據(jù)進行驗證,驗證包括兩方面 :?有效性和合法性。

有效性:?比如用戶所在崗位,是否屬于數(shù)據(jù)庫有記錄的崗位ID,如果不存在,無效赡盘。

合法性:?比如用戶名只允許輸入最多12個字符,用戶提交了20個字符,不合法号枕。

有效性檢查,可以交給java的校驗框架執(zhí)行,比如JSR303. 假設用戶提交的數(shù)據(jù)經(jīng)過驗證都合法,還是有一些情況是不能調(diào)用修改邏輯的。

要修改的用戶ID不存在

用戶被鎖定,不允許修改

樂觀鎖機制發(fā)現(xiàn)用戶已經(jīng)被被人修改過

由于某種原因,我們的程序無法保存到數(shù)據(jù)庫

一些程序員錯誤的開發(fā)了代碼,導致保存過程中出現(xiàn)異常,比如NPE

對于前3種,我們認為是有效性檢查失敗,第4種屬與我們無法處理的異常,第5種就是程序員bug陨享。

現(xiàn)在的問題是,前三種情況我們?nèi)绾瓮ㄖ脩裟?

在ccontroller 調(diào)用userService的checkUserExist()方法

在controller直接書寫業(yè)務邏輯

在service響應一個狀態(tài)碼機制,比如1 2 3表示錯誤信息,0 表示沒有任何錯誤

顯然前2種方法都不可取?,因為MVC不設計模式告訴我們,controller是用來接收頁面參數(shù),并且調(diào)用邏輯處理,最后組織頁面響應的地方葱淳。我們不可以在controller進行邏輯處理,controller只應該負責用戶API入口和響應的處理(如若不然,思考一下如果有一天service的代碼打包成jar放到另一個平臺,沒有controller了,該怎么辦?)

狀態(tài)碼機制是個不錯的選擇,可是如此一來,用戶保存邏輯變了,比如增加一個情況,不允許修改已經(jīng)離職的用戶,那么我們還需要修改controller的代碼,代碼量增加,維護成本增高,并且還耦合了service,不符合MVC設計模式。

那么怎么辦呢?

現(xiàn)在我們來看下service代碼如何編寫

?/**

? * 修改用戶信息

? * @param user 要修改的用戶數(shù)據(jù)

? */

?public?void?updateUser(User?user)?{

? ?User?userOrig?=?userDao.getUserById(user.getUserID());

? ?if?(null?==?userOrig)?{

? ? ?throw?new?ServiceException("用戶不存在");

? ?}

? ?if?(userOrig.isLocked())?{

? ? ?throw?new?ServiceException("用戶被鎖定,不允許修改");

? ?}

? ?if?(!user.getVersion().equals(userOrig.getVersion()))?{

? ? ?throw?new?ServiceException("用戶已經(jīng)被別人修改過,請刷新重試");

? ?}

? ?// TODO 保存用戶數(shù)據(jù) ?...

?}

這樣一來只要我們檢查到不允許保存的項目,我們就可以直接throw 一個新的異常,異常機制會幫助我們中斷代碼執(zhí)行抛姑。

接下來有2種選擇:

在controller 使用try-catch進行處理

直接把異常拋給上層框架統(tǒng)一處理

第1種方式是不可取的?,注意我們拋出的ServiceException,它僅僅邏輯處理異常,并且我們的方法前面沒有聲明throws ServiceException,這表示他是一個非受查異常.controller也沒有關心會發(fā)生什么異常赞厕。

為什么不定義成受查異常呢??

如果是一個受查異常,那么意味著controller必須要處理你的異常.并且如果有一天你的業(yè)務邏輯變了,可能多一種檢查項,就需要增加一個異常,反之需要刪除一個異常,那么你的方法簽名也需要改變,controller也隨之要改變,這又變成了緊耦合,這和用狀態(tài)碼123表示處理結果沒有什么不同。

我們可以為每一種檢查項定義一個異常嗎??

可以,但是那樣顯得太多余了.因為業(yè)務邏輯處理失敗的時候,根據(jù)我們需求,我們只需要通知用戶失敗的原因(通常應該是一段字符串),以及服務器受理失敗的一個狀態(tài)碼(有時可能不需要狀態(tài)碼,這要看你的設計了),這樣這需要一個包含原因?qū)傩缘漠惓<纯蓾M足我們需求定硝。

最后我們決定這個異常繼承自RuntimeException皿桑,并且包含一個接受一個錯誤原因的構造器,這樣controller層也不需要知道異常,只要全局捕獲到ServiceException做統(tǒng)一的處理即可,這無論是在struct1,2時代,還是springMVC中,甚至servlet年代,都是極為容易的!

異常不提供無參構造器?,因為絕對不允許你拋出一個邏輯處理異常,但是不指明原因,想想看,你是必須要告訴用戶為什么受理失敗的!

如此一來,我們只需要全局統(tǒng)一處理下 ServiceException 就可以了,很好,spring為我們提供了ControllerAdvice機制,有關ControllerAdvice,可以查閱springMVC使用文檔,下面是一個簡單的示例:

@ControllerAdvice(basePackages?=?{?"com.xxx.xxx.bussiness.xxx"?})

public?class?ModuleControllerAdvice?{

?private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(ModuleControllerAdvice.class);

?private?static?final?Logger?SERVICE_LOGGER?=?LoggerFactory.getLogger(ServiceException.class);

?/**

? * 業(yè)務受理失敗

? */

?@ResponseBody

?@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

?@ExceptionHandler(ServiceException.class)

?private?JSONResult?handleServiceException(ServiceException?exception)?{

? ?String?message?=?"業(yè)務受理失敗,原因:"?+?exception.getLocalizedMessage();

? ?SERVICE_LOGGER.info(message);

? ?JSONResult?json?=?new?JSONResult();

? ?json.serCode(500001);?// 500000表示系統(tǒng)異常,500001表示業(yè)務邏輯異常

? ?json.setMessage(message);

? ?return?json;

?}

}

在這個時候,我們就可以很輕松的處理各種情況了。

注意一點,在這個類中,我們定義了2個log對象,分別指向 ServiceException.class 和 ModuleControllerAdvice.class . 并且處理 ServiceException的時候使用了info級別的日志輸出,這是很有用的蔬啡。

首先,ServiceException一定要和其他的代碼錯誤分離,不應該混為一談

其次,ServiceException并不一定要記錄日志,我們應該提供獨立的log對象,方便開關

接下來你可以在修改用戶的時候想客戶端響應這樣的JSON

{

? ?code:?200001,

? ?message:?"業(yè)務受理失敗,原因:用戶名稱不存在!"

}

如此一來沒有任何地方需要關心異常,或者業(yè)務邏輯校驗失敗的情況.用戶也可以得到很友好的錯誤提示诲侮。

如何對異常進行分類

如果你只需要一句概括,那么直接定義一個簡單的異常,用于中斷處理,并且與用戶保持友好交互即可。

如果不可能一句話描述清楚,并且包含附加信息,比如需要在日志或者數(shù)據(jù)庫記錄消息ID,此時可能專門針對這種重要/復雜業(yè)務創(chuàng)建獨立異常箱蟆。

上述兩種情況因為web系統(tǒng),是用戶發(fā)起請求之后需要等待程序給予響應結果的沟绪。

如果是后臺作業(yè),或者復雜業(yè)務需要追溯性.這種通常用流程判斷語句控制,要用異常處理.我們認為這些流程判斷一定在一個原子性處理中.并且檢查到(不是遇到)的問題(不是異常)需要記錄到用戶可友好查看的日志.這種情況屬于處理反饋,并不叫異常。

綜上,筆者通常分為如下幾類:

邏輯異常,這類異常用于描述業(yè)務無法按照預期的情況處理下去,屬于用戶制造的意外

代碼錯誤,這類異常用于描述開發(fā)的代碼錯誤,例如NPE,ILLARG,都屬于程序員制造的BUG

專有異常,多用于特定業(yè)務場景,用于描述指定作業(yè)出現(xiàn)意外情況無法預先處理

各類異常必須要有單獨的日志記錄,或者分級,分類可管理.有的時候僅僅想給三方運維看到邏輯異常空猜。

寫在后面的注意

異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多绽慈。

上面這句話出自,但是我們思考如下幾點:

業(yè)務邏輯檢查,也是意外情況

UnknownHostException,表示找不到這樣的主機,這個異常和NoUserException有什么區(qū)別么?換言之,沒有這樣的主機是異常,沒有這樣的用戶不是異常了么? 所以一定要弄明白什么是用異常來控制邏輯,什么是定義程序異常。

異常處理效率很低

書中所示的例子,是在循環(huán)中大量使用try-catch進行檢查,但是業(yè)務系統(tǒng),用戶發(fā)起請求的次數(shù)與該場景天壤地別.淘寶的11`11是個很好的反例.但是請你的系統(tǒng)上到這個級別再考慮這種問題辈毯。

系統(tǒng)有千萬并發(fā),不可能還去考慮這些中規(guī)中矩的按部就班的方式,別忘了MVC本來就浪費很多資源,代碼量增加很多

業(yè)務系統(tǒng)也存在很多巨量任務處理的情況.但是那些任務都是原子性的,現(xiàn)在MVC中的controller和service可不是原子性的,不然為什么要區(qū)分這么多層呢

如果那么在乎效率,考慮下重寫Throwable的fillStackTrace方法.你要知道異常的開銷大到底大在什么地方,fillStackTrace是一個native方法,會填充異常類內(nèi)部的運行軌跡

不要用異常進行業(yè)務邏輯處理

我們先來看一個例子:

? ?//這是一個非常典型的反例,也是一個誤區(qū).

?/**

? * 處理業(yè)務消息

? * @param message 要處理的消息

? */

?public?void?processMessage(Message?message)?{

? ?try{

? ? ? ?// 處理消息驗證

? ? ? ?// 處理消息解析

? ? ? ?// 處理消息入庫

? ?}catch(ValidateException?e?){

? ? ? ?// 驗證失敗

? ?}catch(ParseException?e?){

? ? ? ?// 解析失敗

? ?}catch(PersistException?e?){

? ? ? ?// 入庫失敗

? ?}

?}

上述代碼就是典型的使用異常來處理業(yè)務邏輯.這種方式需要嚴重的禁止!上述代碼最大的問題在于,我們?nèi)绾卫卯惓碜詣犹幚硎聞漳?

然而這和我們的異常中斷service沒有什么沖突.也并不是一回事坝疼。

我們提倡在 業(yè)務處理?的時候,如果發(fā)現(xiàn)無法處理直接拋出異常即可

而并不是在?邏輯處理?的時候,用異常來判斷邏輯進行的狀況

改正后的邏輯

?/**

? * 處理業(yè)務消息

? * @param message 要處理的消息

? */

?public?void?processMessage(Message?message)?{

? ?// 處理消息驗證

? ?if(!message.isValud()){

? ? ? ?MessageLogService.log("消息校驗失敗"+message.errors())

? ? ? ?return?;

? ?}

? ?// 處理消息解析

? ?if(!message.parse()){

? ? ? ?MessageLogService.log("消息解析失敗"+message.errors())

? ? ? ?return?;

? ?}

?// TODO ....

?}

最后俏皮一句:微服務橫行的今天,我們在action里面直接寫業(yè)務處理,也無可厚非。

- END -

以上是來自程序員DD的微信公眾號,我個人感覺這篇文章非常好,首先他沒有很多的代碼篇幅,我們會異常,看代碼也只是告訴我們?nèi)绾螌懙?根本不知道理論知識,以及如何改進.其次,這是以個人開發(fā)的項目經(jīng)驗去談異常,對自己以后在項目中設計異常很有幫助,對于我們新手來說更不知道如何去設計異常.最后表達一下自己的感慨,這篇文章說明了我們程序員不應該是拿著代碼去copy,paste,我們應該稱為技術的先驅(qū)者!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谆沃,一起剝皮案震驚了整個濱河市钝凶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌管毙,老刑警劉巖腿椎,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桌硫,死亡現(xiàn)場離奇詭異夭咬,居然都是意外死亡,警方通過查閱死者的電腦和手機铆隘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門卓舵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人膀钠,你說我怎么就攤上這事掏湾」妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵融击,是天一觀的道長筑公。 經(jīng)常有香客問我,道長尊浪,這世上最難降的妖魔是什么匣屡? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拇涤,結果婚禮上捣作,老公的妹妹穿的比我還像新娘。我一直安慰自己鹅士,他們只是感情好券躁,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掉盅,像睡著了一般也拜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怔接,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天搪泳,我揣著相機與錄音,去河邊找鬼扼脐。 笑死岸军,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的瓦侮。 我是一名探鬼主播艰赞,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肚吏!你這毒婦竟也來了方妖?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤罚攀,失蹤者是張志新(化名)和其女友劉穎党觅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斋泄,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡杯瞻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了炫掐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魁莉。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旗唁,到底是詐尸還是另有隱情畦浓,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布检疫,位于F島的核電站讶请,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏屎媳。R本人自食惡果不足惜秽梅,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剿牺。 院中可真熱鬧企垦,春花似錦、人聲如沸晒来。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湃崩。三九已至荧降,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間攒读,已是汗流浹背朵诫。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留薄扁,地道東北人剪返。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像邓梅,于是被迫代替她去往敵國和親脱盲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容