[toc] ## 1.自定義枚舉類 ```java public enum ReturnCode { RC200(200, "ok"), RC400(400, "請求失敗,參數錯誤,請檢查後重試。"), RC404(404, "未找到您請求的資源。"), RC405(405, "請求方式錯誤,請檢查 ...
目錄
1.自定義枚舉類
public enum ReturnCode {
RC200(200, "ok"),
RC400(400, "請求失敗,參數錯誤,請檢查後重試。"),
RC404(404, "未找到您請求的資源。"),
RC405(405, "請求方式錯誤,請檢查後重試。"),
RC500(500, "操作失敗,伺服器繁忙或伺服器錯誤,請稍後再試。");
// 自定義狀態碼
private final int code;
// 自定義描述
private final String msg;
ReturnCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
該枚舉類為我們和前端約定好的返回狀態碼和描述信息,可根據自己的需求修改狀態碼和描述
2.自定義統一返回格式類
@Data
public class R<T> {
private Integer code; //狀態碼
private String msg; //提示信息
private T data; //數據
private long timestamp;//介面請求時間
public R() {
this.timestamp = System.currentTimeMillis();
}
public static <T> R<T> success(T data) {
R<T> r = new R<>();
r.setCode(ReturnCode.RC200.getCode());
r.setMsg(ReturnCode.RC200.getMsg());
r.setData(data);
return r;
}
public static <T> R<T> error(int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setMsg(msg);
r.setData(null);
return r;
}
}
@Data
註解為Lombok工具類庫中的註解,提供類的get、set、equals、hashCode、canEqual、toString方法,使用時需配置Lombok,如不配置請手動生成相關方法。
我們返回的信息至少包括code、msg、data三部分,其中code是我們後端和前端約定好的狀態碼,msg為提示信息,data為返回的具體數據,沒有返回數據則為null。除了這三部分外,你還可以定義一些其他欄位,比如請求時間timestamp。
定義了統一返回類後,controller層返回數據時統一使用R.success()
方法封裝。
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
public R<List<Student>> getStudent() {
ArrayList<Student> list = new ArrayList<>();
Student student1 = new Student();
student1.setId(1);
student1.setName("name1");
Student student2 = new Student();
student2.setId(2);
student2.setName("name2");
list.add(student1);
list.add(student2);
return R.success(list);
}
}
@Data
class Student {
private Integer id;
private String name;
}
例如在以上代碼中,我們的需求是查詢學生信息,我們調用這個test1介面就返回了以下的結果:
{
"code": 200,
"msg": "ok",
"data": [
{
"id": 1,
"name": "name1"
},
{
"id": 2,
"name": "name2"
}
],
"timestamp": 1692805971309
}
到這裡我們已經基本實現了統一返回格式,但是上面這種實現方式也有一個缺點,就是每次返回數據的時候都需要調用R.success()
方法,非常麻煩,我們希望能夠在controller層里直接返回我們實際的數據,即data欄位中的內容,然後自動幫我們封裝到R.success()
之中,因此我們需要一種更高級的方法。
3.統一返回格式的高級實現
我們需要利用springboot的ResponseBodyAdvice
類來實現這個功能,ResponseBodyAdvice的作用:攔截Controller方法的返回值,統一處理返回值/響應體
/**
* 攔截controller返回值,封裝後統一返回格式
*/
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//如果Controller返回String的話,SpringBoot不會幫我們自動封裝而直接返回,因此我們需要手動轉換成json。
if (o instanceof String) {
return objectMapper.writeValueAsString(R.success(o));
}
//如果返回的結果是R對象,即已經封裝好的,直接返回即可。
//如果不進行這個判斷,後面進行全局異常處理時會出現錯誤
if (o instanceof R) {
return o;
}
return R.success(o);
}
}
@RestControllerAdvice
是@RestController
註解的增強,可以實現三個方面的功能:
- 全局異常處理
- 全局數據綁定
- 全局數據預處理
經過上面的處理後,我們就不需要在controller層使用R.success()
進行封裝了,直接返回原始數據,springboot就會幫我們自動封裝。
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
public List<Student> getStudent() {
ArrayList<Student> list = new ArrayList<>();
Student student1 = new Student();
student1.setId(1);
student1.setName("name1");
Student student2 = new Student();
student2.setId(2);
student2.setName("name2");
list.add(student1);
list.add(student2);
return list;
}
}
@Data
class Student {
private Integer id;
private String name;
}
此時我們調用介面返回的數據依然是自定義統一返回格式的json數據
{
"code": 200,
"msg": "ok",
"data": [
{
"id": 1,
"name": "name1"
},
{
"id": 2,
"name": "name2"
}
],
"timestamp": 1692805971325
}
需要註意的是,即使我們controller層的介面返回類型是void,ResponseBodyAdvice
類依然會幫我們自動封裝,其中data欄位為null。返回的格式如下:
{
"code": 200,
"msg": "ok",
"data": null,
"timestamp": 1692805971336
}
4.全局異常處理
如果我們不做統一異常處理,當後端出現異常時,返回的數據就變成了下麵這樣:
後端介面:
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
public String getStudent() {
int i = 1/0;
return "hello";
}
}
返回json:
{
"code": 200,
"msg": "ok",
"data": {
"timestamp": "2023-08-23T16:13:57.818+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/test/test1"
},
"timestamp": 1692807237832
}
code返回了200,又在data中顯示500錯誤,這顯然不是我們想要的結果,我們想要的結果應該時code返回500,data返回null。解決的方式有很多,你可以通過try catch的方式來捕獲,但是我們並不知道什麼時候會出現異常,而且手動寫try catch並不方便。因此我們需要進行全局異常處理 。
@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {
/**
* 處理異常
*
* @param e otherException
* @return
*/
@ExceptionHandler(Exception.class)
public R<String> exception(Exception e) {
log.error("異常 exception = {}", e.getMessage(), e);
return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
}
}
說明:
@RestControllerAdvice
,RestController的增強類,可用於實現全局異常處理器@ExceptionHandler
,統一處理某一類異常,比如要獲取空指針異常可以@ExceptionHandler(NullPointerException.class)
除此之外,你還可以使用@ResponseStatus
來指定客戶端收到的http狀態碼,如@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
則客戶端收到的http狀態碼為500。如果不指定,則預設返回200。在這裡我們並沒有指定,因此我們的請求返回的http狀態碼全部是200,當出現異常時,我們可以修改統一返回格式中code的狀態碼,來表明具體情況。
具體效果如下:
介面:
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
public void test() {
int i = 1/0; //發生除0異常
}
}
返回json:
{
"code": 500,
"msg": "操作失敗,伺服器繁忙或伺服器錯誤,請稍後再試。",
"data": null,
"timestamp": 1692808061062
}
基本上實現了我們的需求。
5.更優雅的全局異常處理
在上面的全局異常處理中,我們直接捕獲了Exception.class
,無論什麼異常都統一處理,但實際上我們需要根據不同的異常進行不同的處理,如空指針異常可能是前端傳參錯誤,以及我們的自定義異常等。
自定義異常如下:
@Getter
@Setter
public class BusinessException extends RuntimeException {
private int code;
private String msg;
public BusinessException() {
}
public BusinessException(ReturnCode returnCode) {
this(returnCode.getCode(),returnCode.getMsg());
}
public BusinessException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
註:@Getter
和@Setter
分別提供了get和set方法,同樣需要Lombok依賴。
我們在全局異常處理中可以使用@ExceptionHandler
指定異常類型,分別處理不同的異常
@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {
/**
* 處理自定義異常
*
* @param e BusinessException
* @return
*/
@ExceptionHandler(BusinessException.class)
public R<String> businessException(BusinessException e) {
log.error("業務異常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
return R.error(e.getCode(), e.getMsg());
}
/**
* 處理空指針的異常
*
* @param e NullPointerException
* @return
* @description 空指針異常定義為前端傳參錯誤,返回400
*/
@ExceptionHandler(NullPointerException.class)
public R<String> nullPointerException(NullPointerException e) {
log.error("空指針異常 NullPointerException ", e);
return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
}
/**
* 處理其他異常
*
* @param e otherException
* @return
*/
@ExceptionHandler(Exception.class)
public R<String> exception(Exception e) {
log.error("未知異常 exception = {}", e.getMessage(), e);
return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
}
}
需要註意的是一個異常只會被捕獲一次,比如空指針異常,只會被第二個方法捕獲,處理之後不會再被最後一個方法捕獲。當上面兩個方法都沒有捕獲到指定異常時,最後一個方法指定了@ExceptionHandler(Exception.class)
就可以捕獲到所有的異常,相當於if elseif else語句
分別測試自定義異常、空指針異常以及其他異常:
-
自定義異常
介面:
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
public void test() {
throw new BusinessException(ReturnCode.RC500.getCode(),"發生異常");
}
}
返回json:
{
"code": 500,
"msg": "發生異常",
"data": null,
"timestamp": 1692809118244
}
-
空指針異常:
介面:
@RestController @RequestMapping("/test") public class TestController { @PostMapping("/test1") public void test(int id, String name) { System.out.println(id + name); boolean equals = name.equals("11"); } }
請求:
返回json:
{ "code": 400, "msg": "請求失敗,參數錯誤,請檢查後重試。", "data": null, "timestamp": 1692809456917 }
-
其他異常:
介面:
@RestController @RequestMapping("/test") public class TestController { @PostMapping("/test1") public void test() { throw new RuntimeException("發生異常"); } }
返回json:
{ "code": 500, "msg": "操作失敗,伺服器繁忙或伺服器錯誤,請稍後再試。", "data": null, "timestamp": 1692809730234 }
6.處理404錯誤
即使我們配置了全局異常處理,當出現404 not found等4xx錯誤時,依然會出現意外情況:
返回json:
{
"code": 200,
"msg": "ok",
"data": {
"timestamp": "2023-08-23T17:01:15.102+00:00",
"status": 404,
"error": "Not Found",
"path": "/test/nullapi"
},
"timestamp": 1692810075116
}
我們可以看到發生404錯誤時控制台並沒有報異常,原因是404錯誤並不屬於異常,全局異常處理自然不會去捕獲並處理。因此我們的解決方法是當出現4xx錯誤時,讓springboot直接報異常,這樣我們的全局異常處理就可以捕獲到。
在application.yml
配置文件增加以下配置項:
# 當HTTP狀態碼為4xx時直接拋出異常
spring:
mvc:
throw-exception-if-no-handler-found: true
# 關閉預設的靜態資源路徑映射
web:
resources:
add-mappings: false
現在當我們再次請求一個不存在的介面是,控制台會報NoHandlerFoundException
異常,然後被全局異常處理捕獲到並統一返回
返回json:
{
"code": 500,
"msg": "操作失敗,伺服器繁忙或伺服器錯誤,請稍後再試。",
"data": null,
"timestamp": 1692810621545
}
當發生404錯誤時,http的狀態碼依然是200,同時code返回的是500,這不利於用戶或者前端人員的理解,因此我們可以在全局異常處理中單獨對NoHandlerFoundException
異常進行處理。
/**
* 處理404異常
*
* @param e NoHandlerFoundException
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)//指定http狀態碼為404
public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
log.error("404異常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
}
在上面中,我們使用@ExceptionHandler(NoHandlerFoundException.class)
單獨捕獲處理404異常,同時使用@ResponseStatus(HttpStatus.NOT_FOUND)
指定http返回碼為404,我們統一返回格式中code也設置為404
現在當我們再次發生404異常時,返回json如下:
{
"code": 404,
"msg": "未找到您請求的資源。",
"data": null,
"timestamp": 1692811047868
}
控制台日誌:
同理我們還可以為405錯誤進行配置,405錯誤對應的異常為HttpRequestMethodNotSupportedException
/**
* 處理請求方式錯誤(405)異常
*
* @param e HttpRequestMethodNotSupportedException
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)//指定http狀態碼為405
public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
log.error("請求方式錯誤(405)異常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
}
返回json:
{
"code": 405,
"msg": "請求方式錯誤,請檢查後重試。",
"data": null,
"timestamp": 1692811288226
}
控制台日誌:
全局異常處理RestExceptionHandler
類完整代碼如下:
package com.tuuli.config;
import com.tuuli.common.BusinessException;
import com.tuuli.common.R;
import com.tuuli.common.ReturnCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
/**
* 全局異常處理
*/
@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {
/**
* 處理自定義異常
*
* @param e BusinessException
* @return
*/
@ExceptionHandler(BusinessException.class)
public R<String> businessException(BusinessException e) {
log.error("業務異常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
return R.error(e.getCode(), e.getMsg());
}
/**
* 處理空指針的異常
*
* @param e NullPointerException
* @return
* @description 空指針異常定義為前端傳參錯誤,返回400
*/
@ExceptionHandler(value = NullPointerException.class)
public R<String> nullPointerException(NullPointerException e) {
log.error("空指針異常 NullPointerException ", e);
return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
}
/**
* 處理404異常
*
* @param e NoHandlerFoundException
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
log.error("404異常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
}
/**
* 處理請求方式錯誤(405)異常
*
* @param e HttpRequestMethodNotSupportedException
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
log.error("請求方式錯誤(405)異常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
}
/**
* 處理其他異常
*
* @param e otherException
* @return
*/
@ExceptionHandler(Exception.class)
public R<String> exception(Exception e) {
log.error("未知異常 exception = {}", e.getMessage(), e);
return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
}
}