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
  源碼:我的GitHub


![](https://img2018.cnblogs.com/blog/1183871/201906/1183871-20190606114229963-886448059.png)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM