四、全局異常處理
參數(shù)校驗失敗會自動引發(fā)異常,我們當然不可能再去手動捕捉異常進行處理。但我們又不想手動捕捉這個異常,又要對這個異常進行處理,那正好使用[SpringBoot]全局異常處理來達到一勞永逸的效果!
1、基本使用
首先,我們需要新建一個類,在這個類上加上@ControllerAdvice
或@RestControllerAdvice
注解,這個類就配置成全局處理類了。
這個根據(jù)你的Controller層用的是
@Controller
還是@RestController
來決定。
然后在類中新建方法,在方法上加上@ExceptionHandler
注解并指定你想處理的異常類型,接著在方法內(nèi)編寫對該異常的操作邏輯,就完成了對該異常的全局處理!我們現(xiàn)在就來演示一下對參數(shù)校驗失敗拋出的MethodArgumentNotValidException
全局處理:
package com.csdn.demo1.global;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@ResponseBody
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 從異常對象中拿到ObjectError對象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取錯誤提示信息進行返回
return objectError.getDefaultMessage();
}
/**
* 系統(tǒng)異常 預(yù)期以外異常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO? handleUnexpectedServer(Exception ex) {
log.error("系統(tǒng)異常:", ex);
// GlobalMsgEnum.ERROR是我自己定義的枚舉類
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
/**
* 所以異常的攔截
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO? exception(Throwable ex) {
log.error("系統(tǒng)異常:", ex);
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
}
我們再次進行測試,這次返回的就是我們制定的錯誤提示信息!我們通過全局異常處理優(yōu)雅的實現(xiàn)了我們想要的功能!
以后我們再想寫接口參數(shù)校驗,就只需要在入?yún)⒌某蓡T變量上加上Validator校驗規(guī)則注解,然后在參數(shù)上加上@Valid
注解即可完成校驗,校驗失敗會自動返回錯誤提示信息,無需任何其他代碼!
2、自定義異常
在很多情況下,我們需要手動拋出異常,比如在業(yè)務(wù)層當有些條件并不符合業(yè)務(wù)邏輯,而使用自定義異常有諸多優(yōu)點:
- 自定義異常可以攜帶更多的信息,不像這樣只能攜帶一個字符串。
- 項目開發(fā)中經(jīng)常是很多人負責(zé)不同的模塊,使用自定義異常可以統(tǒng)一了對外異常展示的方式。
- 自定義異常語義更加清晰明了,一看就知道是項目中手動拋出的異常。
我們現(xiàn)在就來開始寫一個自定義異常:
package com.csdn.demo1.global;
import lombok.Getter;
@Getter //只要getter方法,無需setter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "接口錯誤");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
然后在剛才的全局異常類中加入如下:
//自定義的全局異常
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}
這樣就對異常的處理就比較規(guī)范了,當然還可以添加對Exception的處理,這樣無論發(fā)生什么異常我們都能屏蔽掉然后響應(yīng)數(shù)據(jù)給前端,不過建議最后項目上線時這樣做,能夠屏蔽掉錯誤信息暴露給前端,在開發(fā)中為了方便調(diào)試還是不要這樣做。
另外,當我們拋出自定義異常的時候全局異常處理只響應(yīng)了異常中的錯誤信息msg給前端,并沒有將錯誤代碼code返回。這還需要配合數(shù)據(jù)統(tǒng)一響應(yīng)。
如果在多模塊使用,全局異常等公共功能抽象成子模塊,則在需要的子模塊中需要將該模塊包掃描加入,@SpringBootApplication(scanBasePackages = {"com.xxx"})
五、數(shù)據(jù)統(tǒng)一響應(yīng)
統(tǒng)一數(shù)據(jù)響應(yīng)是我們自己自定義一個響應(yīng)體類,無論后臺是運行正常還是發(fā)生異常,響應(yīng)給前端的數(shù)據(jù)格式是不變的!這里我包括了響應(yīng)信息代碼code和響應(yīng)信息說明msg,首先可以設(shè)置一個枚舉規(guī)范響應(yīng)體中的響應(yīng)碼和響應(yīng)信息。
@Getter
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "響應(yīng)失敗"),
VALIDATE_FAILED(1002, "參數(shù)校驗失敗"),
ERROR(5000, "未知錯誤");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
自定義響應(yīng)體
package com.csdn.demo1.global;
import lombok.Getter;
@Getter
public class ResultVO<T> {
/**
* 狀態(tài)碼,比如1000代表響應(yīng)成功
*/
private int code;
/**
* 響應(yīng)信息,用來說明響應(yīng)情況
*/
private String msg;
/**
* 響應(yīng)的具體數(shù)據(jù)
*/
private T data;
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}
}
最后需要修改全局異常處理類的返回類型
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,這里傳遞的響應(yīng)碼枚舉
return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 注意哦,這里傳遞的響應(yīng)碼枚舉
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
}
最后在controller層進行接口信息數(shù)據(jù)的返回
@GetMapping("/getUser")
public ResultVO<User> getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq.com");
return new ResultVO<>(user);
}
經(jīng)過測試,這樣響應(yīng)碼和響應(yīng)信息只能是枚舉規(guī)定的那幾個,就真正做到了響應(yīng)數(shù)據(jù)格式、響應(yīng)碼和響應(yīng)信息規(guī)范化、統(tǒng)一化!
還有一種全局返回類如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Msg {
//狀態(tài)碼
private int code;
//提示信息
private String msg;
//用戶返回給瀏覽器的數(shù)據(jù)
private Map<String,Object> data = new HashMap<>();
public static Msg success() {
Msg result = new Msg();
result.setCode(200);
result.setMsg("請求成功!");
return result;
}
public static Msg fail() {
Msg result = new Msg();
result.setCode(400);
result.setMsg("請求失敗!");
return result;
}
public static Msg fail(String msg) {
Msg result = new Msg();
result.setCode(400);
result.setMsg(msg);
return result;
}
public Msg(ReturnResult returnResult){
code = returnResult.getCode();
msg = returnResult.getMsg();
}
public Msg add(String key,Object value) {
this.getData().put(key, value);
return this;
}
}
六、全局處理響應(yīng)數(shù)據(jù)(可選擇)
接口返回統(tǒng)一響應(yīng)體 + 異常也返回統(tǒng)一響應(yīng)體,其實這樣已經(jīng)很好了,但還是有可以優(yōu)化的地方。要知道一個項目下來定義的接口搞個幾百個太正常不過了,要是每一個接口返回數(shù)據(jù)時都要用響應(yīng)體來包裝一下好像有點麻煩,有沒有辦法省去這個包裝過程呢?
當然是有的,還是要用到全局處理。但是為了擴展性,就是允許繞過數(shù)據(jù)統(tǒng)一響應(yīng)(這樣就可以提供多方使用),我們可以自定義注解,利用注解來選擇是否進行全局響應(yīng)包裝
首先創(chuàng)建自定義注解,作用相當于全局處理類開關(guān):
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明該注解只能放在方法上
public @interface NotResponseBody {
}
其次創(chuàng)建一個類并加上注解使其成為全局處理類。然后繼承ResponseBodyAdvice
接口重寫其中的方法,即可對我們的controller進行增強操作,具體看代碼和注釋:
package com.csdn.demo1.global;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 注意哦,這里要加上需要掃描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class? class="hljs-keyword"extends HttpMessageConverter?> aClass) {
// 如果接口返回的類型本身就是ResultVO那就沒有必要進行額外的操作,返回false
// 如果方法上加了我們的自定義注解也沒有必要進行額外的操作
return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class? class="hljs-keyword"extends HttpMessageConverter?> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String類型不能直接包裝,所以要進行些特別的處理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 將數(shù)據(jù)包裝在ResultVO里后,再轉(zhuǎn)換為json字符串響應(yīng)給前端
return objectMapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String類型錯誤");
}
}
// 將原本的數(shù)據(jù)包裝在ResultVO里
return new ResultVO<>(data);
}
}
重寫的這兩個方法是用來在controller將數(shù)據(jù)進行返回前進行增強操作,supports方法要返回為true才會執(zhí)行beforeBodyWrite
方法,所以如果有些情況不需要進行增強操作可以在supports方法里進行判斷。
對返回數(shù)據(jù)進行真正的操作還是在beforeBodyWrite
方法中,我們可以直接在該方法里包裝數(shù)據(jù),這樣就不需要每個接口都進行數(shù)據(jù)包裝了,省去了很多麻煩。此時controller只需這樣寫就行了:
@GetMapping("/getUser")
//@NotResponseBody //是否繞過數(shù)據(jù)統(tǒng)一響應(yīng)開關(guān)
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq.com");
// 注意哦,這里是直接返回的User類型,并沒有用ResultVO進行包裝
return user;
}
-
接口
+關(guān)注
關(guān)注
33文章
8598瀏覽量
151157 -
URL
+關(guān)注
關(guān)注
0文章
139瀏覽量
15340 -
后端
+關(guān)注
關(guān)注
0文章
31瀏覽量
2237 -
SpringBoot
+關(guān)注
關(guān)注
0文章
173瀏覽量
179
發(fā)布評論請先 登錄
相關(guān)推薦
評論