Springboot項目異常處理及返回結(jié)果統(tǒng)一
背景
在創(chuàng)建項目的初期,我們需要規(guī)范后端返回的數(shù)據(jù)結(jié)構(gòu),以便更好地與前端開發(fā)人員合作。
比如后端返回的數(shù)據(jù)為:
{
?"msg": "請?zhí)D(zhuǎn)登陸頁面",
}此時前端無法確定后端服務(wù)的處理結(jié)果是成功的還是失敗的。在前端展示頁面,成功與失敗的展示是要作區(qū)分的,甚至不同的成功或失敗結(jié)果要做出不同的展現(xiàn)效果,這也就是我們?yōu)槭裁匆獙Ψ祷亟Y(jié)果做出統(tǒng)一規(guī)范的原因。
返回結(jié)果定義
public class ResultWrap<T, M> {
// 方便前端判斷當(dāng)前請求處理結(jié)果是否正常
private int code;
// 業(yè)務(wù)處理結(jié)果
private T data;
// 產(chǎn)生錯誤的情況下,提示用戶信息
private String message;
// 產(chǎn)生錯誤情況下的異常堆棧,提示開發(fā)人員
private String error;
// 發(fā)生錯誤的時候,返回的附加信息
private M metaInfo;
}- 1.為了把模糊的消息定性,我們給所有的返回結(jié)果都帶上一個
code字段,前端可以根據(jù)這個字段來判斷我們的處理結(jié)果到底是成功的還是失敗的;比如code=200的時候,我們的處理結(jié)果一定是成功的,其他的code值全都是失敗的,并且我們會有多種code值,以便前端頁面可以根據(jù)不同的code值做出不同的交互動作。 - 2.一般在處理成功的情況下,后端會返回一些業(yè)務(wù)數(shù)據(jù),比如返回訂單列表等,那么這些數(shù)據(jù)我們就放在字段
data里面,只有業(yè)務(wù)邏輯處理成功的情況下,data字段里面才可能有數(shù)據(jù)。 - 3.如若我們的處理失敗了,那么我們應(yīng)該會有對應(yīng)的消息提醒用戶為什么處理失敗了。比如文件上傳失敗了,用戶名或密碼錯誤等等,都是需要告知用戶的。
- 4.除了需要告知用戶失敗原因,我們也需要保留一個字段給開發(fā)人員,當(dāng)錯誤是
服務(wù)器內(nèi)部錯誤的時候,我們需要讓開發(fā)人員能第一時間定位是啥原因引起的,我們會把錯誤的堆棧信息給到error字段。 - 5.當(dāng)發(fā)生異常的時候,我們還會需要返回一些額外的補充數(shù)據(jù)給前端,比如用戶登陸失敗一次和失敗多次需要不同的交互效果,此時我們會在metaInfo里面返回登陸失敗次數(shù);或者在用戶操作沒有權(quán)限的時候,根據(jù)用戶是否已登陸來確定此時是跳轉(zhuǎn)登陸頁面還是直接彈窗提示當(dāng)前操作沒有權(quán)限。
定義好返回結(jié)果后,我們和前端的交互數(shù)據(jù)結(jié)果就統(tǒng)一好了。
異常的定義
之所以定義一個統(tǒng)一的異常類,是為了把所有的異常全部匯總成一個異常,最終我們只需要在異常處理的時候單獨處理這一個異常即可。
@Data
public class AwesomeException extends Throwable {
?// 錯誤碼
private int code;
?// 提示消息
private String msg;
?
public AwesomeException(int code, String msg, Exception e) {
super(e);
this.code = code;
this.msg = msg;
}
?
public AwesomeException(int code, String msg) {
this.code = code;
this.msg = msg;
}
}- 1.我們同樣需要一個與返回結(jié)果一致的
code字段,這樣的話,我們在異常處理的時候,才能把當(dāng)前異常轉(zhuǎn)換成最終的ResultWrap結(jié)果。 - 2.msg就是在產(chǎn)生異常的時候,需要給到用戶的提示消息。
這樣的話,我們的后端開發(fā)人員遇到異常的情況,只需要通過創(chuàng)建AwesomeException異常對象拋出即可,不需要再為創(chuàng)建什么異常而煩惱了。
異常的處理
我們下面需要針對所有拋出的異常進行統(tǒng)一的處理:
import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.vo.ResultWrap;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
?
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @className ExceptionHandler
* @description:
*/
@Slf4j
@RestControllerAdvice
public class AwesomeExceptionHandler {
/**
* 捕獲沒有用戶權(quán)限的異常
*
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public ResultWrap handleException(AuthorizationException e) {
return ResultWrap.failure(401, "您暫時沒有訪問權(quán)限!", e);
}
?
/**
* 處理AwesomeException
*
* @param e
* @param request
* @param response
* @return
*/
@ExceptionHandler(AwesomeException.class)
public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
return ResultWrap.failure(e);
}
?
/**
* 專門針對運行時異常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public ResultWrap handleRuntimeException(RuntimeException e) {
return ResultWrap.failure(e);
}
}在項目中,我們集成了shiro權(quán)限管理框架,因為它拋出的異常沒有被我們的
AwesomeException包裝,所以這個AuthorizationException異常需要我們單獨處理。
AwesomeException是我們大多數(shù)業(yè)務(wù)邏輯拋出來的異常,我們根據(jù)AwesomeException里面的code、msg和它包裝的cause,封裝成一個最終的響應(yīng)數(shù)據(jù)ResultWrap。另一個
RuntimeException是必須要額外處理的,任何開發(fā)人員都無法保證自己的代碼是完全沒有bug的,任何的空指針異常都會影響用戶體驗,這種編碼性的錯誤我們需要通過統(tǒng)一的錯誤處理讓它變得更柔和一點。
返回結(jié)果的處理
我們需要針對所有的返回結(jié)果進行檢查,如果不是ResultWrap類型的返回數(shù)據(jù),我們需要包裝一下,以便保證我們和前端開發(fā)人員達(dá)成的共識。
import com.example.awesomespring.vo.ResultWrap;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Objects;
?
/**
* @className AwesomeResponseAdvice
* @description:
*/
@RestControllerAdvice
public class AwesomeResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
? ?// 如果返回String,那么就不包裝了。
if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
return false;
}
? ?// 有一些情況是不需要包裝的,比如調(diào)用第三方API返回的數(shù)據(jù),所以我們做了一個自定義注解來避免所以的結(jié)果都被包裝成ResultWrap。
boolean ignore = false;
IgnoreResponseAdvice ignoreResponseAdvice =
returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
? ?// 如果我們在方法上添加了IgnoreResponseAdvice注解,那么就不要攔截包裝了
if (Objects.nonNull(ignoreResponseAdvice)) {
ignore = ignoreResponseAdvice.value();
return !ignore;
}
? ?// 如果我們在類上面添加了IgnoreResponseAdvice注解,也在方法上面添加了IgnoreResponseAdvice注解,那么以方法上的注解為準(zhǔn)。
Class<?> clazz = returnType.getDeclaringClass();
ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
RestController restController = clazz.getDeclaredAnnotation(RestController.class);
if (Objects.nonNull(ignoreResponseAdvice)) {
ignore = ignoreResponseAdvice.value();
} else if (Objects.isNull(restController)) {
ignore = true;
}
return !ignore;
}
?
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
? ?// 如果返回結(jié)果為null,那么我們直接返回ResultWrap.success()
if (Objects.isNull(body)) {
return ResultWrap.success();
}
? ?// // 如果返回結(jié)果已經(jīng)是ResultWrap,直接返回
if (body instanceof ResultWrap) {
return body;
}
? ?// 否則我們把返回結(jié)果包裝成ResultWrap
return ResultWrap.success(body);
}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
?
/**
* @className IgnoreResponseAdvice
* @description:
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
?// 是否忽略ResponseAdvice;決定了是否要包裝返回數(shù)據(jù)
boolean value() default true;
}至此,我們把整個異常處理與返回結(jié)果的統(tǒng)一處理全部關(guān)聯(lián)起來了,我們后端的開發(fā)人員無論是返回異常還是返回ResultWrap或者其他數(shù)據(jù)結(jié)果,都能很好地保證與前端開發(fā)人員的正常協(xié)作,不必為數(shù)據(jù)結(jié)構(gòu)的變化過多地溝通。
完整代碼
ResultWrap.java
import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
?
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
?
/**
* @className ResultWrap
* @description:
*/
@Data
@AllArgsConstructor
public class ResultWrap<T, M> {
// 方便前端判斷當(dāng)前請求處理結(jié)果是否正常
private int code;
// 業(yè)務(wù)處理結(jié)果
private T data;
// 產(chǎn)生錯誤的情況下,提示用戶信息
private String message;
// 產(chǎn)生錯誤情況下的異常堆棧,提示開發(fā)人員
private String error;
// 發(fā)生錯誤的時候,返回的附加信息
private M metaInfo;
?
/**
* 成功帶處理結(jié)果
*
* @param data
* @param <T>
* @return
*/
public static <T> ResultWrap success(T data) {
return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null);
}
?
/**
* 成功不帶處理結(jié)果
*
* @return
*/
public static ResultWrap success() {
return success(HttpStatus.OK.name());
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @return
*/
public static <M> ResultWrap failure(int code, String message, String error, M metaInfo) {
return new ResultWrap(code, null, message, error, metaInfo);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @param metaInfo
* @param <M>
* @return
*/
public static <M> ResultWrap failure(int code, String message, Throwable error, M metaInfo) {
String errorMessage = StringUtils.EMPTY;
if (Objects.nonNull(error)) {
errorMessage = toStackTrace(error);
}
return failure(code, message, errorMessage, metaInfo);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @return
*/
public static ResultWrap failure(int code, String message, Throwable error) {
return failure(code, message, error, null);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param metaInfo
* @param <M>
* @return
*/
public static <M> ResultWrap failure(int code, String message, M metaInfo) {
return failure(code, message, StringUtils.EMPTY, metaInfo);
}
?
/**
* 失敗
*
* @param e
* @return
*/
public static ResultWrap failure(AwesomeException e) {
return failure(e.getCode(), e.getMsg(), e.getCause());
}
?
?
/**
* 失敗
*
* @param e
* @return
*/
public static ResultWrap failure(RuntimeException e) {
return failure(500, "服務(wù)異常,請稍后訪問!", e.getCause());
}
?
private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8";
?
/**
* 把結(jié)果寫入響應(yīng)中
*
* @param response
*/
public void writeToResponse(HttpServletResponse response) {
int code = this.getCode();
if (Objects.isNull(HttpStatus.resolve(code))) {
response.setStatus(HttpStatus.OK.value());
} else {
response.setStatus(code);
}
response.setContentType(APPLICATION_JSON_VALUE);
try (PrintWriter writer = response.getWriter()) {
writer.write(JsonUtil.obj2String(this));
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
?
/**
* 獲取異常堆棧信息
*
* @param e
* @return
*/
private static String toStackTrace(Throwable e) {
if (Objects.isNull(e)) {
return StringUtils.EMPTY;
}
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
try {
e.printStackTrace(pw);
return sw.toString();
} catch (Exception e1) {
return StringUtils.EMPTY;
}
}
}AwesomeException.java
?import lombok.Data;
?
/**
* @className AwesomeException
* @description:
*/
@Data
public class AwesomeException extends Throwable {
?
private int code;
private String msg;
public AwesomeException(int code, String msg, Exception e) {
super(e);
this.code = code;
this.msg = msg;
}
public AwesomeException(int code, String msg) {
this.code = code;
this.msg = msg;
}
}AwesomeExceptionHandler.java
import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.vo.ResultWrap;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
?
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
?
/**
* @className ExceptionHandler
* @description:
*/
@Slf4j
@RestControllerAdvice
public class AwesomeExceptionHandler {
/**
* 捕獲沒有用戶權(quán)限的異常
*
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public ResultWrap handleException(AuthorizationException e) {
return ResultWrap.failure(401, "您暫時沒有訪問權(quán)限!", e);
}
?
/**
* 處理AwesomeException
*
* @param e
* @param request
* @param response
* @return
*/
@ExceptionHandler(AwesomeException.class)
public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
return ResultWrap.failure(e);
}
?
/**
* 專門針對運行時異常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public ResultWrap handleRuntimeException(RuntimeException e) {
return ResultWrap.failure(e);
}
}IgnoreResponseAdvice.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
?
/**
* @className IgnoreResponseAdvice
* @description:
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
?
boolean value() default true;
}AwesomeResponseAdvice.java
import com.example.awesomespring.vo.ResultWrap;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
?
import java.util.Objects;
?
/**
* @className AwesomeResponseAdvice
* @description:
*/
@RestControllerAdvice
public class AwesomeResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
return false;
}
boolean ignore = false;
IgnoreResponseAdvice ignoreResponseAdvice =
returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
if (Objects.nonNull(ignoreResponseAdvice)) {
ignore = ignoreResponseAdvice.value();
return !ignore;
}
Class<?> clazz = returnType.getDeclaringClass();
ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
RestController restController = clazz.getDeclaredAnnotation(RestController.class);
if (Objects.nonNull(ignoreResponseAdvice)) {
ignore = ignoreResponseAdvice.value();
} else if (Objects.isNull(restController)) {
ignore = true;
}
return !ignore;
}
?
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (Objects.isNull(body)) {
return ResultWrap.success();
}
if (body instanceof ResultWrap) {
return body;
}
return ResultWrap.success(body);
}
}使用示例
// 這里只要返回AwesomeException,就會被ExceptionHandler處理掉,包裝成ResultWrap
@PostMapping("/image/upload")
String upload(@RequestPart("userImage") MultipartFile userImage) throws AwesomeException {
fileService.putObject("video", userImage);
return "success";
}
?
// 加上IgnoreResponseAdvice注解,該返回結(jié)果就不會被包裝
@IgnoreResponseAdvice
@GetMapping("/read")
Boolean read() {
return true;
}到此這篇關(guān)于Springboot項目異常處理及返回結(jié)果統(tǒng)一的文章就介紹到這了,更多相關(guān)Springboot異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot使用hibernate validation對參數(shù)校驗的實現(xiàn)方法
這篇文章主要介紹了spring-boot 使用hibernate validation對參數(shù)進行優(yōu)雅的校驗,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
Java調(diào)用HTTPS接口實現(xiàn)繞過SSL認(rèn)證
SSL認(rèn)證是確保通信安全的重要手段,有的時候為了方便調(diào)用,我們會繞過SSL認(rèn)證,這篇文章主要介紹了Java如何調(diào)用HTTPS接口實現(xiàn)繞過SSL認(rèn)證,需要的可以參考下2023-11-11
深入學(xué)習(xí)java8?中的CompletableFuture
本文主要介紹了java8中的CompletableFuture,CompletableFuture實現(xiàn)了CompletionStage接口和Future接口,前者是對后者的一個擴展,增加了異步回調(diào)、流式處理、多個Future組合處理的能力,使Java在處理多任務(wù)的協(xié)同工作時更加順暢便利,下文需要的朋友可以參考一下2022-05-05
解決springboot利用ConfigurationProperties注解配置數(shù)據(jù)源無法讀取配置信息問題
今天在學(xué)習(xí)springboot利用ConfigurationProperties注解配置數(shù)據(jù)源的使用遇到一個問題無法讀取配置信息,發(fā)現(xiàn)全部為null,糾結(jié)是哪里出了問題呢,今天一番思考,問題根源找到,下面把我的解決方案分享到腳本之家平臺,感興趣的朋友一起看看吧2021-05-05

