首頁 > 軟體

Springboot專案例外處理及返回結果統一

2022-08-08 14:02:05

背景

在建立專案的初期,我們需要規範後端返回的資料結構,以便更好地與前端開發人員合作。

比如後端返回的資料為:

{
  "msg": "請跳轉登陸頁面",
}

此時前端無法確定後端服務的處理結果是成功的還是失敗的。在前端展示頁面,成功與失敗的展示是要作區分的,甚至不同的成功或失敗結果要做出不同的展現效果,這也就是我們為什麼要對返回結果做出統一規範的原因。

返回結果定義

public class ResultWrap<T, M> {
  //  方便前端判斷當前請求處理結果是否正常
  private int code;
  //  業務處理結果
  private T data;
  //  產生錯誤的情況下,提示使用者資訊
  private String message;
  //  產生錯誤情況下的異常堆疊,提示開發人員
  private String error;
  //  發生錯誤的時候,返回的附加資訊
  private M metaInfo;
}
  • 1.為了把模糊的訊息定性,我們給所有的返回結果都帶上一個code欄位,前端可以根據這個欄位來判斷我們的處理結果到底是成功的還是失敗的;比如code=200的時候,我們的處理結果一定是成功的,其他的code值全都是失敗的,並且我們會有多種code值,以便前端頁面可以根據不同的code值做出不同的互動動作。
  • 2.一般在處理成功的情況下,後端會返回一些業務資料,比如返回訂單列表等,那麼這些資料我們就放在欄位data裡面,只有業務邏輯處理成功的情況下,data欄位裡面才可能有資料。
  • 3.如若我們的處理失敗了,那麼我們應該會有對應的訊息提醒使用者為什麼處理失敗了。比如檔案上傳失敗了,使用者名稱或密碼錯誤等等,都是需要告知使用者的。
  • 4.除了需要告知使用者失敗原因,我們也需要保留一個欄位給開發人員,當錯誤是伺服器內部錯誤的時候,我們需要讓開發人員能第一時間定位是啥原因引起的,我們會把錯誤的堆疊資訊給到error欄位。
  • 5.當發生異常的時候,我們還會需要返回一些額外的補充資料給前端,比如使用者登陸失敗一次和失敗多次需要不同的互動效果,此時我們會在metaInfo裡面返回登陸失敗次數;或者在使用者操作沒有許可權的時候,根據使用者是否已登陸來確定此時是跳轉登陸頁面還是直接彈窗提示當前操作沒有許可權。

定義好返回結果後,我們和前端的互動資料結果就統一好了。

異常的定義

之所以定義一個統一的異常類,是為了把所有的異常全部彙總成一個異常,最終我們只需要在例外處理的時候單獨處理這一個異常即可。

@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就是在產生異常的時候,需要給到使用者的提示訊息。

這樣的話,我們的後端開發人員遇到異常的情況,只需要通過建立AwesomeException異常物件丟擲即可,不需要再為建立什麼異常而煩惱了。

異常的處理

我們下面需要針對所有丟擲的異常進行統一的處理:

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是我們大多數業務邏輯丟擲來的異常,我們根據AwesomeException裡面的code、msg和它包裝的cause,封裝成一個最終的響應資料ResultWrap。

另一個RuntimeException是必須要額外處理的,任何開發人員都無法保證自己的程式碼是完全沒有bug的,任何的空指標異常都會影響使用者體驗,這種編碼性的錯誤我們需要通過統一的錯誤處理讓它變得更柔和一點。

返回結果的處理

我們需要針對所有的返回結果進行檢查,如果不是ResultWrap型別的返回資料,我們需要包裝一下,以便保證我們和前端開發人員達成的共識。

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;
}

至此,我們把整個例外處理與返回結果的統一處理全部關聯起來了,我們後端的開發人員無論是返回異常還是返回ResultWrap或者其他資料結果,都能很好地保證與前端開發人員的正常共同作業,不必為資料結構的變化過多地溝通。

完整程式碼

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;
  //  業務處理結果
  private T data;
  //  產生錯誤的情況下,提示使用者資訊
  private String message;
  //  產生錯誤情況下的異常堆疊,提示開發人員
  private String error;
  //  發生錯誤的時候,返回的附加資訊
  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專案例外處理及返回結果統一的文章就介紹到這了,更多相關Springboot例外處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com