1. 程式人生 > >SpringBoot第十四篇:統一異常處理

SpringBoot第十四篇:統一異常處理

作者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/10984081.html
版權宣告:本文為博主原創文章,轉載請附上博文連結!


引言

  本文將談論 SpringBoot 的預設錯誤處理機制,以及如何自定義錯誤響應。


版本資訊

  • JDK:1.8
  • SpringBoot :2.1.4.RELEASE
  • maven:3.3.9
  • Thymelaf:2.1.4.RELEASE
  • IDEA:2019.1.1


預設錯誤響應

  我們新建一個專案,先來看看 SpringBoot 的預設響應式什麼:

首先,引入 maven 依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

然後,寫一個請求介面:

package com.yanfei1819.customizeerrordemo.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Created by 追夢1819 on 2019-05-09.
 */
@Controller
public class DefaultErrorController {
    @GetMapping("/defaultViewError")
    public void defaultViewError(){
        System.out.println("預設頁面異常");
    }
    @ResponseBody
    @GetMapping("/defaultDataError")
    public void defaultDataError(){
        System.out.println("預設的客戶端異常");
    }
}

隨意訪問一個8080埠的地址,例如 http://localhost:8080/a ,如下效果:

  1. 瀏覽器訪問,返回一個預設頁面

  2. 其它的客戶端訪問,返回確定的json字串

  以上是SpringBoot 預設的錯誤響應頁面和返回值。不過,在實際專案中,這種響應對使用者來說並不友好。通常都是開發者自定義異常頁面和返回值,使其看起來更加友好、更加舒適。


預設的錯誤處理機制

  在定製錯誤頁面和錯誤響應資料之前,我們先來看看 SpringBoot 的錯誤處理機制。

ErrorMvcAutoConfiguration :

容器中有以下元件:

1、DefaultErrorAttributes
2、BasicErrorController

3、ErrorPageCustomizer
4、DefaultErrorViewResolver


系統出現 4xx 或者 5xx 錯誤時,ErrorPageCustomizer 就會生效:

    @Bean
    public ErrorPageCustomizer errorPageCustomizer() {
        return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
    }
   private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
        private final ServerProperties properties;
        private final DispatcherServletPath dispatcherServletPath;
        protected ErrorPageCustomizer(ServerProperties properties,
                DispatcherServletPath dispatcherServletPath) {
            this.properties = properties;
            this.dispatcherServletPath = dispatcherServletPath;
        }
         // 註冊錯誤頁面響應規則
        @Override
        public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
            ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
                    .getRelativePath(this.properties.getError().getPath()));
            errorPageRegistry.addErrorPages(errorPage);
        }
        @Override
        public int getOrder() {
            return 0;
        }
    }

上面的註冊錯誤頁面響應規則能夠的到錯誤頁面的路徑(getPath):

    @Value("${error.path:/error}")
    private String path = "/error"; //(web.xml註冊的錯誤頁面規則)
    public String getPath() {
        return this.path;
    }

此時會被 BasicErrorController 處理:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
}

BasicErrorController 中有兩個請求:

    // //產生html型別的資料;瀏覽器傳送的請求來到這個方法處理
    //  MediaType.TEXT_HTML_VALUE ==> "text/html"
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        //去哪個頁面作為錯誤頁面;包含頁面地址和頁面內容
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }
    //產生json資料,其他客戶端來到這個方法處理;
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }

上面原始碼中有兩個請求,分別是處理瀏覽器傳送的請求和其它瀏覽器傳送的請求的。是通過請求頭來區分的:

1、瀏覽器請求頭

2、其他客戶端請求頭

resolveErrorView,獲取所有的異常檢視解析器 ;

    protected ModelAndView resolveErrorView(HttpServletRequest request,
            HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
         //獲取所有的 ErrorViewResolver 得到 ModelAndView
        for (ErrorViewResolver resolver : this.errorViewResolvers) {
            ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
            if (modelAndView != null) {
                return modelAndView;
            }
        }
        return null;
    }

DefaultErrorViewResolver,預設錯誤檢視解析器,去哪個頁面是由其解析得到的;

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
            Map<String, Object> model) {
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        // 檢視名,拼接在 error/ 後面
        String errorViewName = "error/" + viewName;
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
                .getProvider(errorViewName, this.applicationContext);
        if (provider != null) {
             // 使用模板引擎的情況
            return new ModelAndView(errorViewName, model);
        }
         // 未使用模板引擎的情況
        return resolveResource(errorViewName, model);
    }

