今天繼續(xù)為大家分享在工作中如何優(yōu)雅的校驗(yàn)接口的參數(shù)的合法性以及如何統(tǒng)一處理接口返回的json格式。每個(gè)字都是干貨,原創(chuàng)不易,分享不易。
validation主要是校驗(yàn)用戶(hù)提交的數(shù)據(jù)的合法性,比如是否為空,密碼是否符合規(guī)則,郵箱格式是否正確等等,校驗(yàn)框架比較多,用的比較多的是hibernate-validator, 也支持國(guó)際化,也可以自定義校驗(yàn)類(lèi)型的注解,這里只是簡(jiǎn)單地演示校驗(yàn)框架在Spring Boot中的簡(jiǎn)單集成,要想了解更多可以參考 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="手機(jī)號(hào)格式不正確")
private String phone;
@Min(value=18)
@Max(value = 200)
private int age;
@NotBlank
@Length(min=6, max=12, message="昵稱(chēng)長(zhǎng)度為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("開(kāi)始注冊(cè)用戶(hù)...");
return "success";
}
}
4. 去掉BindingResult參數(shù)
每個(gè)接口都需要BindingResult參數(shù),而且每個(gè)接口都需要處理錯(cuò)誤信息,這樣增加一個(gè)參數(shù)也不優(yōu)雅,處理錯(cuò)誤信息代碼量也很重復(fù)。如果去掉BindingResult參數(shù),系統(tǒng)就會(huì)報(bào)錯(cuò)MethodArgumentNotValidException,我們只需要使用全局異常來(lái)捕獲該錯(cuò)誤,處理一下就可以省略傳BindingResult參數(shù)了。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("開(kāi)始注冊(cè)用戶(hù)...");
return "success";
}
}
@RestControllerAdvice 用于攔截所有的@RestController
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 從異常對(duì)象中拿到ObjectError對(duì)象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取錯(cuò)誤提示信息進(jìn)行返回
return objectError.getDefaultMessage();
}
}
5. 統(tǒng)一返回格式
錯(cuò)誤碼枚舉
@Getter
public enum ErrorCodeEnum {
SUCCESS(1000, "成功"),
FAILED(1001, "響應(yīng)失敗"),
VALIDATE_FAILED(1002, "參數(shù)校驗(yàn)失敗"),
ERROR(5000, "未知錯(cuò)誤");
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> {
/**
* 狀態(tài)碼,比如1000代表響應(yīng)成功
*/
private int code;
/**
* 響應(yīng)信息,用來(lái)說(shuō)明響應(yīng)情況
*/
private String msg;
/**
* 響應(yīng)的具體數(shù)據(jù)
*/
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;
}
}
全局異常處理器增加對(duì)APIException的攔截,并修改異常時(shí)返回的數(shù)據(jù)格式。
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 從異常對(duì)象中拿到ObjectError對(duì)象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取錯(cuò)誤提示信息進(jìn)行返回
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 增加一個(gè)拋出異常的方法。
@RestController
public class SimpleController {
@PostMapping("/users")
public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
System.out.println("開(kāi)始注冊(cè)用戶(hù)...");
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);
}
// 為了保持?jǐn)?shù)據(jù)格式統(tǒng)一,必須使用Response包裝一下
return new Response<>(userInfoIDto);
}
}
報(bào)錯(cuò)返回的格式。
不報(bào)錯(cuò),返回的格式。
6. 去掉接口中的Response包裝
@RestControllerAdvice既可以全局?jǐn)r截異常也可攔截指定包下正常的返回值,可以對(duì)返回值進(jìn)行修改。
@RestControllerAdvice(basePackages = {"com.example.validator.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
/**
* 對(duì)那些方法需要包裝,如果接口直接返回Response就沒(méi)有必要再包裝了
*
* @param returnType
* @param aClass
* @return 如果為true才會(huì)執(zhí)行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類(lèi)型不能直接包裝,所以要進(jìn)行些特別的處理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 將數(shù)據(jù)包裝在Response里后,再轉(zhuǎn)換為json字符串響應(yīng)給前端
return objectMapper.writeValueAsString(new Response<>(data));
} catch (JsonProcessingException e) {
throw new APIException(ErrorCodeEnum.ERROR);
}
}
// 這里統(tǒng)一包裝
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. 每個(gè)校驗(yàn)錯(cuò)誤都對(duì)應(yīng)不同的錯(cuò)誤碼
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ValidateErrorCode {
/** 校驗(yàn)錯(cuò)誤碼 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="手機(jī)號(hào)格式不正確")
@ValidateErrorCode(value = 30000)
private String phone;
}
校驗(yàn)異常獲取注解中的錯(cuò)誤碼。
@ExceptionHandler(MethodArgumentNotValidException.class)
public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) throws NoSuchFieldException {
// 從異常對(duì)象中拿到ObjectError對(duì)象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 參數(shù)的Class對(duì)象,等下好通過(guò)字段名稱(chēng)獲取Field對(duì)象
Class<?> parameterType = e.getParameter().getParameterType();
// 拿到錯(cuò)誤的字段名稱(chēng)
String fieldName = e.getBindingResult().getFieldError().getField();
Field field = parameterType.getDeclaredField(fieldName);
// 獲取Field對(duì)象上的自定義注解
ValidateErrorCode annotation = field.getAnnotation(ValidateErrorCode.class);
if (annotation != null) {
return new Response<>(annotation.value(),objectError.getDefaultMessage());
}
// 然后提取錯(cuò)誤提示信息進(jìn)行返回
return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
}
8. 個(gè)別接口不統(tǒng)一包裝響應(yīng)
有時(shí)候第三方接口回調(diào)我們的接口,我們的接口必須按照第三方定義的返回格式來(lái),此時(shí)第三方不一定和我們自己的返回格式一樣,所以要提供一種可以繞過(guò)統(tǒng)一包裝的方式。
@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("開(kāi)始注冊(cè)用戶(hù)...");
return "success";
}
}
ResponseControllerAdvice 增加一個(gè)不包裝的條件,配置了@NotResponseWrap注解就跳過(guò)包裝。
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
return !(returnType.getParameterType().equals(Response.class) || returnType.hasMethodAnnotation(NotResponseWrap.class));
}






