前言
之前一篇文章介紹了基本的統(tǒng)一異常處理思路: spring mvc/boot 統(tǒng)一異常處理最佳實(shí)踐.
上篇文章也有許多人提出了一些問(wèn)題:
- 如何區(qū)分 ajax 請(qǐng)求和普通頁(yè)面請(qǐng)求, 以分別返回 json 錯(cuò)誤信息和錯(cuò)誤頁(yè)面.
- 如何結(jié)合 http 狀態(tài)碼進(jìn)行統(tǒng)一異常處理.
今天這篇文章就主要來(lái)講講這些, 以及其他的一些拓展點(diǎn).
區(qū)分請(qǐng)求方式
其實(shí) spring boot 本身是內(nèi)置了一個(gè)異常處理機(jī)制的, 會(huì)判斷請(qǐng)求頭的參數(shù)來(lái)區(qū)分要返回 json 數(shù)據(jù)還是錯(cuò)誤頁(yè)面. 源碼為: org.springframework.boot.autoconfigure.web.servlet.error.basicerrorcontroller
, 他會(huì)處理 /error 請(qǐng)求. 核心處理代碼如下:
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
|
@requestmapping ( produces = { "text/html" } ) // 如果請(qǐng)求頭是 text/html, 則找到錯(cuò)誤頁(yè)面, 并返回 public modelandview errorhtml(httpservletrequest request, httpservletresponse response) { // 1. 獲取 http 錯(cuò)誤狀態(tài)碼 httpstatus status = this .getstatus(request); // 2. 調(diào)用 geterrorattributes 獲取響應(yīng)的 map 結(jié)果集. map<string, object> model = collections.unmodifiablemap( this .geterrorattributes(request, this .isincludestacktrace(request, mediatype.text_html))); // 3. 設(shè)置響應(yīng)頭的狀態(tài)碼 response.setstatus(status.value()); // 4. 獲取錯(cuò)誤頁(yè)面的路徑 modelandview modelandview = this .resolveerrorview(request, response, status, model); return modelandview != null ? modelandview : new modelandview( "error" , model); } @requestmapping @responsebody public responseentity<map<string, object>> error(httpservletrequest request) { // 調(diào)用 geterrorattributes 獲取響應(yīng)的 map 結(jié)果集. map<string, object> body = this .geterrorattributes(request, this .isincludestacktrace(request, mediatype.all)); // 獲取 http 錯(cuò)誤狀態(tài)碼 httpstatus status = this .getstatus(request); // 返回給頁(yè)面 json 信息. return new responseentity(body, status); } |
這兩個(gè)方法的共同點(diǎn)是: 他們都調(diào)用了 this.geterrorattributes(…) 方法來(lái)獲取響應(yīng)信息.
然后來(lái)看看他默認(rèn)情況下對(duì)于 ajax 請(qǐng)求和 html 請(qǐng)求, 分別的返回結(jié)果是怎樣的:
對(duì)于返回錯(cuò)誤頁(yè)面, 其中還調(diào)用了一個(gè)非常重要的方法: this.resolveerrorview(...)
方法, 源碼我就不帶大家看了, 他的作用就是根據(jù) http 狀態(tài)碼來(lái)去找錯(cuò)誤頁(yè)面, 如 500 錯(cuò)誤會(huì)去找 /error/500.html, 403 錯(cuò)誤回去找 /error/403.html, 如果找不到則再找 /error/4xx.html 或 /error/5xx.html 頁(yè)面. 還找不到的話, 則會(huì)去找 /error.html 頁(yè)面, 如果都沒(méi)有配置, 則會(huì)使用 spring boot 默認(rèn)的頁(yè)面. 即:
看到這里, 應(yīng)該就清楚了, 我們主要需要做四件事:
- 發(fā)送異常后, 重定向到 basicerrorcontroller 來(lái)處理 (既然spring boot 都已經(jīng)寫(xiě)好了區(qū)分請(qǐng)求的功能, 我們就不必要再寫(xiě)這些判斷代碼了)
- 自定義 http 錯(cuò)誤狀態(tài)碼
-
他返回的信息格式可能不是我們想要的, 所以必須要改造
geterrorattributes(...)
方法, 以自定義我們向頁(yè)面返回的數(shù)據(jù). (自定義錯(cuò)誤信息) - 創(chuàng)建我們自己的 /error/4xx.html 或 /error/5xx.html 等頁(yè)面, (自定義錯(cuò)誤頁(yè)面)
basicerrorcontroller
第一點(diǎn)很簡(jiǎn)單, basicerrorcontroller 他處理 /error 請(qǐng)求, 我們只需要將頁(yè)面重定向到 /error 即可, 在 controlleradvice 中是這樣的:
1
2
3
4
5
6
7
8
9
|
@controlleradvice public class webexceptionhandler { @exceptionhandler public string methodargumentnotvalid(bindexception e) { // do something return "/error" ; } } |
自定義 http 錯(cuò)誤狀態(tài)碼
我們來(lái)看下 this.getstatus(request); 的源碼, 看他原來(lái)時(shí)如何獲取錯(cuò)誤狀態(tài)碼的:
1
2
3
4
5
6
7
8
9
10
11
12
|
protected httpstatus getstatus(httpservletrequest request) { integer statuscode = (integer)request.getattribute( "javax.servlet.error.status_code" ); if (statuscode == null ) { return httpstatus.internal_server_error; } else { try { return httpstatus.valueof(statuscode); } catch (exception var4) { return httpstatus.internal_server_error; } } } |
簡(jiǎn)單來(lái)說(shuō)就是從 request 域中獲取 javax.servlet.error.status_code 的值, 如果為 null 或不合理的值, 都返回 500. 既然如何在第一步, 重定向到 /error 之前將其配置到 request 域中即可, 如:
1
2
3
4
5
6
7
8
9
10
|
@controlleradvice public class webexceptionhandler { @exceptionhandler public string methodargumentnotvalid(bindexception e, httpservletrequest request) { request.setattribute( "javax.servlet.error.status_code" , 400 ); // do something return "forward:/error" ; } } |
自定義錯(cuò)誤信息
也就是 geterrorattributes 方法, 默認(rèn)的代碼是這樣的:
1
2
3
4
5
6
7
8
|
public map<string, object> geterrorattributes(webrequest webrequest, boolean includestacktrace) { map<string, object> errorattributes = new linkedhashmap(); errorattributes.put( "timestamp" , new date()); this .addstatus(errorattributes, webrequest); this .adderrordetails(errorattributes, webrequest, includestacktrace); this .addpath(errorattributes, webrequest); return errorattributes; } |
他獲取了時(shí)間戳, 錯(cuò)誤狀態(tài)碼, 錯(cuò)誤信息, 錯(cuò)誤路徑等信息, 和我們之前看到默認(rèn)的返回內(nèi)容是一致的:
1
2
3
4
5
6
7
|
{ "timestamp" : "2019-01-27t07:08:30.011+0000" , "status" : 500 , "error" : "internal server error" , "message" : "/ by zero" , "path" : "/user/index" } |
同樣的思路, 我們將錯(cuò)誤信息也放到 request 域中, 然后在 geterrorattributes 中從 request 域中獲取:
1
2
3
4
5
6
7
8
9
10
11
12
|
@controlleradvice public class webexceptionhandler { @exceptionhandler public string methodargumentnotvalid(bindexception e, httpservletrequest request) { request.setattribute( "javax.servlet.error.status_code" , 400 ); request.setattribute( "code" , 1 ); request.setattribute( "message" , "參數(shù)校驗(yàn)失敗, xxx" ); // do something return "forward:/error" ; } } |
再繼承 defaulterrorattributes 類(lèi), 重寫(xiě) geterrorattributes 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//@component public class mydefaulterrorattributes extends defaulterrorattributes { @override //重寫(xiě) geterrorattributes方法-添加自己的項(xiàng)目數(shù)據(jù) public map<string, object> geterrorattributes(webrequest webrequest, boolean includestacktrace) { map<string, object> map = new hashmap<>(); // 從 request 域中獲取 code object code = webrequest.getattribute( "code" , requestattributes.scope_request); // 從 request 域中獲取 message object message = webrequest.getattribute( "message" , requestattributes.scope_request); map.put( "code" , code); map.put( "message" , message); return map; } } |
自定義錯(cuò)誤頁(yè)面
我們遵循 springboot 的規(guī)則, 在 /error/ 下建立 400.html, 500.html 等頁(yè)面細(xì)粒度的錯(cuò)誤, 并配置一個(gè) /error.html 用來(lái)處理細(xì)粒度未處理到的其他錯(cuò)誤.
/error/400.html
1
2
3
4
5
6
7
8
9
10
11
12
|
<!doctype html> <html lang= "en" xmlns:th= "http://www.thymeleaf.org" > <head> <meta charset= "utf-8" > <title> 400 </title> </head> <body> <h1> 400 </h1> <h1 th:text= "$[code]" ></h1> <h1 th:text= "${message}" ></h1> </body> </html> |
/error/500.html
1
2
3
4
5
6
7
8
9
10
11
12
|
<!doctype html> <html lang= "en" xmlns:th= "http://www.thymeleaf.org" > <head> <meta charset= "utf-8" > <title> 500 </title> </head> <body> <h1> 500 </h1> <h1 th:text= "$[code]" ></h1> <h1 th:text= "${message}" ></h1> </body> </html> |
/error.html
1
2
3
4
5
6
7
8
9
10
11
12
|
<!doctype html> <html lang= "en" xmlns:th= "http://www.thymeleaf.org" > <head> <meta charset= "utf-8" > <title>系統(tǒng)出現(xiàn)了錯(cuò)誤</title> </head> <body> <h1>error page</h1> <h1 th:text= "$[code]" ></h1> <h1 th:text= "${message}" ></h1> </body> </html> |
測(cè)試效果
到此位置, 大功告成, 然后來(lái)創(chuàng)造一個(gè)異常來(lái)測(cè)試一下效果:
前端 error 處理
現(xiàn)在使用了 http 狀態(tài)碼, 所以 ajax 請(qǐng)求出現(xiàn)錯(cuò)誤后, 需要在每個(gè) ajax 請(qǐng)求方法中都寫(xiě) error: function() {}
方法, 甚至麻煩. 好在 jquery 為我們提供了全局處理 ajax 的 error 結(jié)果的方法 ajaxerror() :
1
2
3
4
5
|
$(document).ajaxerror(function(event, response){ console.log( "錯(cuò)誤響應(yīng)狀態(tài)碼: " ,response.status); console.log( "錯(cuò)誤響應(yīng)結(jié)果: " ,response.responsejson); alert( "an error occurred!" ); }); |
結(jié)語(yǔ)
回顧一下講到的這些內(nèi)容:
- 理解 springboot 默認(rèn)提供的 basicerrorcontroller
-
自定義 http 錯(cuò)誤狀態(tài)碼, (通過(guò) request 域的
javax.servlet.error.status_code
參數(shù)) - 自定義錯(cuò)誤信息, (將我們自定義的錯(cuò)誤信息放到 request 域中, 并重寫(xiě) defaulterrorattributes 的 geterrorattributes 方法, 從 request 域中獲取這些信息).
- 自定義錯(cuò)誤頁(yè)面, (根據(jù) springboot 查找錯(cuò)誤頁(yè)面的邏輯來(lái)自定義錯(cuò)誤頁(yè)面: /error/500.html, /error/400.html, /error.html)
可以自己根據(jù)文章一步一步走一遍, 或者看我寫(xiě)好的演示項(xiàng)目先看看效果, 總是動(dòng)手實(shí)踐, 而不是收藏文章并封存。
演示項(xiàng)目地址:https://github.com/zhaojun1998/exception-handler-demo
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)服務(wù)器之家的支持。
原文鏈接:http://www.zhaojun.im/springboot-exception-expand/