今天繼續為大家分享在工作中如何優雅的校驗接口的參數的合法性以及如何統一處理接口返回的json格式。每個字都是干貨,原創不易,分享不易。
validation主要是校驗用戶提交的數據的合法性,比如是否為空,密碼是否符合規則,郵箱格式是否正確等等,校驗框架比較多,用的比較多的是hibernate-validator, 也支持國際化,也可以自定義校驗類型的注解,這里只是簡單地演示校驗框架在Spring Boot中的簡單集成,要想了解更多可以參考 hibernate-validator。
1. pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. dto
public class UserInfoIDto {
private Long id;
@NotBlank
@Length(min=3, max=10)
private String username;
@NotBlank
@Email
private String email;
@NotBlank
@Pattern(regexp="^((13[0-9])|(15[^4,\D])|(18[0,3-9]))\d{8}$", message="手機號格式不正確")
private String phone;
@Min(value=18)
@Max(value = 200)
private int age;
@NotBlank
@Length(min=6, max=12, message="昵稱長度為6到12位")
private String nickname;
// Getter & Setter
}
3. controller
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
@RestController
public class SimpleController {
@PostMApping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto, BindingResult result){
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
String field = fieldError.getField();
String msg = fieldError.getDefaultMessage();
return field + ":" + msg;
}
System.out.println("開始注冊用戶...");
return "success";
}
}
4. 去掉BindingResult參數
每個接口都需要BindingResult參數,而且每個接口都需要處理錯誤信息,這樣增加一個參數也不優雅,處理錯誤信息代碼量也很重復。如果去掉BindingResult參數,系統就會報錯MethodArgumentNotValidException,我們只需要使用全局異常來捕獲該錯誤,處理一下就可以省略傳BindingResult參數了。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("開始注冊用戶...");
return "success";
}
}
@RestControllerAdvice 用于攔截所有的@RestController
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 從異常對象中拿到ObjectError對象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取錯誤提示信息進行返回
return objectError.getDefaultMessage();
}
}
5. 統一返回格式
錯誤碼枚舉
@Getter
public enum ErrorCodeEnum {
SUCCESS(1000, "成功"),
FAILED(1001, "響應失敗"),
VALIDATE_FAILED(1002, "參數校驗失敗"),
ERROR(5000, "未知錯誤");
private Integer code;
private String msg;
ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
自定義異常。
@Getter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException(ErrorCodeEnum errorCodeEnum) {
super(errorCodeEnum.getMsg());
this.code = errorCodeEnum.getCode();
this.msg = errorCodeEnum.getMsg();
}
}
定義返回格式。
@Getter
public class Response<T> {
/**
* 狀態碼,比如1000代表響應成功
*/
private int code;
/**
* 響應信息,用來說明響應情況
*/
private String msg;
/**
* 響應的具體數據
*/
private T data;
public Response(T data) {
this.code = ErrorCodeEnum.SUCCESS.getCode();
this.msg = ErrorCodeEnum.SUCCESS.getMsg();
this.data = data;
}
public Response(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
全局異常處理器增加對APIException的攔截,并修改異常時返回的數據格式。
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 從異常對象中拿到ObjectError對象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取錯誤提示信息進行返回
return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
}
@ExceptionHandler(APIException.class)
public Response<String> APIExceptionHandler(APIException e) {
return new Response<>(e.getCode(), e.getMsg());
}
}
SimpleController 增加一個拋出異常的方法。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("開始注冊用戶...");
return "success";
}
@GetMapping("/users")
public Response<UserInfoIDto> list() {
UserInfoIDto userInfoIDto = new UserInfoIDto();
userInfoIDto.setUsername("monday");
userInfoIDto.setAge(30);
userInfoIDto.setPhone("123456789");
if (true) {
throw new APIException(ErrorCodeEnum.ERROR);
}
// 為了保持數據格式統一,必須使用Response包裝一下
return new Response<>(userInfoIDto);
}
}
報錯返回的格式。
不報錯,返回的格式。
6. 去掉接口中的Response包裝
@RestControllerAdvice既可以全局攔截異常也可攔截指定包下正常的返回值,可以對返回值進行修改。
@RestControllerAdvice(basePackages = {"com.example.validator.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
/**
* 對那些方法需要包裝,如果接口直接返回Response就沒有必要再包裝了
*
* @param returnType
* @param aClass
* @return 如果為true才會執行beforeBodyWrite
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
return !returnType.getParameterType().equals(Response.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String類型不能直接包裝,所以要進行些特別的處理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 將數據包裝在Response里后,再轉換為json字符串響應給前端
return objectMapper.writeValueAsString(new Response<>(data));
} catch (JsonProcessingException e) {
throw new APIException(ErrorCodeEnum.ERROR);
}
}
// 這里統一包裝
return new Response<>(data);
}
}
@RestController
public class SimpleController {
@GetMapping("/users")
public UserInfoIDto list() {
UserInfoIDto userInfoIDto = new UserInfoIDto();
userInfoIDto.setUsername("monday");
userInfoIDto.setAge(30);
userInfoIDto.setPhone("123456789");
// 直接返回值,不需要再使用Response包裝
return userInfoIDto;
}
}
7. 每個校驗錯誤都對應不同的錯誤碼
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ValidateErrorCode {
/** 校驗錯誤碼 code */
int value() default 100000;
}
@Data
public class UserInfoIDto {
@NotBlank
@Email
@ValidateErrorCode(value = 20000)
private String email;
@NotBlank
@Pattern(regexp="^((13[0-9])|(15[^4,\D])|(18[0,3-9]))\d{8}$", message="手機號格式不正確")
@ValidateErrorCode(value = 30000)
private String phone;
}
校驗異常獲取注解中的錯誤碼。
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) throws NoSuchFieldException {
// 從異常對象中拿到ObjectError對象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 參數的Class對象,等下好通過字段名稱獲取Field對象
Class<?> parameterType = e.getParameter().getParameterType();
// 拿到錯誤的字段名稱
String fieldName = e.getBindingResult().getFieldError().getField();
Field field = parameterType.getDeclaredField(fieldName);
// 獲取Field對象上的自定義注解
ValidateErrorCode annotation = field.getAnnotation(ValidateErrorCode.class);
if (annotation != null) {
return new Response<>(annotation.value(),objectError.getDefaultMessage());
}
// 然后提取錯誤提示信息進行返回
return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
}
8. 個別接口不統一包裝響應
有時候第三方接口回調我們的接口,我們的接口必須按照第三方定義的返回格式來,此時第三方不一定和我們自己的返回格式一樣,所以要提供一種可以繞過統一包裝的方式。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseWrap {
}
@RestController
public class SimpleController {
@NotResponseWrap
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("開始注冊用戶...");
return "success";
}
}
ResponseControllerAdvice 增加一個不包裝的條件,配置了@NotResponseWrap注解就跳過包裝。
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
return !(returnType.getParameterType().equals(Response.class) || returnType.hasMethodAnnotation(NotResponseWrap.class));
}