SpringBoot統一返回格式及參數校驗
說明:以下內容摘抄自以下博文:
https://www.cnblogs.com/jianzh5/p/15018838.html
https://www.cnblogs.com/jianzh5/p/15131121.html
一、SpringBoot統一返回格式
一個標准的返回格式至少包含3部分:當然也可以按需加入其他擴展值,比如我們就在返回對象中添加了接口調用時間
- status 狀態值:由后端統一定義各種返回結果的狀態碼
- message 描述:本次接口調用的結果描述
- data 數據:本次返回的數據。
- timestamp: 接口調用時間
{
"status":"100", "message":"操作成功", "data":"hello,javadaily" }
1. 定義返回對象
package com.linwei.jsr.demo.base;
import com.linwei.jsr.demo.enums.ReturnCodeEnum; import lombok.Data; /** * @author: Linwei * @date 2021/8/12 * @Description: */ @Data public class ResultData<T> { private int status; private String message; private T data; private long timestamp ; public ResultData (){ this.timestamp = System.currentTimeMillis(); } public static <T> ResultData<T> success(T data) { ResultData<T> resultData = new ResultData<>(); resultData.setStatus(ReturnCodeEnum.RC100.getCode()); resultData.setMessage(ReturnCodeEnum.RC100.getMessage()); resultData.setData(data); return resultData; } public static <T> ResultData<T> fail(int code, String message) { ResultData<T> resultData = new ResultData<T>(); resultData.setStatus(code); resultData.setMessage(message); return resultData; } }
2. 定義狀態碼枚舉類
package com.linwei.jsr.demo.enums;
/**
* @author: Linwei
* @date 2021/8/12
* @Description:
*/
public enum ReturnCodeEnum { /**操作成功**/ RC100(100,"操作成功"), /**操作失敗**/ RC999(999,"操作失敗"), /**服務限流**/ RC200(200,"服務開啟限流保護,請稍后再試!"), /**服務降級**/ RC201(201,"服務開啟降級保護,請稍后再試!"), /**熱點參數限流**/ RC202(202,"熱點參數限流,請稍后再試!"), /**系統規則不滿足**/ RC203(203,"系統規則不滿足要求,請稍后再試!"), /**授權規則不通過**/ RC204(204,"授權規則不通過,請稍后再試!"), /**access_denied**/ RC403(403,"無訪問權限,請聯系管理員授予權限"), /**access_denied**/ RC401(401,"匿名用戶訪問無權限資源時的異常"), /**服務異常**/ RC500(500,"系統異常,請稍后重試"), INVALID_TOKEN(2001,"訪問令牌不合法"), ACCESS_DENIED(2003,"沒有權限訪問該資源"), CLIENT_AUTHENTICATION_FAILED(1001,"客戶端認證失敗"), USERNAME_OR_PASSWORD_ERROR(1002,"用戶名或密碼錯誤"), UNSUPPORTED_GRANT_TYPE(1003, "不支持的認證模式"); /**自定義狀態碼**/ private final int code; /**自定義描述**/ private final String message; ReturnCodeEnum(int code, String message){ this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }
3. 統一返回格式驗證
@ApiOperation("正常普通請求")
@GetMapping("/hello") public ResultData<String> getInfo(){ return ResultData.success("hello,java"); }
此時調用接口獲取到的返回值是這樣:
{
"status": 100, "message": "操作成功", "data": "hello,java", "timestamp": 1628764577677 }
這樣確實已經實現了我們想要的結果,我在很多項目中看到的都是這種寫法,在Controller層通過ResultData.success()
對返回結果進行包裝后返回給前端。
看到這里我們不妨停下來想想,這樣做有什么弊端呢?
最大的弊端就是我們后面每寫一個接口都需要調用ResultData.success()
這行代碼對結果進行包裝,重復勞動,浪費體力;而且還很容易被其他老鳥給嘲笑。
所以呢我們需要對代碼進行優化,目標就是不要每個接口都手工制定ResultData
返回值。
要優化這段代碼很簡單,我們只需要借助SpringBoot提供的ResponseBodyAdvice
即可。我們只需要編寫一個具體實現類即可。
package com.linwei.jsr.demo.base;
import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * @author: Linwei * @date 2021/8/12 * @Description: * 避免每個接口都手工制定ResultData返回值 * 借助SpringBoot提供的ResponseBodyAdvice, controller直接返回數據對象即可,advice自動封裝成統一返回對象; * --增加該類后,Knife4j訪問報錯,通過basePackages可解決,如下: * * swagger相當於是寄宿在應用程序中的一個web服務,統一響應處理器攔截了應用所有的響應,對swagger-ui的響應產生了影響。 * * 解決集成Swagger出現404問題,配置統一響應處理器攔截的范圍,只攔截本項目的Controller類 */ @RestControllerAdvice(basePackages = "com.linwei.jsr.demo.controller") public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper; // 啟用 advice功能 ; 默認false @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } if(o instanceof ResultData){ return o; } return ResultData.success(o); } }
@RestControllerAdvice
是@RestController
注解的增強,可以實現三個方面的功能:
- 全局異常處理
- 全局數據綁定
- 全局數據預處理
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o)); }
這段代碼一定要加,如果Controller直接返回String的話,SpringBoot是直接返回,故我們需要手動轉換成json。
經過上面的處理我們就再也不需要通過ResultData.success()
來進行轉換了,直接返回原始數據格式,SpringBoot自動幫我們實現包裝類的封裝。
@ApiOperation("正常普通請求")
@GetMapping("/hello") public String getInfo(){ return "hello,java"; }
{
"status":100, "message":"操作成功", "data":"hello,java", "timestamp":1628764866582 }
是不是感覺很完美,別急,還有個問題在等着你呢。
接口異常問題
此時有個問題,由於我們沒對Controller的異常進行處理,當我們調用的方法一旦出現異常,就會出現問題,比如下面這個接口
@ApiOperation("系統異常測試")
@GetMapping("/wrong") public int error(){ int i = 9/0; return i; }
{
"timestamp": "2021-08-12T10:43:05.251+00:00", "status": 500, "error": "Internal Server Error", "path": "/result/wrong" }
這顯然不是我們想要的結果,沒有按照我們統一的格式返回數據,前端看了會打人的。
別急,接下來我們進入第二個議題,如何優雅的處理全局異常。
這個時候,我們還是要用到@RestControllerAdvice 這個注解,上面提到,它也可用於全局異常處理器;
package com.linwei.jsr.demo.base;
import com.linwei.jsr.demo.enums.ReturnCodeEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import java.util.stream.Collectors; /** * @author: Linwei * @date 2021/8/12 * @Description: * 服務層全局響應異常處理器 */ @Slf4j @RestControllerAdvice public class RestExceptionHandler { /** * 默認全局異常處理。 * @param e the e * @return ResultData */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultData<String> exception(Exception e) { log.error("全局異常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCodeEnum.RC500.getCode(),e.getMessage()); } @ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class}) public ResponseEntity<ResultData<String>> handleValidatedException(Exception e) { ResultData<String> resp = null; if (e instanceof MethodArgumentNotValidException) { // BeanValidation exception MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getBindingResult().getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof ConstraintViolationException) { // BeanValidation GET simple param ConstraintViolationException ex = (ConstraintViolationException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof BindException) { // BeanValidation GET object param BindException ex = (BindException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } log.error("參數校驗異常:{}",resp.getMessage()); return new ResponseEntity<>(resp,HttpStatus.BAD_REQUEST); } }
全局異常接入返回的標准格式
要讓全局異常接入標准格式很簡單,因為全局異常處理器已經幫我們封裝好了標准格式,我們只需要直接返回給客戶端即可。關鍵代碼:
ResponseAdvice.java
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } if(o instanceof ResultData){ return o; } return ResultData.success(o); }
這時候我們再調用上面的錯誤方法,返回的結果就符合我們的要求了。
package com.linwei.jsr.demo.controller;
import com.linwei.jsr.demo.base.ResultData; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author: Linwei * @date 2021/8/12 * @Description: */ @Api("統一返回結果測試接口") @RequestMapping("/result") @RestController public class ResultTestController { @ApiOperation("正常普通請求") @GetMapping("/hello") public String getInfo(){ return "hello,java"; } @ApiOperation("系統異常測試") @GetMapping("/wrong") public int error(){ int i = 9/0; return i; } @ApiOperation("自定義異常測試") @GetMapping("error1") public void empty(){ throw new RuntimeException("自定義異常"); } }
/result/wrong
{
"status": 500, "message": "/ by zero", "data": null, "timestamp": 1628765343273 }
/result/error1
{
"status": 500, "message": "自定義異常", "data": null, "timestamp": 1628765364205 }
好了,今天的文章就到這里了,希望通過這篇文章你能掌握如何在你項目中友好實現統一標准格式到返回並且可以優雅的處理全局異常。
二、SpringBoot中集成參數校驗
在日常的接口開發中,為了防止非法參數對業務造成影響,經常需要對接口的參數做校驗,例如登錄的時候需要校驗用戶名密碼是否為空,創建用戶的時候需要校驗郵件、手機號碼格式是否准確。靠代碼對接口參數一個個校驗的話就太繁瑣了,代碼可讀性極差。
Validator框架就是為了解決開發人員在開發的時候少寫代碼,提升開發效率;Validator專門用來進行接口參數校驗,例如常見的必填校驗,email格式校驗,用戶名必須位於6到12之間 等等
Validator校驗框架遵循了JSR-303驗證規范(參數校驗規范), JSR是 Java Specification Requests
的縮寫。
1. 加依賴
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-validation</artifactid> </dependency>
注:從 springboot-2.3
開始,校驗包被獨立成了一個 starter
組件,所以需要引入validation和web,而 springboot-2.3
之前的版本只需要引入 web 依賴就可以了。
2. 定義一個用來測試的實體
package com.linwei.jsr.demo.entity; import io.swagger.annotations.ApiModel; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; /** * @author: Linwei * @date 2021/8/12 * @Description: */ @Data @ApiModel("測試實體") public class TestEntityVO { private String id; @Length(min = 6,max = 12,message = "appId長度必須位於6到12之間") private String appId; @NotBlank(message = "名字為必填項") private String name; @Email(message = "請填寫正確的郵箱地址") private String email; private String sex; @NotEmpty(message = "級別不能為空") private String level; }
在實際開發中對於需要校驗的字段都需要設置對應的業務提示,即message屬性。
常見的約束注解如下:
注解 | 功能 |
---|---|
@AssertFalse | 可以為null,如果不為null的話必須為false |
@AssertTrue | 可以為null,如果不為null的話必須為true |
@DecimalMax | 設置不能超過最大值 |
@DecimalMin | 設置不能超過最小值 |
@Digits | 設置必須是數字且數字整數的位數和小數的位數必須在指定范圍內 |
@Future | 日期必須在當前日期的未來 |
@Past | 日期必須在當前日期的過去 |
@Max | 最大不得超過此最大值 |
@Min | 最大不得小於此最小值 |
@NotNull | 不能為null,可以是空 |
@Null | 必須為null |
@Pattern | 必須滿足指定的正則表達式 |
@Size | 集合、數組、map等的size()值必須在指定范圍內 |
必須是email格式 | |
@Length | 長度必須在指定范圍內 |
@NotBlank | 字符串不能為null,字符串trim()后也不能等於“” |
@NotEmpty | 不能為null,集合、數組、map等size()不能為0;字符串trim()后可以等於“” |
@Range | 值必須在指定范圍內 |
@URL | 必須是一個URL |
注:此表格只是簡單的對注解功能的說明,並沒有對每一個注解的屬性進行說明;可詳見源碼。
3. 定義一個controller測試
package com.linwei.jsr.demo.controller; import com.linwei.jsr.demo.entity.TestEntityVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.Email; /** * @author: Linwei * @date 2021/8/12 * @Description: */ @Slf4j @Validated @Api("JSR參數校驗測試接口") @RequestMapping("/valid") @RestController public class ValidTestController { @ApiOperation("RequestBody校驗") @PostMapping("/test1") public String test1(@Validated @RequestBody TestEntityVO validVO){ log.info("validEntity is {}", validVO); return "test1 valid success"; } @ApiOperation("Form校驗") @PostMapping(value = "/test2") public String test2(@Validated TestEntityVO validVO){ log.info("validEntity is {}", validVO); return "test2 valid success"; } @ApiOperation("單參數校驗") @PostMapping(value = "/test3") public String test3(@Email String email){ log.info("email is {}", email); return "email valid success"; } }
雖然我們之前定義了全局異常攔截器,也看到了攔截器確實生效了,但是 Validator
校驗框架返回的錯誤提示太臃腫了,不便於閱讀,為了方便前端提示,我們需要將其簡化一下。
直接修改之前定義的 RestExceptionHandler
,單獨攔截參數校驗的三個異常:javax.validation.ConstraintViolationException
,org.springframework.validation.BindException
,org.springframework.web.bind.MethodArgumentNotValidException
,代碼如下:
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class}) public ResponseEntity<resultdata<string>> handleValidatedException(Exception e) { ResultData<string> resp = null; if (e instanceof MethodArgumentNotValidException) { // BeanValidation exception MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getBindingResult().getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof ConstraintViolationException) { // BeanValidation GET simple param ConstraintViolationException ex = (ConstraintViolationException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof BindException) { // BeanValidation GET object param BindException ex = (BindException) e; resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(), ex.getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } return new ResponseEntity<>(resp,HttpStatus.BAD_REQUEST); }
4. 自定義參數校驗
雖然Spring Validation 提供的注解基本上夠用,但是面對復雜的定義,我們還是需要自己定義相關注解來實現自動校驗。
比如上面實體類中的sex性別屬性,只允許前端傳遞傳 M,F 這2個枚舉值,如何實現呢?
第一步,創建自定義注解
package com.linwei.jsr.demo.vaild; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * @author: Linwei * @date 2021/8/12 * @Description: */ @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Repeatable(EnumString.List.class) @Documented @Constraint(validatedBy = EnumStringValidator.class)//標明由哪個類執行校驗邏輯 public @interface EnumString { String message() default "value not in enum values."; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; /** * @return date must in this value array */ String[] value(); /** * Defines several {@link EnumString} annotations on the same element. * * @see EnumString */ @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @interface List { EnumString[] value(); } }
第二步,自定義校驗邏輯
package com.linwei.jsr.demo.vaild; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.Arrays; import java.util.List; /** * @author: Linwei * @date 2021/8/12 * @Description: */ public class EnumStringValidator implements ConstraintValidator<EnumString, String> { private List<String> enumStringList; @Override public void initialize(EnumString constraintAnnotation) { enumStringList = Arrays.asList(constraintAnnotation.value()); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(value == null){ return true; } return enumStringList.contains(value); } }
第三步,在字段上增加注解
@ApiModelProperty(value = "性別") @EnumString(value = {"F","M"}, message="性別只允許為F或M") private String sex;
第四步,體驗效果
POST http://localhost:8080/valid/test2 Content-Type: application/x-www-form-urlencoded id=1&name=javadaily&level=12&email=476938977@qq.com&appId=ab1cdddd&sex=N
{ "status": 400, "message": "性別只允許為F或M", "data": null, "timestamp": 1628767471838 }