關於合理使用SpringMVC統一異常處理機制以改善代碼風格的一些思考

問題背景:

統一異常處理在WEB開發中可不是一個新穎的問題,然而,根據項目的實際情況,用的恰到好處,是可以在項目中省去大量冗餘代碼的。在以spring/springMVC做IOC容器的web項目中,常見的統一異常處理不外乎如下三種方式:
springMVC處理異常的3種方式:

(1) 使用Spring MVC提供的簡單異常處理器SimpleMappingExceptionResolver;

(2) 實現Spring的異常處理接口HandlerExceptionResolver 自定義自己的異常處理器; 比如目前項目中就採用了這種方式,下面是項目中用於統一異常處理的類的代碼:

public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {

private Log log = LogFactory.getLog(this.getClass());

    @ResponseBody
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        LogUtils.logException(ex);
        String viewName = determineViewName(ex, request);
        if (viewName == null && HttpRequestUtils.isAnsynJsonRequest(request)){
            Writer writer = null;
            try {
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
                response.setHeader("Cache-Control", "no-cache");

                Map<String, Object> hashMap = new HashMap<String, Object>();
                hashMap.put("success", false);
                String exMessage = ex.getMessage();
                if ((ex instanceof BaseRuntimeException) && exMessage != null){
                    exMessage = exMessage.replaceAll("\"", "");
                } else {
                    exMessage = "系統異常";
                }
                writer = response.getWriter();
                hashMap.put("message", exMessage);
                String result = JsonUtils.object2Json(hashMap);
                writer.write(result);
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                if (writer != null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return new ModelAndView();
        } else {
            // Apply HTTP status code for error views, if specified.
            // Only apply it if we're processing a top-level request.
            Integer statusCode = determineStatusCode(request, viewName);
            if (statusCode != null) {
                applyStatusCodeIfPossible(request, response, statusCode);
                return getModelAndView(viewName, ex, request);
            }
        }

        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

(3) 使用@ExceptionHandler註解實現異常處理;

然而,在項目中,最常見的處理方式就是在項目報出異常的時候跳轉到一個友好的提示頁面,以此規避頁面上打印出大量異常堆棧信息導致用戶看不懂、並且還影響體驗的問題。
然而,上述處理方式對於在前端大量使用Ajax的情況下作用及其有限。Web項目中用戶在進行了某項操作,或者後臺進行了相關的參數校驗後,往往需要返回提示信息給用戶,告訴用戶操作是否成功。 所以,在大多數場景下,項目發生異常(一般是業務異常),或者參數校驗不成功時候,我們更希望的是這些出錯的信息以數據的形式(比如json/xml數據)返回。然後web前端根據這些返回的提示信息彈出模態框或其他方式給予用戶友好的提示。而不是直接跳轉到某個或某幾個特定的頁面。

目前的解決方式:
然而,正如之前所說,目前本人所接觸的項目採用的統一異常處理是上述提到的第(2)種方式。然而,web前端又使用了大量的ajax技術和後臺交互。Web前端傳入的參數在後端校驗不成功,爲了給予用戶提示,是採用如下代碼段所示的方式:


ActionResult result = new ActionResult(true);

        if (null == condition || null == condition.getBeginTime() || null == condition.getEndTime()) {
            result.setSuccess(false);
            result.setMessage("上傳的參數錯誤");
        } else if (condition.getBeginTime().after(condition.getEndTime())) {
            result.setSuccess(false);
            result.setMessage("開始時間不能大於結束時間");
        } else if (PmsDateTimeUtil.gtMonthBetween(condition.getBeginTime(), condition.getEndTime(), PmsConstants.MAX_MONTH_QUERY)) {
            result.setSuccess(false);
            result.setMessage(String.format("查詢時間不能超過%s個月", PmsConstants.MAX_MONTH_QUERY));
        }
        return result;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

上述代碼段中,類ActionResult封裝了參數校驗不通過的錯誤信息,web前端調用一旦參數校驗不通過,就會返回一個ActionResult類的實例對象,該對象會被轉爲json字符串然後返回給前端。比如,前端傳入的開始時間大於結束時間,後端返回給前端的提示信息是如下這個樣子的json字符串:

{ 
“msg”: ”開始時間不能大於結束時間”, 
“data”: “null”,
“success” : “false” 
}
  • 1
  • 2
  • 3
  • 4
  • 5

使用ExceptionHandler實現統一異常處理:
事實上,使用ExceptionHandler註解實現統一異常處理,也可以實現當拋出異常後後端返回異常信息字符串(json/xml)而不是跳轉到某個特定頁面的功能。使用統一異常處理的好處是能夠將異常信息統一捕捉並組裝成固定格式的數據返回,我想在ajax回調處理中好處可多了, 回調得到的數據因爲格式統一,前端可以很方便的通過某種控件進行呈現或友好提示 。 雖然也可以手動在Controller層的方法返回的結果中添加異常信息,但是隻會徒增代碼量,卻不能使我們更好的專注於業務邏輯。
項目中的使用示例如下:

1、增加BaseExceptionHandleAction類,並在類中同時使用@ExceptionHandler和@ResponseBody註解聲明異常處理,示例代碼如下:


public class BaseExceptionHandleAction {

    /** 基於@ExceptionHandler異常處理 */
    @ExceptionHandler
    @ResponseBody
    public Map<String, Object>  handleAndReturnData(HttpServletRequest request, HttpServletResponse response, Exception ex) {

        Map<String, Object> data = new HashMap<String, Object>();
        if(ex instanceof BusinessException) {
            BusinessException e = (BusinessException)ex;
            data.put("code", e.getCode());
        }
        data.put("msg", ex.getMessage());
        data.put("success", false);
        data.put("data", null);
        return data;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2、在項目中,我們可以使所有需要統一異常處理的Controller都繼承該類,如下所示,我們寫了一個Controller,名字叫ExceptionTestController, 該類繼承於BaseExceptionHandleAction:作爲示例,本人寫了一個名爲test的方法以演示統一異常處理。


@Controller
public class ExceptionTestController extends BaseExceptionHandleAction {

    @RequestMapping(value = "/exceptionTest", method = RequestMethod.GET)
    public void  test(HttpServletRequest request, HttpServletResponse response, Condition condition) {

        // 此處可能還有大量代碼,略

        if (null == condition || null == condition.getBeginTime() || null == condition.getEndTime()) {
            throw new BusinessException("上傳的參數錯誤");
        }

        if (condition.getBeginTime().after(condition.getEndTime())) {
            throw new BusinessException("開始時間不能大於結束時間");
        }

        if (PmsDateTimeUtil.gtMonthBetween(condition.getBeginTime(), condition.getEndTime(),
                PmsConstants.MAX_MONTH_QUERY)) {
            throw new BusinessException(String.format("查詢時間不能超過%s個月", PmsConstants.MAX_MONTH_QUERY));
        }

        // 此處可能還有大量代碼,略
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

然後,假如前端傳入的參數開始時間小於結束時間,前端收到的相應字符串如下:

{ 
“msg “: "開始時間不能大於結束時間"
“data” : “null”, 
“success” : “false” 
}
  • 1
  • 2
  • 3
  • 4
  • 5

兩種方式的比較:
在上述的示例中,本人講述了兩種返回提示信息給web前端的方式。第一種方式每次都創建一個對象用於封裝提示信息,上述闡述中封裝錯誤信息的對象是ActionResult。第二種方式則是巧妙的利用了java的異常處理機制。

雖然兩種方式都達到了同樣的效果,然而,第一種方式需要主動封裝錯誤信息並返回給前端,導致代碼量劇增。由於歷史原因,目前項目中也存在同樣的問題。 並且,很容易在代碼中寫出大量if else這樣的語句,代碼顯得不夠優雅。 第二種方式則不需要手動組裝結果,重複代碼量少了,代碼也顯得足夠優雅。並且,可以使開發人員更專注的去處理相關業務邏輯。 另外,相比於第一中方式,第二種方式返回提示信息是即時的,參數不合法立即返回。第一種方式則需要等到return 語句返回的時候才返回提示信息,很可能參數已經出錯了,代因爲開發人員的粗心大意,導致代碼還是走了一段無用的業務邏輯才返回,所以業務延遲大大增加,有沒有起到實際的作用。最後,試想一下,在一個業務量龐大的項目中,利用框架或語言本身儘可能降低編碼的複雜度和代碼量意味着什麼?新人容易上手?容易維護 ? 我想,都有吧。

簡單總結
使用@ExceptionHandler進行統一異常處理的好處已經在上面有所闡述,作爲示例,上述的代碼略顯簡單,要想讓統一異常處理機制更加健壯和可靠,需要開發人員進一步的完善。但需要注意的是,在spring/springMVC中使用上述機制,需要@ExceptionHandler和@ResponseBody兩個註解同時使用。

發佈了8 篇原創文章 · 獲贊 18 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章