程序中出現異常是普遍現象, Java 程序員想必早已習慣,根據控制臺輸出的異常信息,分析異常產生的原因,然后進行針對性處理的過程。
Spring Boot 項目中,數據持久層、服務層到控制器層都可能拋出異常。如果我們在各層都進行異常處理,程序代碼會顯得支離破碎,難以理解。
實際上,異常可以從內層向外層不斷拋出,最后在控制器層進行統一處理。 Spring Boot 提供了全局性的異常處理機制,本節我們就分別演示下,默認情況、控制器返回視圖、控制器返回 JSON 數據三種情況的異常處理方法。
Spring Boot 開發的 Web 項目具備默認的異常處理機制,無須編寫異常處理相關代碼,即可提供默認異常機制,下面具體演示下。
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-exception-default ,生成項目后導入 Eclipse 開發環境。
引入 Web 項目依賴即可。
實例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
我們在啟動項目, Spring Boot Web 項目默認啟動端口為 8080 ,所以直接訪問 http://127.0.0.1:8080 ,顯示如下:
Spring Boot 默認異常信息提示頁面
如上圖所示,Spring Boot 默認的異常處理機制生效,當出現異常時會自動轉向 /error 路徑。
在使用模板引擎開發 Spring Boot Web 項目時,控制器會返回視圖頁面。我們使用 Thymeleaf 演示控制器返回視圖時的異常處理方式,其他模板引擎處理方式也是相似的。
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-exception-controller,生成項目后導入 Eclipse 開發環境。
引入 Web 項目依賴、熱部署依賴。此處使用 Thymeleaf 演示控制器返回視圖時的異常處理方式,所以引入 Thymeleaf 依賴。
實例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- ThymeLeaf依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在異常處理之前,我們應該根據業務場景具體情況,定義一系列的異常類,習慣性的還會為各種異常分配錯誤碼,如下圖為支付寶開放平臺的公共錯誤碼信息。
支付寶開放平臺錯誤碼
本節我們為了演示,簡單的定義 2 個異常類,包含錯誤碼及錯誤提示信息。
實例:
/**
* 自定義異常
*/
public class BaseException extends Exception {
/**
* 錯誤碼
*/
private int code;
/**
* 錯誤提示信息
*/
private String msg;
public BaseException(int code, String msg) {
super();
this.code=code;
this.msg=msg;
}
// 省略get set
}
代碼塊1234567891011121314151617181920
實例:
/**
* 密碼錯誤異常
*/
public class PasswordException extends BaseException {
public PasswordException() {
super(10001, "密碼錯誤");
}
}
實例:
/**
* 驗證碼錯誤異常
*/
public class VerificationCodeException extends BaseException {
public VerificationCodeException() {
super(10002, "驗證碼錯誤");
}
}
定義控制器 GoodsController ,然后使用注解 @Controller 標注該類,類中方法的返回值即為視圖文件名。
在 GoodsController 類定義 4 個方法,分別用于正常訪問、拋出密碼錯誤異常、拋出驗證碼錯誤異常、拋出未自定義的異常,代碼如下。
實例:
/**
* 商品控制器
*/
@Controller
public class GoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public String goods() {
return "goods";// 跳轉到resource/templates/goods.html頁面
}
/**
* 拋出密碼錯誤異常的方法
*/
@RequestMapping("/checkPassword")
public String checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模擬拋出異常,便于測試
}
return "goods";
}
/**
* 拋出驗證碼錯誤異常的方法
*/
@RequestMapping("/checkVerification")
public String checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模擬拋出異常,便于測試
}
return "goods";
}
/**
* 拋出未自定義的異常
*/
@RequestMapping("/other")
public String other() throws Exception {
int a=1 / 0;// 模擬異常
return "goods";
}
}
@ControllerAdvice 注解標注的類可以處理 @Controller 標注的控制器類拋出的異常,然后進行統一處理。
實例:
/**
* 控制器異常處理類
*/
@ControllerAdvice(annotations=Controller.class) // 全局異常處理
public class ControllerExceptionHandler {
@ExceptionHandler({ BaseException.class }) // 當發生BaseException類(及其子類)的異常時,進入該方法
public ModelAndView baseExceptionHandler(BaseException e) {
ModelAndView mv=new ModelAndView();
mv.addObject("code", e.getCode());
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳轉到resource/templates/myerror.html頁面
return mv;
}
@ExceptionHandler({ Exception.class }) // 當發生Exception類的異常時,進入該方法
public ModelAndView exceptionHandler(Exception e) {
ModelAndView mv=new ModelAndView();
mv.addObject("code", 99999);// 其他異常統一編碼為99999
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳轉到resource/templates/myerror.html頁面
return mv;
}
}
按照 ControllerExceptionHandler 類的處理邏輯,當發生 BaseException 類型的異常時,會跳轉到 myerror.html 頁面,并顯示相應的錯誤碼和錯誤信息;當發生其他類型的異常時,錯誤碼為 99999 ,錯誤信息為相關的異常信息。
在 resource/templates 下分別新建 goods.html 和 myerror.html 頁面,作為正常訪問及發生異常時跳轉的視圖頁面。
實例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>goods.html頁面</title>
</head>
<body>
<div>商品信息頁面</div>
</body>
</html>
實例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>myerror.html頁面</title>
</head>
<body>
錯誤碼:
<span th:text="${code}"></span>
錯誤信息:
<span th:text="${message}"></span>
</body>
</html>
代碼塊12345678910111213
啟動項目,分別訪問控制器中的 4 個方法,結果如下:
訪問正常方法 /goods
訪問拋出自定義異常的方法 /checkPassword
訪問拋出自定義異常的方法 /checkVerification
訪問拋出未自定義異常的方法 /other
可見,當控制器方法拋出異常時,會按照全局異常類設定的邏輯統一處理。
在控制器類上添加 @RestController 注解,控制器方法處理完畢后會返回 JSON 格式的數據。
此時,可以使用 @RestControllerAdvice 注解標注的類 ,來捕獲 @RestController 標注的控制器拋出的異常。
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-exception-restcontroller,生成項目后導入 Eclipse 開發環境。
引入 Web 項目依賴、熱部署依賴即可。
實例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
還是使用上文中定義的異常類即可。
這時候,我們就需要思考一個問題了。前端請求后端控制器接口后,怎么區分后端接口是正常返回結果,還是發生了異常?
不論后端接口是正常執行,還是中間發生了異常,最好給前端返回統一的數據格式,便于前端統一分析處理。
OK,此時我們就可以封裝后端接口返回的業務邏輯對象 ResultBo ,代碼如下:
實例:
/**
* 后端接口返回的統一業務邏輯對象
*/
public class ResultBo<T> {
/**
* 錯誤碼 0表示沒有錯誤(異常) 其他數字代表具體錯誤碼
*/
private int code;
/**
* 后端返回消息
*/
private String msg;
/**
* 后端返回的數據
*/
private T data;
/**
* 無參數構造函數
*/
public ResultBo() {
this.code=0;
this.msg="操作成功";
}
/**
* 帶數據data構造函數
*/
public ResultBo(T data) {
this();
this.data=data;
}
/**
* 存在異常的構造函數
*/
public ResultBo(Exception ex) {
if (ex instanceof BaseException) {
this.code=((BaseException) ex).getCode();
this.msg=ex.getMessage();
} else {
this.code=99999;// 其他未定義異常
this.msg=ex.getMessage();
}
}
// 省略 get set
}
定義控制器 RestGoodsController ,并使用 @RestController 注解標注。在其中定義 4 個方法,然后分別用于正常訪問、拋出密碼錯誤異常、拋出驗證碼錯誤異常,以及拋出不屬于自定義異常類的異常。
實例:
/**
* Rest商品控制器
*/
@RestController
public class RestGoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public ResultBo goods() {
return new ResultBo<>(new ArrayList());// 正常情況下應該返回商品列表
}
/**
* 拋出密碼錯誤異常的方法
*/
@RequestMapping("/checkPassword")
public ResultBo checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模擬拋出異常,便于測試
}
return new ResultBo<>(true);// 正常情況下應該返回檢查密碼的結果true或false
}
/**
* 拋出驗證碼錯誤異常的方法
*/
@RequestMapping("/checkVerification")
public ResultBo checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模擬拋出異常,便于測試
}
return new ResultBo<>(true);// 正常情況下應該返回檢查驗證碼的結果true或false
}
/**
* 拋出未自定義的異常
*/
@RequestMapping("/other")
public ResultBo other() throws Exception {
int a=1 / 0;// 模擬異常
return new ResultBo<>(true);
}
}
@RestControllerAdvice 注解標注的類可以處理 RestController 控制器類拋出的異常,然后進行統一處理。
實例:
/**
* Rest控制器異常處理類
*/
@RestControllerAdvice(annotations=RestController.class) // 全局異常處理
public class RestControllerExceptionHandler {
/**
* 處理BaseException類(及其子類)的異常
*/
@ExceptionHandler({ BaseException.class })
public ResultBo baseExceptionHandler(BaseException e) {
return new ResultBo(e);
}
/**
* 處理Exception類的異常
*/
@ExceptionHandler({ Exception.class })
public ResultBo exceptionHandler(Exception e) {
return new ResultBo(e);
}
}
啟動項目,分別嘗試訪問控制器中的 4 個接口,結果如下。
訪問正常方法 /goods
訪問拋出異常的方法 /checkPassword
訪問拋出異常的方法 /checkVerification
訪問拋出異常的方法 /other
Spring Boot 的默認異常處理機制,實際上只能做到提醒開發者 “這個后端接口不存在” 的作用,作用非常有限。
所以我們在開發 Spring Boot 項目時,需要根據項目的實際情況,定義各類異常,并站在全局的角度統一處理異常。
不管項目有多少層次,所有異常都可以向外拋出,直到控制器層進行集中處理。
于數千個項目,通過收集和分析大量數據選出了 10 大 JavaScript 錯誤,對其產生的根源,以及如何避免再發生這些錯誤進行剖析。實際開發中,如果能夠避免這些錯誤,就可以成為更好的開發者。
這里只關注影響面最大的那些錯誤。為此,我們統計了錯誤在各個公司的項目中發生的次數,而不是錯誤發生的總次數,因為如果是這樣的話,讀者就可能看到大量與他們不相干的統計信息。
以下是排名靠前的 10 大 JavaScript 錯誤:
出于可讀性方面的考慮,每個錯誤的描述經過精簡。
如果你是一名 JavaScript 開發者,對這個錯誤可能已經熟視無睹。在 Chrome 里讀取未定義對象的屬性或調用未定義對象的方法時就會發生這個錯誤,在 Chrome 開發者控制臺可以很容易地重現這個錯誤。
發生這個錯誤的原因有很多,其中最為常見的是,在渲染 UI 組件時沒有正確初始化狀態。我們通過一個真實的例子來看看這個錯誤是怎么發生的。我們選擇 React 作為示例,不過在其他框架(Angular、Vue 等)中也是一樣的。
class Quiz extends Component { componentWillMount() { axios.get('/thedata').then(res=> { this.setState({items: res.data}); }); } render() { return ( <ul> {this.state.items.map(item=> <li key={item.id}>{item.name}</li> )} </ul> ); } }
這里要注意兩件事:
組件的狀態(如 this.state)在一開始就是 undefined。
如果是通過異步的方式來加載數據,那么在數據加載進來之前,至少要渲染一次組件——不管是在構造器、componentWillMout() 還是 componentDidMout() 中加載數據。Quiz 在進行第一次渲染時,this.state.items 是 undefined,那么 ItemList 就會得到 undefined 的數據項,這樣就會在控制臺看到這個錯誤——“Uncaught TypeError:Cannot read property ‘map’ of undefined”。
要解決這個問題其實很簡單,在構造器里使用適當的默認值進行初始化。
class Quiz extends Component { // 增加這個: constructor(props) { super(props); // 使用空數組給 state 賦值 this.state={ items: [] }; } componentWillMount() { axios.get('/thedata').then(res=> { this.setState({items: res.data}); }); } render() { return ( <ul> {this.state.items.map(item=> <li key={item.id}>{item.name}</li> )} </ul> ); } }
在 Safari 里讀取未定義對象的屬性或調用未定義對象的方法時就會發生這個錯誤,在 Safari 開發者控制臺可以很容易地重現這個錯誤。這個錯誤與發生在 Chrome 里的是差不多的,只是 Safari 為它提供了不同的錯誤信息。
在 Safari 里讀取空(null)對象的屬性或調用空對象的方法時就會發生這個錯誤,在 Safari 開發者控制臺可以很容易地重現這個錯誤。
有意思的是,在 JavaScript 里,null 和 undefined 其實是不一樣的,所以我們會看到兩個不同的錯誤消息。undefined 表示未賦值的變量,而 null 表示變量值為空。可以使用嚴格等于號來證明它們不是同一個東西。
在實際應用當中,在 JavaScript 里調用一個未加載的 DOM 元素就會出現這個錯誤。如果對象為空,DOM API 就會返回 null。
DOM 元素需要在創建之后才能被訪問。JavaScript 代碼是按照從上到下的順序進行解析的,所以,如果在 DOM 元素之前有一個標簽包含了 JavaScript 代碼,瀏覽器在解析 HTML 時就會執行這些代碼。在加載 JavaScript 之前,如果 DOM 元素沒有被創建,就會出現這個錯誤。
在這個例子里,我們可以通過添加一個事件監聽器來解決這個問題,在頁面加載完畢時,事件監聽器會通知我們。在 addEventListener 被觸發之后,init() 方法就可以大膽地訪問 DOM 元素了。
<script> function init() { var myButton=document.getElementById("myButton"); var myTextfield=document.getElementById("myTextfield"); myButton.onclick=function() { var userName=myTextfield.value; } } document.addEventListener('readystatechange', function() { if (document.readyState==="complete") { init(); } }); </script> <form> <input type="text" id="myTextfield" placeholder="Type your name" /> <input type="button" id="myButton" value="Go" /> </form>
跨域的未捕捉 JavaScript 異常會變成 Script error。例如,假設 JavaScript 托管在 CDN 上,那么未捕捉的錯誤(錯誤沒有在 try-catch 里被捕獲,一路直上到了 window.onerror 里)就會顯示成“Script error”,而不是顯示具體的錯誤消息。這是瀏覽器出于安全方面的考慮,防止跨域傳遞數據。
要想獲得具體的錯誤信息,可以這樣做:
1). 使用 Access-Control-Allow-Origin
將 Access-Control-Allow-Origin 設置成“*”,表示該資源可以被任何一個域訪問。如果有必要,可以把“*”替換成你的域名,例如 Access-Control-Allow-Origin: www.example.com。不過,如果使用了 CDN,那么要支持多個域名可能就會得不償失,因為 CDN 存在緩存問題。
下面是在各種環境如何設置該字段的示例:
Apache:
在 JavaScript 文件所在的目錄創建一個叫作.htaccess 的文件,并加入如下內容:
Header add Access-Control-Allow-Origin "*"
Nginx:
在 JavaScript 對應的 location 配置代碼塊中加入 add_header 指令:
location ~ ^/assets/ { add_header Access-Control-Allow-Origin *; }
HAProxy:
在 JavaScript 文件對應的 backend 配置塊中加入如下內容:
rspadd Access-Control-Allow-Origin:\ *
2). 在 script 標簽里設置 crossorigin=“anonymous”
在每個設置了 Access-Control-Allow-Origin 字段的 HTML 頁面里,將它們的 script 標簽的 crossorigin 屬性設置為“anonymous”。在 Firefox 里,如果出現了 crossorigin,但沒有設置 Access-Control-Allow-Origin,JavaScript 腳本就不會被執行。
在 IE 里讀取未定義對象的屬性或調用未定義對象的方法時就會發生這個錯誤,在 IE 開發者控制臺可以很容易地重現這個錯誤。
這個錯誤與 Chrome 里的“TypeError: ‘undefined’ is not a function”是同一個東西。不同的瀏覽器為相同的錯誤提供的錯誤消息可能是不一樣的。
在 IE 里使用 JavaScript 的命名空間時,就很容易碰到這個錯誤。發生這個錯誤十有八九是因為 IE 無法將當前命名空間里的方法綁定到 this 關鍵字上。例如,假設有個命名空間 Rollbar,它有一個方法叫 isAwesome()。在 Rollbar 命名空間中,可以直接使用 this 關鍵字來調用這個方法:
this.isAwesome();
在 Chrome、Firefox 和 Opera 中這樣做都是沒有問題的,但在 IE 中就不行。所以,最安全的做法是指定全命名空間:
Rollbar.isAwesome();
在 Chrome 里調用一個未定義的函數時就會發生這個錯誤,可以在 Chrome 開發者控制臺和 Mozilla 開發者控制臺重現這個錯誤。
近年來,JavaScript 的編碼技術和設計模式變得日趨復雜,回調和閉包中的自引用情況越來越普遍,讓人搞不清楚代碼中的 this/that 表示的是什么意思。
比如下面這段代碼:
function testFunction() { this.clearLocalStorage(); this.timer=setTimeout(function() { this.clearBoard(); // 這里的”this"是指什么? }, 0); };
執行上面的代碼會出現這樣的錯誤:“Uncaught TypeError: undefined is not a function”。因為在調用 setTimeout() 方法時,實際上是在調用 window.setTimeout()。傳給 setTimeout() 的匿名函數的上下文實際上是 window,而 window 并不包含 clearBoard() 方法。
對于舊瀏覽器,以往的解決辦法是將 this 賦值給某個變量,然后在閉包里使用這個變量。例如:
function testFunction () { this.clearLocalStorage(); var self=this; // 將 this 賦值給 self this.timer=setTimeout(function(){ self.clearBoard(); }, 0); };
在新瀏覽器中,可以使用 bind() 方法來傳遞引用:
function testFunction () { this.clearLocalStorage(); this.timer=setTimeout(this.reset.bind(this), 0); // 綁定到 'this' }; function testFunction(){ this.clearBoard(); // 以’this’作為上下文 };
在 Chrome 里,有幾種情況會發生這個錯誤,其中一個就是無限遞歸調用一個函數。這個錯誤可以在 Chrome 開發者控制臺重現。
當傳給函數的值超出可接受的范圍時也會出現這個錯誤。很多函數只接受指定范圍的數值,例如,Number.toExponential(digits) 和 Number.toFixed(digits) 只接受 0 到 20 的數值,而 Number.toPrecision(digits) 只接受 1 到 21 的數值。
var a=new Array(4294967295); //OK var b=new Array(-1); //range error var num=2.555555; document.writeln(num.toExponential(4)); //OK document.writeln(num.toExponential(-2)); //range error! num=2.9999; document.writeln(num.toFixed(2)); //OK document.writeln(num.toFixed(25)); //range error! num=2.3456; document.writeln(num.toPrecision(1)); //OK document.writeln(num.toPrecision(22)); //range error!
在 Chrome 里讀取 undefined 變量的 length 屬性時會發生這個錯誤,這個錯誤可以在 Chrome 開發者控制臺重現。
length 是數組的屬性,但如果數組沒有初始化或者數組的變量名被另一個上下文隱藏起來的話,訪問 length 屬性就會發生這個錯誤。例如:
var testArray=["Test"]; function testFunction(testArray) { for (var i=0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();
函數的參數名會覆蓋全局的變量名。也就是說,全局的 testArray 被函數的參數名覆蓋了,所以在函數體里訪問到的是本地的 testArray,但本地并沒有定義 testArray,所以出現了這個錯誤。
有兩種方法可用于解決這個問題:
1). 將函數的參數名移除(這就表示函數里要訪問的變量已經在函數外面定義好了,所以函數不需要參數):
var testArray=["Test"]; /* 前提是要在函數外面定義好 testArray */ function testFunction(/* No params */) { for (var i=0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();
2). 在調用函數時將變量傳遞進去:
var testArray=["Test"]; function testFunction(testArray) { for (var i=0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction(testArray);
我們無法對 undefined 變量進行賦值或讀取操作,否則的話會拋出“Uncaught TypeError: cannot set property of undefined”異常。
例如,在 Chrome 中:
如果 test 對象不存在,就會拋出“Uncaught TypeError: cannot set property of undefined”異常。
在訪問一個未定義的對象或超出當前作用域的對象時就會發生這個錯誤,這個錯誤可以在 Chrome 開發者控制臺重現。
如果在進行事件處理時遇到這個錯誤,請確保事件對象被作為參數傳入到函數當中。舊瀏覽器(IE)提供了全局的 event 變量,但并不是所有的瀏覽器都會這樣。盡管 jQuery 嘗試對這種行為進行規范化,但最好還是使用傳給函數的 event 對象:
function myFunction(event) { event=event.which || event.keyCode; if(event.keyCode===13){ alert(event.keyCode); } }
我們希望這些內容能夠幫助大家在未來避免這些錯誤,解決大家的痛點。不過,即使有了這些最佳實踐,在生產環境中仍然會出現各種不可預期的錯誤。關鍵是要及時發現那些影響用戶體驗的錯誤,并使用適當的工具快速解決這些問題。
原文(英文)地址 | https://rollbar.com/blog/top-10-javascript-errors/
質文章,及時送達
作者 | 嘟嘟MD
來源 | tengj.top/2018/05/16/springboot13
前言
今天來一起學習一下Spring Boot中的異常處理,在日常web開發中發生了異常,往往是需要通過一個統一的異常處理來保證客戶端能夠收到友好的提示。
正文
本篇要點如下:
介紹Spring Boot默認的異常處理機制
如何自定義錯誤頁面
通過@ControllerAdvice注解來處理異常
默認情況下,Spring Boot為兩種情況提供了不同的響應方式。
一種是瀏覽器客戶端請求一個不存在的頁面或服務端處理發生異常時,一般情況下瀏覽器默認發送的請求頭中Accept: text/html,所以Spring Boot默認會響應一個html文檔內容,稱作“Whitelabel Error Page”
另一種是使用Postman等調試工具發送請求一個不存在的url或服務端處理發生異常時,Spring Boot會返回類似如下的Json格式字符串信息
{
"timestamp": "2018-05-12T06:11:45.209+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/index.html"
}
原理也很簡單,Spring Boot 默認提供了程序出錯的結果映射路徑/error。這個/error請求會在BasicErrorController中處理,其內部是通過判斷請求頭中的Accept的內容是否為text/html來區分請求是來自客戶端瀏覽器(瀏覽器通常默認自動發送請求頭內容Accept:text/html)還是客戶端接口的調用,以此來決定返回頁面視圖還是 JSON 消息內容。
相關BasicErrorController中代碼如下:
好了,了解完Spring Boot默認的錯誤機制后,我們來點有意思的,瀏覽器端訪問的話,任何錯誤Spring Boot返回的都是一個Whitelabel Error Page
的錯誤頁面,這個很不友好,所以我們可以自定義下錯誤頁面。
1、先從最簡單的開始,直接在/resources/templates
下面創建error.html就可以覆蓋默認的Whitelabel Error Page
的錯誤頁面,我項目用的是thymeleaf模板,對應的error.html代碼如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
動態error錯誤頁面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>
這樣運行的時候,請求一個不存在的頁面或服務端處理發生異常時,展示的自定義錯誤界面如下:
2、此外,如果你想更精細一點,根據不同的狀態碼返回不同的視圖頁面,也就是對應的404,500等頁面,這里分兩種,錯誤頁面可以是靜態HTML(即,添加到任何靜態資源文件夾下),也可以使用模板構建,文件的名稱應該是確切的狀態碼。
如果只是靜態HTML頁面,不帶錯誤信息的,在resources/public/下面創建error目錄,在error目錄下面創建對應的狀態碼html即可 ,例如,要將404映射到靜態HTML文件,您的文件夾結構如下所示:
靜態404.html簡單頁面如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
靜態404錯誤頁面
</body>
</html>
這樣訪問一個錯誤路徑的時候,就會顯示靜態404錯誤頁面
錯誤頁面
注:這時候如果存在上面第一種介紹的error.html頁面,則狀態碼錯誤頁面將覆蓋error.html,具體狀態碼錯誤頁面優先級比較高。
如果是動態模板頁面,可以帶上錯誤信息,在resources/templates/
下面創建error目錄,在error目錄下面命名即可:
這里我們模擬下500錯誤,控制層代碼,模擬一個除0的錯誤:
@Controller
publicclassBaseErrorControllerextendsAbstractController{
private Logger logger=LoggerFactory.getLogger(this.getClass);
@RequestMapping(value="/ex")
@ResponseBody
public String error{
int i=5/0;
return "ex";
}
}
500.html代碼:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
動態500錯誤頁面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>
這時訪問 http://localhost:8080/spring/ex 即可看到如下錯誤,說明確實映射到了500.html
注:如果同時存在靜態頁面500.html和動態模板的500.html,則后者覆蓋前者。即
templates/error/
這個的優先級比resources/public/error
高。
整體概括上面幾種情況,如下:
error.html會覆蓋默認的 whitelabel Error Page 錯誤提示
靜態錯誤頁面優先級別比error.html高
動態模板錯誤頁面優先級比靜態錯誤頁面高
3、上面介紹的只是最簡單的覆蓋錯誤頁面的方式來自定義,如果對于某些錯誤你可能想特殊對待,則可以這樣
@Configuration
publicclassContainerConfig{
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer{
return new EmbeddedServletContainerCustomizer{
@Override
publicvoidcustomize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500"));
}
};
}
}
上面這段代碼中HttpStatus.INTERNAL_SERVER_ERROR
就是對應500錯誤碼,也就是說程序如果發生500錯誤,就會將請求轉發到/error/500
這個映射來,那我們只要實現一個方法是對應這個/error/500
映射即可捕獲這個異常做出處理
@RequestMapping("/error/500")
@ResponseBody
public String showServerError {
return "server error";
}
這樣,我們再請求前面提到的異常請求 http://localhost:8080/spring/ex 的時候,就會被我們這個方法捕獲了。
這里我們就只對500做了特殊處理,并且返還的是字符串,如果想要返回視圖,去掉 @ResponseBody注解,并返回對應的視圖頁面。如果想要對其他狀態碼自定義映射,在customize方法中添加即可。
上面這種方法雖然我們重寫了/500映射,但是有一個問題就是無法獲取錯誤信息,想獲取錯誤信息的話,我們可以繼承BasicErrorController或者干脆自己實現ErrorController接口,除了用來響應/error這個錯誤頁面請求,可以提供更多類型的錯誤格式等(BasicErrorController在上面介紹SpringBoot默認異常機制的時候有提到)
這里博主選擇直接繼承BasicErrorController,然后把上面 /error/500
映射方法添加進來即可
@Controller
public class MyBasicErrorController extends BasicErrorController {
public MyBasicErrorController {
super(new DefaultErrorAttributes, new ErrorProperties);
}
/**
* 定義500的ModelAndView
* @param request
* @param response
* @return
*/
@RequestMapping(produces="text/html",value="/500")
public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {
response.setStatus(getStatus(request).value);
Map<String, Object> model=getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
model.put("msg","自定義錯誤信息");
return new ModelAndView("error/500", model);
}
/**
* 定義500的錯誤JSON信息
* @param request
* @return
*/
@RequestMapping(value="/500")
@ResponseBody
public ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {
Map<String, Object> body=getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
HttpStatus status=getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
}
代碼也很簡單,只是實現了自定義的500錯誤的映射解析,分別對瀏覽器請求以及json請求做了回應。
BasicErrorController默認對應的@RequestMapping是/error
,固我們方法里面對應的@RequestMapping(produces="text/html",value="/500")
實際上完整的映射請求是/error/500
,這就跟上面 customize 方法自定義的映射路徑對上了。
errorHtml500 方法中,我返回的是模板頁面,對應/templates/error/500.html,這里順便自定義了一個msg信息,在500.html也輸出這個信息<p th:text="${msg}"></p>
,如果輸出結果有這個信息,則表示我們配置正確了。
再次訪問請求http://localhost:8080/spring/ex ,結果如下
Tips:大家可以關注微信公眾號:Java后端,獲取更多推送。
Spring Boot提供的ErrorController是一種全局性的容錯機制。此外,你還可以用@ControllerAdvice注解和@ExceptionHandler注解實現對指定異常的特殊處理。
這里介紹兩種情況:
局部異常處理 @Controller + @ExceptionHandler
全局異常處理 @ControllerAdvice + @ExceptionHandler
局部異常處理 @Controller + @ExceptionHandler
局部異常主要用到的是@ExceptionHandler注解,此注解注解到類的方法上,當此注解里定義的異常拋出時,此方法會被執行。如果@ExceptionHandler所在的類是@Controller,則此方法只作用在此類。如果@ExceptionHandler所在的類帶有@ControllerAdvice注解,則此方法會作用在全局。
該注解用于標注處理方法處理那些特定的異常。被該注解標注的方法可以有以下任意順序的參數類型:
Throwable、Exception 等異常對象;
ServletRequest、HttpServletRequest、ServletResponse、HttpServletResponse;
HttpSession 等會話對象;
org.springframework.web.context.request.WebRequest;
java.util.Locale;
java.io.InputStream、java.io.Reader;
java.io.OutputStream、java.io.Writer;
org.springframework.ui.Model;
并且被該注解標注的方法可以有以下的返回值類型可選:
ModelAndView;
org.springframework.ui.Model;
java.util.Map;
org.springframework.web.servlet.View;
@ResponseBody 注解標注的任意對象;
HttpEntity<?> or ResponseEntity<?>;
void;
以上羅列的不完全,更加詳細的信息可參考:Spring ExceptionHandler。
舉個簡單例子,這里我們對除0異常用@ExceptionHandler來捕捉。
@Controller
publicclassBaseErrorControllerextendsAbstractController{
private Logger logger=LoggerFactory.getLogger(this.getClass);
@RequestMapping(value="/ex")
@ResponseBody
public String error{
int i=5/0;
return "ex";
}
//局部異常處理
@ExceptionHandler(Exception.class)
@ResponseBody
public String exHandler(Exception e){
// 判斷發生異常的類型是除0異常則做出響應
if(e instanceof ArithmeticException){
return "發生了除0異常";
}
// 未知的異常做出響應
return "發生了未知異常";
}
}
全局異常處理 @ControllerAdvice + @ExceptionHandler
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定義@ExceptionHandler、@InitBinder、@ModelAttribute,并應用到所有@RequestMapping中。
簡單的說,進入Controller層的錯誤才會由@ControllerAdvice處理,攔截器拋出的錯誤以及訪問錯誤地址的情況@ControllerAdvice處理不了,由SpringBoot默認的異常處理機制處理。
我們實際開發中,如果是要實現RESTful API,那么默認的JSON錯誤信息就不是我們想要的,這時候就需要統一一下JSON格式,所以需要封裝一下。
/**
* 返回數據
*/
public class AjaxObject extends HashMap<String, Object> {
private static final long serialVersionUID=1L;
publicAjaxObject {
put("code", 0);
}
public static AjaxObject error {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知異常,請聯系管理員");
}
public static AjaxObject error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static AjaxObject error(int code, String msg) {
AjaxObject r=new AjaxObject;
r.put("code", code);
r.put("msg", msg);
return r;
}
public static AjaxObject ok(String msg) {
AjaxObject r=new AjaxObject;
r.put("msg", msg);
return r;
}
public static AjaxObject ok(Map<String, Object> map) {
AjaxObject r=new AjaxObject;
r.putAll(map);
return r;
}
public static AjaxObject ok {
return new AjaxObject;
}
public AjaxObject put(String key, Object value) {
super.put(key, value);
return this;
}
public AjaxObject data(Object value) {
super.put("data", value);
return this;
}
public static AjaxObject apiError(String msg) {
return error(1, msg);
}
}
上面這個AjaxObject就是我平時用的,如果是正確情況返回的就是:
{
code:0,
msg:“獲取列表成功”,
data:{
queryList :
}
}
正確默認code返回0,data里面可以是集合,也可以是對象,如果是異常情況,返回的json則是:
{
code:500,
msg:“未知異常,請聯系管理員”
}
然后創建一個自定義的異常類:
public classBusinessExceptionextendsRuntimeExceptionimplementsSerializable{
private static final long serialVersionUID=1L;
private String msg;
private int code=500;
publicBusinessException(String msg) {
super(msg);
this.msg=msg;
}
publicBusinessException(String msg, Throwable e) {
super(msg, e);
this.msg=msg;
}
publicBusinessException(int code,String msg) {
super(msg);
this.msg=msg;
this.code=code;
}
publicBusinessException(String msg, int code, Throwable e) {
super(msg, e);
this.msg=msg;
this.code=code;
}
public String getMsg {
return msg;
}
publicvoidsetMsg(String msg) {
this.msg=msg;
}
publicintgetCode {
return code;
}
publicvoidsetCode(int code) {
this.code=code;
}
}
注:spring 對于 RuntimeException 異常才會進行事務回滾
Controler中添加一個json映射,用來處理這個異常
@Controller
public class BaseErrorController{
@RequestMapping("/json")
public void json(ModelMap modelMap) {
System.out.println(modelMap.get("author"));
int i=5/0;
}
}
最后創建這個全局異常處理類:
/**
* 異常處理器
*/
@RestControllerAdvice
publicclassBusinessExceptionHandler{
private Logger logger=LoggerFactory.getLogger(getClass);
/**
* 應用到所有@RequestMapping注解方法,在其執行之前初始化數據綁定器
* @param binder
*/
@InitBinder
publicvoidinitBinder(WebDataBinder binder) {
System.out.println("請求有參數才進來");
}
/**
* 把值綁定到Model中,使全局@RequestMapping可以獲取到該值
* @param model
*/
@ModelAttribute
publicvoidaddAttributes(Model model) {
model.addAttribute("author", "嘟嘟MD");
}
@ExceptionHandler(Exception.class)
public Object handleException(Exception e,HttpServletRequest req){
AjaxObject r=new AjaxObject;
//業務異常
if(e instanceof BusinessException){
r.put("code", ((BusinessException) e).getCode);
r.put("msg", ((BusinessException) e).getMsg);
}else{//系統異常
r.put("code","500");
r.put("msg","未知異常,請聯系管理員");
}
//使用HttpServletRequest中的header檢測請求是否為ajax, 如果是ajax則返回json, 如果為非ajax則返回view(即ModelAndView)
String contentTypeHeader=req.getHeader("Content-Type");
String acceptHeader=req.getHeader("Accept");
String xRequestedWith=req.getHeader("X-Requested-With");
if ((contentTypeHeader !=&& contentTypeHeader.contains("application/json"))
|| (acceptHeader !=&& acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return r;
} else {
ModelAndView modelAndView=new ModelAndView;
modelAndView.addObject("msg", e.getMessage);
modelAndView.addObject("url", req.getRequestURL);
modelAndView.addObject("stackTrace", e.getStackTrace);
modelAndView.setViewName("error");
return modelAndView;
}
}
}
@ExceptionHandler 攔截了異常,我們可以通過該注解實現自定義異常處理。其中,@ExceptionHandler 配置的 value 指定需要攔截的異常類型,上面我配置了攔截Exception,
再根據不同異常類型返回不同的相應,最后添加判斷,如果是Ajax請求,則返回json,如果是非ajax則返回view,這里是返回到error.html頁面。
為了展示錯誤的時候更友好,我封裝了下error.html,不僅展示了錯誤,還添加了跳轉百度谷歌以及StackOverFlow的按鈕,如下:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">
<head>
<title>Spring Boot管理后臺</title>
<script type="text/javascript">
</script>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<div id="navbar">
<h1>系統異常統一處理</h1>
<h3 th:text="'錯誤信息:'+${msg}"></h3>
<h3 th:text="'請求地址:'+${url}"></h3>
<h2>Debug</h2>
<a th:href="@{'https://www.google.com/webhp?hl=zh-CN#safe=strict&hl=zh-CN&q='+${msg}}"
class="btn btn-primary btn-lg" target="_blank" id="Google">Google</a>
<a th:href="@{'https://www.baidu.com/s?wd='+${msg}}" class="btn btn-info btn-lg" target="_blank" id="Baidu">Baidu</a>
<a th:href="@{'http://stackoverflow.com/search?q='+${msg}}"
class="btn btn-default btn-lg" target="_blank" id="StackOverFlow">StackOverFlow</a>
<h2>異常堆棧跟蹤日志StackTrace</h2>
<div th:each="line:${stackTrace}">
<div th:text="${line}"></div>
</div>
</div>
</div>
<div layout:fragment="js" th:remove="tag">
</div>
</body>
</html>
訪問http://localhost:8080/json的時候,因為是瀏覽器發起的,返回的是error界面:
如果是ajax請求,返回的就是錯誤:
{ "msg":"未知異常,請聯系管理員", "code":500 }
這里我給帶@ModelAttribute注解的方法通過Model設置了author值,在json映射方法中通過 ModelMwap 獲取到改值。
認真的你可能發現,全局異常類我用的是@RestControllerAdvice,而不是@ControllerAdvice,因為這里返回的主要是json格式,這樣可以少寫一個@ResponseBody。
總結
到此,SpringBoot中對異常的使用也差不多全了,本項目中處理異常的順序會是這樣,當發送一個請求:
1.攔截器那邊先判斷是否登錄,沒有則返回登錄頁。
2.在進入Controller之前,譬如請求一個不存在的地址,返回404錯誤界面。
3.在執行@RequestMapping時,發現的各種錯誤(譬如數據庫報錯、請求參數格式錯誤/缺失/值非法等)統一由@ControllerAdvice處理,根據是否Ajax返回json或者view。
-END-
*請認真填寫需求信息,我們會在24小時內與您取得聯系。