其中 SERIES_VIEWS 是:

    private static final Map<Series, String> SERIES_VIEWS;
    static {
        Map<Series, String> views = new EnumMap<>(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

下面看看沒有使用模板引擎的情況:

    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        for (String location : this.resourceProperties.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }

以上程式碼可以總結為:

模板引擎不可用
就在靜態資原始檔夾下
找errorViewName對應的頁面 error/4xx.html

如果,靜態資原始檔夾下存在,返回這個頁面
如果,靜態資原始檔夾下不存在,返回null


定製錯誤響應

  按照 SpringBoot 的預設異常響應,分為預設響應頁面和預設響應資訊。我們也分為定製錯誤頁面和錯誤資訊。

定製錯誤的頁面

  1. 有模板引擎的情況

    ​ SpringBoot 預設定位到模板引擎資料夾下面的 error/ 資料夾下。根據發生的狀態碼的錯誤尋找到響應的頁面。注意一點的是,頁面可以"精確匹配"和"模糊匹配"。
    ​ 精確匹配的意思是返回的狀態碼是什麼,就找到對應的頁面。例如,返回的狀態碼是 404,就匹配到 404.html.

    ​ 模糊匹配,意思是可以使用 4xx 和 5xx 作為錯誤頁面的檔名來匹配這種型別的所有錯誤。不過,"精確匹配"優先。

  2. 沒有模板引擎

    ​ 專案如果沒有使用模板引擎,則在靜態資原始檔夾下面查詢。

下面自定義異常頁面,並模擬異常發生。

在以上的示例基礎上,首先,自定義一個異常:

public class UserNotExistException extends RuntimeException {
    public UserNotExistException() {
        super("使用者不存在");
    }
}

然後,進行異常處理:

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        // 傳入我們自己的錯誤狀態碼  4xx 5xx,否則就不會進入定製錯誤頁面的解析流程
        // Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        request.setAttribute("javax.servlet.error.status_code",500);
        map.put("code","user.notexist");
        map.put("message","使用者出錯啦");
        request.setAttribute("ext",map);
        //轉發到/error
        return "forward:/error";
    }
}

注意幾點,一定要定製自定義的狀態碼,否則沒有作用。

第三步,定製一個頁面:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <title>Internal Server Error | 伺服器錯誤</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        <!--省略css程式碼-->
    </style>
</head>
<body>
<h1>伺服器錯誤</h1>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h1>status:[[${status}]]</h1>
    <h2>timestamp:[[${timestamp}]]</h2>
    <h2>exception:[[${exception}]]</h2>
    <h2>message:[[${message}]]</h2>
    <h2>ext:[[${ext.code}]]</h2>
    <h2>ext:[[${ext.message}]]</h2>
</main>
</body>
</html>

最後,模擬一個異常:

@Controller
public class CustomizeErrorController {
    @GetMapping("/customizeViewError")
    public void customizeViewError(){
        System.out.println("自定義頁面異常");
        throw new UserNotExistException();
    }
}

啟動專案,可以觀察到以下結果:

定製響應的json

針對瀏覽器意外的其他客戶端錯誤響應,相似的道理,我們先進行自定義異常處理:

    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handleException(Exception e){
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notexist");
        map.put("message",e.getMessage());
        return map;
    }

然後模擬異常的出現:

    @ResponseBody
    @GetMapping("/customizeDataError")
    public void customizeDataError(){
        System.out.println("自定義客戶端異常");
        throw new UserNotExistException();
    }

啟動專案,看到結果是:


總結

  異常處理同日志一樣,也屬於專案的“基礎設施”,它的存在,可以擴大系統的容錯處理,加強系統的健壯性。在自定義的基礎上,優化了錯誤提示,對使用者更加友好。

  由於篇幅所限,以上的 SpringBoot 的內部錯誤處理機制也只屬於“蜻蜓點水”。後期將重點分析 SpringBoot 的工作機制。

  最後,如果需要完整程式碼,請移步至我的GitHub。



相關推薦

SpringBoot統一異常處理

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/10984081.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   本文將談論 SpringBoot 的預設錯誤處理機制,以及如何自定義錯誤響應。 版本資訊 JDK:1.8 Spri

一起來學SpringBoot | 強大的 actuator 服務監控與管理

SpringBoot 是為了簡化 Spring 應用的建立、執行、除錯、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規範,引入相關的依賴就可以輕易的搭建出一個 WEB 工程 actuato

史上最簡單的 SpringCloud 教程 | 服務註冊(consul)

配置 資料 源碼下載 擴展性 local sta tar value mark 這篇文章主要介紹 spring cloud consul 組件,它是一個提供服務發現和配置的工具。consul具有分布式、高可用、高擴展性。 consul 具有以下性質: 服務發現:cons

PowerBI開發 DAX 表達式(時間+過濾+關系)

