<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 9656字,需瀏覽 20分鐘

           ·

          2021-07-21 23:57

          點(diǎn)擊下方“IT牧場”,選擇“設(shè)為星標(biāo)”


          本文僅按照業(yè)務(wù)系統(tǒng)開發(fā)角度描述異常的一些處理看法.不涉及java的異?;A(chǔ)知識(shí),可以自行查閱

          《Java核心技術(shù) 卷I》和 《java編程思想》 可以得到更多的基礎(chǔ)信息.

          寫在前面的話
          筆者文筆功力尚淺,言語多有不妥,請(qǐng)慷慨指正,必定感激不盡. 本文提出了幾個(gè)概念: 處理反饋 業(yè)務(wù)異常 代碼錯(cuò)誤 ,請(qǐng)認(rèn)真思考一下各中區(qū)別.
          在開發(fā)業(yè)務(wù)系統(tǒng)中,我們目前絕大多數(shù)采用MVC模式,但是往往有人把service跟controller緊緊的耦合在一起,甚至直接使用Threadlocal來隱式傳值,并且復(fù)雜的邏輯幾乎只能使用service中存儲(chǔ)的全局對(duì)象來傳遞處理結(jié)果,包括異常.
          這樣一來首先有違MVC模式,二來邏輯十分不清晰,難以維護(hù).本文結(jié)合工作經(jīng)驗(yàn),給出一些異常使用建議,使用spring來實(shí)戰(zhàn)異常為我們帶來的好處.
          常常,我們讀罷了各種java的書,異常的各種機(jī)制,特性都很清楚,但是始終還是不知道如何使用,甚至背下了概念,卻不知道如何致用.
          我們開發(fā)的業(yè)務(wù)系統(tǒng),或者是產(chǎn)品,常常面臨著這樣的問題:
          • 系統(tǒng)運(yùn)行出錯(cuò),但是完全不知道錯(cuò)誤發(fā)生的位置.
          • 我們找到了錯(cuò)誤的位置,但是完全不知道是因?yàn)槭裁?
          • 系統(tǒng)明明出了錯(cuò)誤,但是就是看不到錯(cuò)誤堆棧信息.

          什么情況需要自定義異常

          經(jīng)常看到一些項(xiàng)目,在全局定義一個(gè) AppException,然后所有地方都只拋出這個(gè)異常,并且把捕獲的異常case到這個(gè)AppException中.會(huì)有如下問題:
          • 浪費(fèi)log日志存儲(chǔ)空間,并且棧頂并不是最接近發(fā)生異常的代碼位置.
          • 只有一種異常類,無法精準(zhǔn)區(qū)分開異常類型
          • 異常類后期難以修改以增加其攜帶的信息.

          什么情況需要手動(dòng)處理異常

          我不會(huì)把書上的東西直接復(fù)制下來,這里說一下容易記住的,并且適合業(yè)務(wù)開發(fā)的.
          • 你有能力處理異常,并且你知道如何處理
          • 你有責(zé)任處理異常

          自定義業(yè)務(wù)異常

          考慮如下場景: 系統(tǒng)提供一個(gè)API,用于修改用戶信息,服務(wù)器端采用json數(shù)據(jù)交互.首先我們定義ServiceException,用來表示業(yè)務(wù)邏輯受理失敗,它僅表示我們處理業(yè)務(wù)的時(shí)候發(fā)現(xiàn)無法繼續(xù)執(zhí)行下去.
          /**
           * 業(yè)務(wù)受理失敗異常
           */
          public class ServiceException extends RuntimeException {
              //接收reason參數(shù)用來描述業(yè)務(wù)失敗原因.
            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(); //準(zhǔn)備業(yè)務(wù)邏輯層使用的領(lǐng)域模型
              BeanUtils.copyProperties(userForm, user); //拷貝要修改的值
              user.setUserId(userID); //設(shè)置主鍵到用戶數(shù)據(jù)中
              userService.updateUser(user); //調(diào)用更新業(yè)務(wù)邏輯
              JSONResult json = new JSONResult(); //準(zhǔn)備要響應(yīng)的數(shù)據(jù)
              json.put("user", user); //把修改后的用戶數(shù)據(jù)還給頁面
              return json; // --  
            }
          關(guān)于上述Controller寫法乍一看會(huì)有一些冗余,如果無法理解,請(qǐng)仔細(xì)研讀MVC設(shè)計(jì)模式. 先不管service,我們來考慮下. 一個(gè)業(yè)務(wù)系統(tǒng)不可能不對(duì)用戶提交的數(shù)據(jù)進(jìn)行驗(yàn)證,驗(yàn)證包括兩方面 : 有效性合法性,
          • 有效性: 比如用戶所在崗位,是否屬于數(shù)據(jù)庫有記錄的崗位ID,如果不存在,無效.
          • 合法性: 比如用戶名只允許輸入最多12個(gè)字符,用戶提交了20個(gè)字符,不合法.
          有效性檢查,可以交給java的校驗(yàn)框架執(zhí)行,比如JSR303. 假設(shè)用戶提交的數(shù)據(jù)經(jīng)過驗(yàn)證都合法,還是有一些情況是不能調(diào)用修改邏輯的.
          1. 要修改的用戶ID不存在.
          2. 用戶被鎖定,不允許修改.
          3. 樂觀鎖機(jī)制發(fā)現(xiàn)用戶已經(jīng)被被人修改過.
          4. 由于某種原因,我們的程序無法保存到數(shù)據(jù)庫.
          5. 一些程序員錯(cuò)誤的開發(fā)了代碼,導(dǎo)致保存過程中出現(xiàn)異常,比如NPE.
          對(duì)于前3種,我們認(rèn)為是有效性檢查失敗,第4種屬與我們無法處理的異常,第5種就是程序員bug.
          現(xiàn)在的問題是,前三種情況我們?nèi)绾瓮ㄖ脩裟?
          1. 在ccontroller 調(diào)用userService的checkUserExist()方法.
          2. 在controller直接書寫業(yè)務(wù)邏輯.
          3. 在service響應(yīng)一個(gè)狀態(tài)碼機(jī)制,比如1 2 3表示錯(cuò)誤信息,0 表示沒有任何錯(cuò)誤.
          顯然前2種方法都不可取 ,因?yàn)镸VC不設(shè)計(jì)模式告訴我們,controller是用來接收頁面參數(shù),并且調(diào)用邏輯處理,最后組織頁面響應(yīng)的地方.我們不可以在controller進(jìn)行邏輯處理,controller只應(yīng)該負(fù)責(zé)用戶API入口和響應(yīng)的處理(如若不然,思考一下如果有一天service的代碼打包成jar放到另一個(gè)平臺(tái),沒有controller了,該怎么辦?)
          狀態(tài)碼機(jī)制是個(gè)不錯(cuò)的選擇,可是如此一來,用戶保存邏輯變了,比如增加一個(gè)情況,不允許修改已經(jīng)離職的用戶,那么我們還需要修改controller的代碼,代碼量增加,維護(hù)成本增高,并且還耦合了service,不符合MVC設(shè)計(jì)模式.
          那么怎么辦呢?現(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)被別人修改過,請(qǐng)刷新重試");
              }
              // TODO 保存用戶數(shù)據(jù)  ... 
            }
          這樣一來只要我們檢查到不允許保存的項(xiàng)目,我們就可以直接throw 一個(gè)新的異常,異常機(jī)制會(huì)幫助我們中斷代碼執(zhí)行.
          接下來有2種選擇:
          1. 在controller 使用try-catch進(jìn)行處理.
          2. 直接把異常拋給上層框架統(tǒng)一處理.
          第1種方式是不可取的 ,注意我們拋出的ServiceException,它僅僅邏輯處理異常,并且我們的方法前面沒有聲明throws ServiceException,這表示他是一個(gè)非受查異常.controller也沒有關(guān)心會(huì)發(fā)生什么異常.
          為什么不定義成受查異常呢? 如果是一個(gè)受查異常,那么意味著controller必須要處理你的異常.并且如果有一天你的業(yè)務(wù)邏輯變了,可能多一種檢查項(xiàng),就需要增加一個(gè)異常,反之需要?jiǎng)h除一個(gè)異常,那么你的方法簽名也需要改變,controller也隨之要改變,這又變成了緊耦合,這和用狀態(tài)碼123表示處理結(jié)果沒有什么不同.
          我們可以為每一種檢查項(xiàng)定義一個(gè)異常嗎? 可以,但是那樣顯得太多余了.因?yàn)闃I(yè)務(wù)邏輯處理失敗的時(shí)候,根據(jù)我們需求,我們只需要通知用戶失敗的原因(通常應(yīng)該是一段字符串),以及服務(wù)器受理失敗的一個(gè)狀態(tài)碼(有時(shí)可能不需要狀態(tài)碼,這要看你的設(shè)計(jì)了),這樣這需要一個(gè)包含原因?qū)傩缘漠惓<纯蓾M足我們需求.
          最后我們決定這個(gè)異常繼承自RuntimeException.并且包含一個(gè)接受一個(gè)錯(cuò)誤原因的構(gòu)造器,這樣controller層也不需要知道異常,只要全局捕獲到ServiceException做統(tǒng)一的處理即可,這無論是在struct1,2時(shí)代,還是springMVC中,甚至servlet年代,都是極為容易的!
          異常不提供無參構(gòu)造器 ,因?yàn)榻^對(duì)不允許你拋出一個(gè)邏輯處理異常,但是不指明原因,想想看,你是必須要告訴用戶為什么受理失敗的!
          如此一來,我們只需要全局統(tǒng)一處理下 ServiceException 就可以了,很好,spring為我們提供了ControllerAdvice機(jī)制,有關(guān)ControllerAdvice,可以查閱springMVC使用文檔,下面是一個(gè)簡單的示例:
          @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è)務(wù)受理失敗
             */
            @ResponseBody
            @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
            @ExceptionHandler(ServiceException.class)
            private JSONResult handleServiceException(ServiceException exception) {
              String message = "業(yè)務(wù)受理失敗,原因:" + exception.getLocalizedMessage();
              SERVICE_LOGGER.info(message);
              JSONResult json = new JSONResult();
              json.serCode(500001); // 500000表示系統(tǒng)異常,500001表示業(yè)務(wù)邏輯異常
              json.setMessage(message); 
              return json;
            }
          }
          在這個(gè)時(shí)候,我們就可以很輕松的處理各種情況了.
          注意一點(diǎn),在這個(gè)類中,我們定義了2個(gè)log對(duì)象,分別指向 ServiceException.class 和 ModuleControllerAdvice.class . 并且處理 ServiceException的時(shí)候使用了info級(jí)別的日志輸出,這是很有用的.
          • 首先,ServiceException一定要和其他的代碼錯(cuò)誤分離,不應(yīng)該混為一談.
          • 其次,ServiceException并不一定要記錄日志,我們應(yīng)該提供獨(dú)立的log對(duì)象,方便開關(guān).
          接下來你可以在修改用戶的時(shí)候想客戶端響應(yīng)這樣的JSON
          {
              code: 200001,
              message: "業(yè)務(wù)受理失敗,原因:用戶名稱不存在!"
          }
          如此一來沒有任何地方需要關(guān)心異常,或者業(yè)務(wù)邏輯校驗(yàn)失敗的情況.用戶也可以得到很友好的錯(cuò)誤提示.
          如何對(duì)異常進(jìn)行分類
          如果你只需要一句概括,那么直接定義一個(gè)簡單的異常,用于中斷處理,并且與用戶保持友好交互即可.
          如果不可能一句話描述清楚,并且包含附加信息,比如需要在日志或者數(shù)據(jù)庫記錄消息ID,此時(shí)可能專門針對(duì)這種重要/復(fù)雜業(yè)務(wù)創(chuàng)建獨(dú)立異常.
          上述兩種情況因?yàn)閣eb系統(tǒng),是用戶發(fā)起請(qǐng)求之后需要等待程序給予響應(yīng)結(jié)果的.
          如果是后臺(tái)作業(yè),或者復(fù)雜業(yè)務(wù)需要追溯性.這種通常用流程判斷語句控制,要用異常處理.我們認(rèn)為這些流程判斷一定在一個(gè)原子性處理中.并且檢查到(不是遇到)的問題(不是異常)需要記錄到用戶可友好查看的日志.這種情況屬于處理反饋,并不叫異常.
          綜上,筆者通常分為如下幾類:
          1. 邏輯異常,這類異常用于描述業(yè)務(wù)無法按照預(yù)期的情況處理下去,屬于用戶制造的意外.
          2. 代碼錯(cuò)誤,這類異常用于描述開發(fā)的代碼錯(cuò)誤,例如NPE,ILLARG,都屬于程序員制造的BUG.
          3. 專有異常,多用于特定業(yè)務(wù)場景,用于描述指定作業(yè)出現(xiàn)意外情況無法預(yù)先處理.
          各類異常必須要有單獨(dú)的日志記錄,或者分級(jí),分類可管理.有的時(shí)候僅僅想給三方運(yùn)維看到邏輯異常.
          寫在后面的注意
          異常設(shè)計(jì)的初衷是解決程序運(yùn)行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多.
          上面這句話出自,但是我們思考如下幾點(diǎn):
          業(yè)務(wù)邏輯檢查,也是意外情況
          UnknownHostException,表示找不到這樣的主機(jī),這個(gè)異常和NoUserException有什么區(qū)別么?換言之,沒有這樣的主機(jī)是異常,沒有這樣的用戶不是異常了么? 所以一定要弄明白什么是用異常來控制邏輯,什么是定義程序異常.
          異常處理效率很低
          書中所示的例子,是在循環(huán)中大量使用try-catch進(jìn)行檢查,但是業(yè)務(wù)系統(tǒng),用戶發(fā)起請(qǐng)求的次數(shù)與該場景天壤地別.淘寶的11`11是個(gè)很好的反例.但是請(qǐng)你的系統(tǒng)上到這個(gè)級(jí)別再考慮這種問題.
          1. 系統(tǒng)有千萬并發(fā),不可能還去考慮這些中規(guī)中矩的按部就班的方式,別忘了MVC本來就浪費(fèi)很多資源,代碼量增加很多.
          2. 業(yè)務(wù)系統(tǒng)也存在很多巨量任務(wù)處理的情況.但是那些任務(wù)都是原子性的,現(xiàn)在MVC中的controller和service可不是原子性的,不然為什么要區(qū)分這么多層呢.
          3. 如果那么在乎效率,考慮下重寫Throwable的fillStackTrace方法.你要知道異常的開銷大到底大在什么地方,fillStackTrace是一個(gè)native方法,會(huì)填充異常類內(nèi)部的運(yùn)行軌跡.
          不要用異常進(jìn)行業(yè)務(wù)邏輯處理
          我們先來看一個(gè)例子:
              //這是一個(gè)非常典型的反例,也是一個(gè)誤區(qū).
            /**
             * 處理業(yè)務(wù)消息
             * @param message 要處理的消息
             */
            public void processMessage(Message<String> message) {
              try{
                  // 處理消息驗(yàn)證
                  // 處理消息解析
                  // 處理消息入庫
              }catch(ValidateException e ){
                  // 驗(yàn)證失敗
              }catch(ParseException e ){
                  // 解析失敗
              }catch(PersistException e ){
                  // 入庫失敗
              }
            }
          上述代碼就是典型的使用異常來處理業(yè)務(wù)邏輯.這種方式需要嚴(yán)重的禁止!上述代碼最大的問題在于,我們?nèi)绾卫卯惓碜詣?dòng)處理事務(wù)呢?
          然而這和我們的異常中斷service沒有什么沖突.也并不是一回事.
          • 我們提倡在 業(yè)務(wù)處理 的時(shí)候,如果發(fā)現(xiàn)無法處理直接拋出異常即可.
          • 而并不是在 邏輯處理 的時(shí)候,用異常來判斷邏輯進(jìn)行的狀況.
          改正后的邏輯
            /**
             * 處理業(yè)務(wù)消息
             * @param message 要處理的消息
             */
            public void processMessage(Message<String> message) {
              // 處理消息驗(yàn)證
              if(!message.isValud()){
                  MessageLogService.log("消息校驗(yàn)失敗"+message.errors())
                  return ;
              }
              // 處理消息解析
              if(!message.parse()){
                  MessageLogService.log("消息解析失敗"+message.errors())
                  return ;
              }
               // TODO ....
            }
          最后俏皮一句:微服務(wù)橫行的今天,我們在action里面直接寫業(yè)務(wù)處理,也無可厚非.

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

          干貨分享

          最近將個(gè)人學(xué)習(xí)筆記整理成冊,使用PDF分享。關(guān)注我,回復(fù)如下代碼,即可獲得百度盤地址,無套路領(lǐng)取!

          ?001:《Java并發(fā)與高并發(fā)解決方案》學(xué)習(xí)筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學(xué)習(xí)筆記;?003:《Java面試寶典》?004:《Docker開源書》?005:《Kubernetes開源書》?006:《DDD速成(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)速成)》?007:全部?008:加技術(shù)群討論

          加個(gè)關(guān)注不迷路

          喜歡就點(diǎn)個(gè)"在看"唄^_^

          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  8050午夜一级免费 | 精品无码久久久久久久久爆乳 | 久久你懂的 | 色777色 | 国产一级a毛一级a看免费视频野外 |