Springboot項目異常處理及返回結果統(tǒng)一
背景
在創(chuàng)建項目的初期,我們需要規(guī)范后端返回的數據結構,以便更好地與前端開發(fā)人員合作。
比如后端返回的數據為:
{ ?"msg": "請?zhí)D登陸頁面", }
此時前端無法確定后端服務的處理結果是成功的還是失敗的。在前端展示頁面,成功與失敗的展示是要作區(qū)分的,甚至不同的成功或失敗結果要做出不同的展現效果,這也就是我們?yōu)槭裁匆獙Ψ祷亟Y果做出統(tǒng)一規(guī)范的原因。
返回結果定義
public class ResultWrap<T, M> { // 方便前端判斷當前請求處理結果是否正常 private int code; // 業(yè)務處理結果 private T data; // 產生錯誤的情況下,提示用戶信息 private String message; // 產生錯誤情況下的異常堆棧,提示開發(fā)人員 private String error; // 發(fā)生錯誤的時候,返回的附加信息 private M metaInfo; }
- 1.為了把模糊的消息定性,我們給所有的返回結果都帶上一個
code
字段,前端可以根據這個字段來判斷我們的處理結果到底是成功的還是失敗的;比如code=200
的時候,我們的處理結果一定是成功的,其他的code值全都是失敗的,并且我們會有多種code值,以便前端頁面可以根據不同的code值做出不同的交互動作。 - 2.一般在處理成功的情況下,后端會返回一些業(yè)務數據,比如返回訂單列表等,那么這些數據我們就放在字段
data
里面,只有業(yè)務邏輯處理成功的情況下,data
字段里面才可能有數據。 - 3.如若我們的處理失敗了,那么我們應該會有對應的消息提醒用戶為什么處理失敗了。比如文件上傳失敗了,用戶名或密碼錯誤等等,都是需要告知用戶的。
- 4.除了需要告知用戶失敗原因,我們也需要保留一個字段給開發(fā)人員,當錯誤是
服務器內部錯誤
的時候,我們需要讓開發(fā)人員能第一時間定位是啥原因引起的,我們會把錯誤的堆棧信息給到error
字段。 - 5.當發(fā)生異常的時候,我們還會需要返回一些額外的補充數據給前端,比如用戶登陸失敗一次和失敗多次需要不同的交互效果,此時我們會在metaInfo里面返回登陸失敗次數;或者在用戶操作沒有權限的時候,根據用戶是否已登陸來確定此時是跳轉登陸頁面還是直接彈窗提示當前操作沒有權限。
定義好返回結果后,我們和前端的交互數據結果就統(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.我們同樣需要一個與返回結果一致的
code
字段,這樣的話,我們在異常處理的時候,才能把當前異常轉換成最終的ResultWrap結果。 - 2.msg就是在產生異常的時候,需要給到用戶的提示消息。
這樣的話,我們的后端開發(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 { /** * 捕獲沒有用戶權限的異常 * * @return */ @ExceptionHandler(AuthorizationException.class) public ResultWrap handleException(AuthorizationException e) { return ResultWrap.failure(401, "您暫時沒有訪問權限!", 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權限管理框架,因為它拋出的異常沒有被我們的
AwesomeException
包裝,所以這個AuthorizationException
異常需要我們單獨處理。
AwesomeException
是我們大多數業(yè)務邏輯拋出來的異常,我們根據AwesomeException
里面的code、msg和它包裝的cause,封裝成一個最終的響應數據ResultWrap。另一個
RuntimeException
是必須要額外處理的,任何開發(fā)人員都無法保證自己的代碼是完全沒有bug的,任何的空指針異常都會影響用戶體驗,這種編碼性的錯誤我們需要通過統(tǒng)一的錯誤處理讓它變得更柔和一點。
返回結果的處理
我們需要針對所有的返回結果進行檢查,如果不是ResultWrap
類型的返回數據,我們需要包裝一下,以便保證我們和前端開發(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; } ? ?// 有一些情況是不需要包裝的,比如調用第三方API返回的數據,所以我們做了一個自定義注解來避免所以的結果都被包裝成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) { ? ?// 如果返回結果為null,那么我們直接返回ResultWrap.success() if (Objects.isNull(body)) { return ResultWrap.success(); } ? ?// // 如果返回結果已經是ResultWrap,直接返回 if (body instanceof ResultWrap) { return body; } ? ?// 否則我們把返回結果包裝成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;決定了是否要包裝返回數據 boolean value() default true; }
至此,我們把整個異常處理與返回結果的統(tǒng)一處理全部關聯起來了,我們后端的開發(fā)人員無論是返回異常還是返回ResultWrap或者其他數據結果,都能很好地保證與前端開發(fā)人員的正常協作,不必為數據結構的變化過多地溝通。
完整代碼
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> { // 方便前端判斷當前請求處理結果是否正常 private int code; // 業(yè)務處理結果 private T data; // 產生錯誤的情況下,提示用戶信息 private String message; // 產生錯誤情況下的異常堆棧,提示開發(fā)人員 private String error; // 發(fā)生錯誤的時候,返回的附加信息 private M metaInfo; ? /** * 成功帶處理結果 * * @param data * @param <T> * @return */ public static <T> ResultWrap success(T data) { return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null); } ? /** * 成功不帶處理結果 * * @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, "服務異常,請稍后訪問!", e.getCause()); } ? private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8"; ? /** * 把結果寫入響應中 * * @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 { /** * 捕獲沒有用戶權限的異常 * * @return */ @ExceptionHandler(AuthorizationException.class) public ResultWrap handleException(AuthorizationException e) { return ResultWrap.failure(401, "您暫時沒有訪問權限!", 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注解,該返回結果就不會被包裝 @IgnoreResponseAdvice @GetMapping("/read") Boolean read() { return true; }
到此這篇關于Springboot項目異常處理及返回結果統(tǒng)一的文章就介紹到這了,更多相關Springboot異常處理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot使用hibernate validation對參數校驗的實現方法
這篇文章主要介紹了spring-boot 使用hibernate validation對參數進行優(yōu)雅的校驗,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12解決springboot利用ConfigurationProperties注解配置數據源無法讀取配置信息問題
今天在學習springboot利用ConfigurationProperties注解配置數據源的使用遇到一個問題無法讀取配置信息,發(fā)現全部為null,糾結是哪里出了問題呢,今天一番思考,問題根源找到,下面把我的解決方案分享到腳本之家平臺,感興趣的朋友一起看看吧2021-05-05