通用異常處理
在web層的方法中如果出現異常,SpringMVC會自動幫我們處理,並向前端返回500狀態碼以及錯誤信息。但是這樣的錯誤信息是不合理的,我們應該自行處理異常,讓用戶看到一個相對友好的頁面。
如何處理統一異常
我們在學習Spring的時候,了解過AOP的概念,利用AOP可以幫助我們處理全局異常。但是切面切點這些的配置比較繁瑣,SpringMVC為我們提供了簡單的異常處理的方法。
案例
項目代碼:
package com.leyou.web;
import com.leyou.pojo.Goods;
import com.leyou.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/6 8:41 下午
* @Description:
*/
@RestController("goods")
public class GoodsController {
@Autowired
private GoodsService goodsService;
@PostMapping
public ResponseEntity<Goods> saveGoods(Goods goods){
//校驗價格是否為空
if(goods.getPrice() == null){
throw new RuntimeException("商品價格不能為空");
}
Goods goods1 = goodsService.saveGoods(goods);
return ResponseEntity.status(HttpStatus.CREATED).body(goods1);
}
}
異常處理:
我們要使用之前需要先導入SpringMVC的jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>x.x</version>
</dependency>
創建異常處理類:
package com.leyou.common.advice;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:06 上午
* @Description:
*/
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> exceptionHandler(RuntimeException e){
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
//還可以聲明其他類型異常的處理。。。
}
要注意的是:異常處理類必須要被Spring掃描。
此時不同於傳統的異常只能返回500、錯誤信息含糊不清、響應體體為空。我們的統一異常處理的返回內容會更加清晰。使用postman查詢接口后,返回響應體:商品價格不能為空
優化
此時雖然能實現統一異常處理,但是狀態碼仍然存在硬編碼問題(同一類異常返回的都是同一個狀態碼),下面我們就來優化一下上面的案例:
創建異常信息枚舉
枚舉中存放了若干個該類型的實例,並且枚舉是無法實例化的,因為其構造默認是private修飾的,所以可以粗淺的理解為枚舉就是存放果然當前類個實例的類。
-
枚舉的實例聲明可以簡寫為構造加參數的形式
PRICE_COUNT_BE_NULL(400,"價格不能為空"),
多個枚舉實例之間以逗號間隔,最后一個實例以分號結束。 -
枚舉的實例必須在屬性之前聲明。
package com.leyou.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:28 上午
* @Description: 錯誤異常信息枚舉
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionEnums {
PRICE_COUNT_BE_NULL(400,"價格不能為空"),
PRICE_NAME_BE_NULL(400,"價格名稱能為空"),
PRICE_ID_BE_NULL(400,"價格不能為空"),
PAGE_NOT_FOUND(404,"頁面未找到")
;
private Integer code;
private String msg;
}
自定義異常
由於java提供的異常類無法封裝狀態碼信息,我們需要自定義一個異常,用於異常處理。
package com.leyou.common.exception;
import com.leyou.common.enums.ExceptionEnums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:37 上午
* @Description:
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MyRuntimeExcpetion extends RuntimeException{
private ExceptionEnums exceptionEnums;
}
使用自定義異常優化異常處理類
package com.leyou.common.advice;
import com.leyou.common.enums.ExceptionEnums;
import com.leyou.common.exception.MyRuntimeExcpetion;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:06 上午
* @Description: 通用異常處理
*/
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(MyRuntimeExcpetion.class)
@ResponseBody
public ResponseEntity<String> exceptionHandler(MyRuntimeExcpetion ex){
//獲取枚舉
ExceptionEnums em = ex.getExceptionEnums();
//返回異常信息
return ResponseEntity.status(em.getCode()).body(em.getMsg());
}
}
業務代碼優化
package com.leyou.web;
import com.leyou.common.enums.ExceptionEnums;
import com.leyou.common.exception.MyRuntimeExcpetion;
import com.leyou.pojo.Goods;
import com.leyou.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/6 8:41 下午
* @Description:
*/
@RestController("goods")
public class GoodsController {
@Autowired
private GoodsService goodsService;
@PostMapping
public ResponseEntity<Goods> saveGoods(Goods goods){
//校驗價格是否為空
if(goods.getPrice() == null){
throw new MyRuntimeExcpetion(ExceptionEnums.PRICE_COUNT_BE_NULL);
}
Goods goods1 = goodsService.saveGoods(goods);
return ResponseEntity.status(HttpStatus.CREATED).body(goods1);
}
}
此時已經可以完成狀態碼和異常信息的自定義了,但是此時對異常的信息還是不滿意,只能返回簡單的字符串提示。我們繼續優化!
封裝異常信息類
package com.leyou.common.vo;
import com.leyou.common.enums.ExceptionEnums;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:58 上午
* @Description: 異常信息類
*/
@Data
public class ExceptionResult {
/**
* 狀態碼
*/
private Integer status;
/**
* 異常信息
*/
private String message;
/**
* 時間戳
*/
private Long timestamp;
public ExceptionResult(ExceptionEnums em){
this.status = em.getCode();
this.message = em.getMsg();
this.timestamp = System.currentTimeMillis();
}
}
再次優化通用異常處理
package com.leyou.common.advice;
import com.leyou.common.enums.ExceptionEnums;
import com.leyou.common.exception.MyRuntimeExcpetion;
import com.leyou.common.vo.ExceptionResult;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/7 11:06 上午
* @Description: 通用異常處理
*/
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(MyRuntimeExcpetion.class)
@ResponseBody
public ResponseEntity<ExceptionResult> exceptionHandler(MyRuntimeExcpetion ex){
//獲取枚舉
ExceptionEnums em = ex.getExceptionEnums();
//返回異常信息
return ResponseEntity.status(em.getCode()).body(new ExceptionResult(em));
}
}
除外ControllerAdvice注解還有如下兩個作用
全局數據綁定
全局數據綁定功能可以用來做一些初始化的數據操作,我們可以將一些公共的數據定義在添加了 @ControllerAdvice 注解的類中,這樣,在每一個 Controller 的接口中,就都能夠訪問導致這些數據。
使用步驟,首先定義全局數據,如下:
@ControllerAdvice
public class MyGlobalExceptionHandler {
@ModelAttribute(name = "md")
public Map<String,Object> mydata() {
HashMap<String, Object> map = new HashMap<>();
map.put("age", 99);
map.put("gender", "男");
return map;
}
}
使用 @ModelAttribute 注解標記該方法的返回數據是一個全局數據,默認情況下,這個全局數據的 key 就是返回的變量名,value 就是方法返回值,當然開發者可以通過 @ModelAttribute 注解的 name 屬性去重新指定 key。
定義完成后,在任何一個Controller 的接口中,都可以獲取到這里定義的數據:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(Model model) {
Map<String, Object> map = model.asMap();
System.out.println(map);
int i = 1 / 0;
return "hello controller advice";
}
}
全局數據預處理
考慮我有兩個實體類,Book 和 Author,分別定義如下:
public class Book {
private String name;
private Long price;
//getter/setter
}
public class Author {
private String name;
private Integer age;
//getter/setter
}
此時,如果我定義一個數據添加接口,如下:
@PostMapping("/book")
public void addBook(Book book, Author author) {
System.out.println(book);
System.out.println(author);
}
這個時候,添加操作就會有問題,因為兩個實體類都有一個 name 屬性,從前端傳遞時 ,無法區分。此時,通過 @ControllerAdvice 的全局數據預處理可以解決這個問題
解決步驟如下:
1.給接口中的變量取別名
@PostMapping("/book")
public void addBook(@ModelAttribute("b") Book book, @ModelAttribute("a") Author author) {
System.out.println(book);
System.out.println(author);
}
2.進行請求數據預處理
在 @ControllerAdvice 標記的類中添加如下代碼:
@InitBinder("b")
public void b(WebDataBinder binder) {
binder.setFieldDefaultPrefix("b.");
}
@InitBinder("a")
public void a(WebDataBinder binder) {
binder.setFieldDefaultPrefix("a.");
}
@InitBinder("b") 注解表示該方法用來處理和Book和相關的參數,在方法中,給參數添加一個 b 前綴,即請求參數要有b前綴.
3.發送請求
請求發送時,通過給不同對象的參數添加不同的前綴,可以實現參數的區分.
補充
1、AOP中拋出的自定義異常需要繼承RuntimeException.否則無法被全局異常處理監測
2、攔截器中的異常也可以被全局異常處理,處理到
RestControllerAdvice
此注解類似於@RestController和Controller的關系,即自動轉換json