Spring Boot 優雅地實現接口參數校驗

今天繼續爲你們分享在工做中如何優雅的校驗接口的參數的合法性以及如何統一處理接口返回的json格式。每一個字都是乾貨,原創不易,分享不易。前端

另外我本身也整理了一些Java資料,須要的能夠自行領取! 最全學習筆記大廠真題+微服務+MySQL+分佈式+SSM框架+Java+Redis+數據結構與算法+網絡+Linux+Spring全家桶+JVM+高併發+各大學習思惟腦圖+面試集合面試

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參數了。spring

@RestController
public class SimpleController {

    @PostMapping("/users")
    public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
        System.out.println("開始註冊用戶...");
        return "success";
    }
}

複製代碼

@RestControllerAdvice 用於攔截全部的@RestControllerjson

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 從異常對象中拿到ObjectError對象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 而後提取錯誤提示信息進行返回
        return objectError.getDefaultMessage();
    }
}

複製代碼

5. 統一返回格式

錯誤碼枚舉markdown

@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 增長一個拋出異常的方法。app

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

複製代碼

相關文章
相關標籤/搜索