數據 bsp 可能 library 聚合 ont 數據分析 狀態 mon DAX表達式中包含時間關系(Time Intelligence)相關的函數,用於對日期維度進行累加、同比和環比等分析。PowerBI能夠創建關系,通過過濾器來對影響計算的上下文。 一,時間關系 D

Spring Cloud系列教程 | Spring Cloud與Kubernetes的整合

推薦 Spring Cloud 視訊: Spring Cloud與Kubernetes的整合      Spring Cloud提供了專門的spring-cloud-kubernetes專案與k8s整合,儘管k8s提供了服務註冊和發現等功能與Spring cl

PowerBI開發 DAX 表示式(時間+過濾+關係)

DAX表示式中包含時間關係(Time Intelligence)相關的函式,用於對日期維度進行累加、同比和環比等分析。PowerBI能夠建立關係,通過過濾器來對影響互動的上下文。 一,時間關係 DAX表示式有兩種方式計算累加和,TOTALxTD()是DATESxTD()的語法糖,使得PowerBI對累加和

Spring Boot+MyBatis配置多資料來源

說起多資料來源,一般都來用來解決主從模式或者業務比較複雜需要連線不同的分庫來支援業務。本篇文章主要講解後者的模式,利用AOP動態切換來達到專案訪問不同資料來源。 構架工程 建立一個springboot工程,在其pom檔案加入: <dependency> &

一起來學SpringBoot | 整合Swagger線上除錯

SpringBoot 是為了簡化 Spring 應用的建立、執行、除錯、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規範,引入相關的依賴就可以輕易的搭建出一個 WEB 工程 隨著網際網路技

一起來學SpringBoot | actuator與spring-boot-admin 可以說的祕密

SpringBoot 是為了簡化 Spring 應用的建立、執行、除錯、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規範,引入相關的依賴就可以輕易的搭建出一個 WEB 工程 一起來學Spr

一起來學SpringBoot | 輕鬆搞定資料驗證(一)

SpringBoot是為了簡化Spring應用的建立、執行、除錯、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規範,引入相關的依賴就可以輕易的搭建出一個 WEB 工程 對於任何一個

Python 學習 命名元組

http .com 類方法 tps lis company 存儲 port rep Python的元組不能為元組內部的數據進行命名,而 collections.namedtuple 可以來構造一個含有字段名稱的元組類,命名元組可以通過字段名來獲取元素值: collec

SpringBootSpringBoot+MyBatis+Thymelaf實現CRUD

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/10936304.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   總結前面幾章,我們瞭解了 SpringBoot 的基本用法及其配置,整合各大 ORM 框架,並瞭解了 Thymelaf

SpringBoot整合jsp

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/10953600.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   SpringBoot 雖然官方推薦使用 thymelaf 模板引擎,但是也支援jsp,只不過需要做一些修改。本文將講解

SpringBootswagger構建優雅文件

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/11007470.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   前面的十四篇文介紹了 SpringBoot 的一些基本和常用的功能。後面,我們將介紹 SpringBoot 的高階的功

SpringBoot自定義starter

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/11058502.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 前言   這一段時間專案趕進度,故該系列部落格更新沒有之前那麼頻繁,望諒解。   SpringBoot 用起來方便,它預設集成了

SpringBoot定時任務

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/11076555.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   相信大家對定時任務很熟悉,其重要性也不言而喻。定時發簡訊、定時批量操作、定時統計資料等,都離不開定時任務。本文將講解定

跟我學SpringCloud | Spring Cloud Gateway高階應用

SpringCloud系列教程 | 第十四篇:Spring Cloud Gateway高階應用 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如無特殊說明,本系列教程全採用以上版本 上一篇我們聊了Gateway和註冊中心的使用,以及 Ga

SpringBoot第二應用監控之Admin

作者:追夢1819 原文:https://www.cnblogs.com/yanfei1819/p/11457867.html 版權宣告:本文為博主原創文章,轉載請附上博文連結! 引言   前一章(SpringBoot第二十二篇:應用監控之Actuator)介紹了 SpringBoot 應用使用 Actuc

Java程式設計思想 二章通過異常處理錯誤

發現錯誤的理想時機是在編譯階段,也就是程式在編碼過程中發現錯誤,然而一些業務邏輯錯誤,編譯器並不能一定會找到錯誤,餘下的問題需要在程式執行期間解決,這就需要發生錯誤的地方能夠準確的將錯誤資訊傳遞給某個接收者,以便接收者知道如何正確的處理這個錯誤資訊。 改進錯誤的機制在Java中尤為重要,

Python開發【Web框架本質

中一 用戶 contain get pattern app sta doc connect Web框架本質 眾所周知,對於所有的Web應用,本質上其實就是一個socket服務端,用戶的瀏覽器其實就是一個socket客戶端。 1 2 3 4 5 6 7 8 9