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ā)人員達成的共識。
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注解,那么以方法上的注解為準。 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)雅的校驗,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12Java調(diào)用HTTPS接口實現(xiàn)繞過SSL認證
SSL認證是確保通信安全的重要手段,有的時候為了方便調(diào)用,我們會繞過SSL認證,這篇文章主要介紹了Java如何調(diào)用HTTPS接口實現(xiàn)繞過SSL認證,需要的可以參考下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