Java如何優雅地實現接口數據校驗


本篇文章給大家分享平時開發中總結的一點小技巧!在工作中寫過Java程序的朋友都知道,目前使用Java開發服務最主流的方式就是通過Spring MVC定義一個Controller層接口,並將接口請求或返回參數分別定義在一個Java實體類中,這樣Spring MVC在接收到Http請求(POST/GET)后,就會自動將請求報文自動映射成一個Java對象。這樣的代碼通常是這樣寫的:

 

@RestController
public class OrderController {

    @Autowired
    private OrderService orderServiceImpl;

    @PostMapping("/createOrder")
    public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
        return orderServiceImpl.createOrder(createOrderDTO);
    }
}

 

 

 

這樣的代碼相信大家並不陌生,但在后續的邏輯實現過程中卻會遇到這樣的問題:“在接收請求參數后如何實現報文對象數據值的合法性校驗?”。一些同學也可能認為這並不是什么問題,因為具體某個參數字段是否為空、值的取值是否在約定范圍、格式是否合法等等,在業務代碼中校驗就好了。例如可以在Service實現類中對報文格式進行各種if-else的數據校驗。

 

從功能上說冗余的if-else代碼沒啥毛病,但從代碼的優雅性來說冗長的if-else代碼會顯得非常臃腫。接下來的內容將給大家介紹一種處理此類問題的實用方法。具體將從以下幾個方面進行介紹:

 

  • 使用@Validated注解實現Controller接口層數據直接綁定校驗;

  • 擴展約束性注解實現數據取值范圍的校驗;

  • 更加靈活的對象數據合法性校驗工具類封裝;

  • 數據合法性校驗結果異常統一返回處理;

 

 

Controller接口層數據綁定校驗

 

 

實際上在Java開發中目前普通使用的Bean數據校驗工具是"hibernate-validator",它是一個hibernete獨立的jar包,所以使用這個jar包並不需要一定要集成Hibernete框架。該jar包主要實現並擴展了javax.validation(是一個基於JSR-303標准開發出來的Bean校驗規范)接口。

 

由於Spring Boot在內部默認集成了"hibernate-validator",所以使用Spring Boot構建的Java工程可以直接使用相關注解來實現Bean的數據校驗。例如我們最常編寫的Controller層接口參數對象,可以在定義Bean類時直接編寫這樣的代碼:

 

@Data
public class CreateOrderDTO {

    @NotNull(message = "訂單號不能為空")
    private String orderId;
    @NotNull(message = "訂單金額不能為空")
    @Min(value = 1, message = "訂單金額不能小於0")
    private Integer amount;
    @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "用戶手機號不合法")
    private String mobileNo;
    private String orderType;
    private String status;
}

 

 

 

如上所示代碼,我們可以使用@NotNull注解來約束該字段必須不能為空,也可以使用@Min注解來約束字段的最小取值,或者還可以通過@Pattern注解來使用正則表達式來約束字段的格式(如手機號格式)等等。

 

以上這些注解都是“hibernate-validator”依賴包默認提供的,更多常用的注解還有很多,例如:

 

 

利用這些約束注解,我們就可以很輕松的搞定接口數據校驗,而不需要在業務邏輯中編寫大量的if-else來進行數據合法性校驗。而定義好Bean參數對象並使用相關注解實現參數值約束后,在Controller層接口定義中只需要使用@Validated注解就可以實現在接收參數后自動進行數據綁定校驗了,具體代碼如下:

 

@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
    return orderServiceImpl.createOrder(createOrderDTO);
}

 

 

 

如上所示,在Controller層中通過Spring提供的@Validated注解可以自動實現數據Bean的綁定校驗,如果數據異常則會統一拋出校驗異常!

 

 

約束性注解擴展

 

 

在“hibernate-validator”依賴jar包中,雖然提供了很多很方便的約束注解,但是也有不滿足某些實際需要的情況,例如我們想針對參數中的某個值約定其值的枚舉范圍,如orderType訂單類型只允許傳“pay”、“refund”兩種值,那么現有的約束注解可能就沒有特別適用的了。此外,如果對這樣的枚舉值,我們還想在約束定義中直接匹配代碼中的枚舉定義,以更好地統一接口參數與業務邏輯的枚舉定義。那么這種情況下,我們還可以自己擴展定義相應地約束注解邏輯。

 

接下來我們定義新的約束注解@EnumValue,來實現上面我們所說的效果,具體代碼如下:

 

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {

    //默認錯誤消息
    String message() default "必須為指定值";

    //支持string數組驗證
    String[] strValues() default {};

    //支持int數組驗證
    int[] intValues() default {};

    //支持枚舉列表驗證
    Class<?>[] enumValues() default {};

    //分組
    Class<?>[] groups() default {};

    //負載
    Class<? extends Payload>[] payload() default {};

    //指定多個時使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        EnumValue[] value();
    }

    /**
     * 校驗類邏輯定義
     */
    class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

        //字符串類型數組
        private String[] strValues;
        //int類型數組
        private int[] intValues;
        //枚舉類
        private Class<?>[] enumValues;

        /**
         * 初始化方法
         *
         * @param constraintAnnotation
         */
        @Override
        public void initialize(EnumValue constraintAnnotation) {
            strValues = constraintAnnotation.strValues();
            intValues = constraintAnnotation.intValues();
            enumValues = constraintAnnotation.enumValues();
        }

        /**
         * 校驗方法
         *
         * @param value
         * @param context
         * @return
         */
        @SneakyThrows
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            //針對字符串數組的校驗匹配
            if (strValues != null && strValues.length > 0) {
                if (value instanceof String) {
                    for (String s : strValues) {//判斷值類型是否為Integer類型
                        if (s.equals(value)) {
                            return true;
                        }
                    }
                }
            }
            //針對整型數組的校驗匹配
            if (intValues != null && intValues.length > 0) {
                if (value instanceof Integer) {//判斷值類型是否為Integer類型
                    for (Integer s : intValues) {
                        if (s == value) {
                            return true;
                        }
                    }
                }
            }
            //針對枚舉類型的校驗匹配
            if (enumValues != null && enumValues.length > 0) {
                for (Class<?> cl : enumValues) {
                    if (cl.isEnum()) {
                        //枚舉類驗證
                        Object[] objs = cl.getEnumConstants();
                        //這里需要注意,定義枚舉時,枚舉值名稱統一用value表示
                        Method method = cl.getMethod("getValue");
                        for (Object obj : objs) {
                            Object code = method.invoke(obj, null);
                            if (value.equals(code.toString())) {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }
    }
}

 

 

 

如上所示的@EnumValue約束注解,是一個非常實用的擴展,通過該注解我們可以實現對參數取值范圍(不是大小范圍)的約束,它支持對int、string以及enum三種數據類型的約束,具體使用方式如下:

 

/**
 * 定制化注解,支持參數值與指定類型數組列表值進行匹配(缺點是需要將枚舉值寫死在字段定義的注解中)
 */
@EnumValue(strValues = {"pay", "refund"}, message = "訂單類型錯誤")
private String orderType;
/**
 * 定制化注解,實現參數值與枚舉列表的自動匹配校驗(能更好地與實際業務開發匹配)
 */
@EnumValue(enumValues = Status.class, message = "狀態值不在指定范圍")
private String status;

 

 

 

如上所示代碼,該擴展注解既可以使用strValues或intValues屬性來編程列舉取值范圍,也可以直接通過enumValues來綁定枚舉定義。但是需要注意,處於通用考慮,具體枚舉定義的屬性的名稱要統一匹配為value、desc,例如Status枚舉定義如下:

 

public enum Status {
    PROCESSING(1, "處理中"),
    SUCCESS(2, "訂單已完成");
    Integer value;
    String desc;

    Status(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    public Integer getValue() {
        return value;
    }

    public String getDesc() {
        return desc;
    }
}

 

 

 

通過注解擴展,就能實現更多方便的約束性注解!

 

 

更加靈活的數據校驗工具類封裝

 

 

除了上面直接在Controller層使用@Validated進行綁定數據校驗外,在有些情況,例如你的參數對象中的某個字段是一個復合對象,或者業務層的某個方法所定義的入參對象也需要進行數據合法性校驗,那么這種情況下如何實現像Controller層一樣的校驗效果呢?

 

需要說明在這種情況下@Validated已經無法直接使用了,因為@Validated注解發揮作用主要是Spring MVC在接收參數的過程中實現了自動數據綁定校驗,而在普通的業務方法或者復合參數對象中是沒有辦法直接綁定校驗的。這種情況下,我們可以通過定義ValidateUtils工具類來實現一樣的校驗效果,具體代碼如下:

 

public class ValidatorUtils {

    private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    /**
     * bean整體校驗,有不合規范,拋出第1個違規異常
     */
    public static void validate(Object obj, Class<?>... groups) {
        Set<ConstraintViolation<Object>> resultSet = validator.validate(obj, groups);
        if (resultSet.size() > 0) {
            //如果存在錯誤結果,則將其解析並進行拼湊后異常拋出
            List<String> errorMessageList = resultSet.stream().map(o -> o.getMessage()).collect(Collectors.toList());
            StringBuilder errorMessage = new StringBuilder();
            errorMessageList.stream().forEach(o -> errorMessage.append(o + ";"));
            throw new IllegalArgumentException(errorMessage.toString());
        }
    }
}

 

 

 

如上所示,我們定義了一個基於"javax.validation"接口的工具類實現,這樣就可以在非@Validated直接綁定校驗的場景中通過校驗工具類來實現對Bean對象約束注解的校驗處理,具體使用代碼如下:

 

public boolean orderCheck(OrderCheckBO orderCheckBO) {
    //對參數對象進行數據校驗
    ValidatorUtils.validate(orderCheckBO);
    return true;
}

 

 

 

而方法入參對象則還是可以繼續使用前面我們介紹的約束性注解進行約定,例如上述方法的入參對象定義如下:

 

@Data
@Builder
public class OrderCheckBO {

    @NotNull(message = "訂單號不能為空")
    private String orderId;
    @Min(value = 1, message = "訂單金額不能小於0")
    private Integer orderAmount;
    @NotNull(message = "創建人不能為空")
    private String operator;
    @NotNull(message = "操作時間不能為空")
    private String operatorTime;
}

 

 

這樣在編程體驗上就可以整體上保持一致!

 

數據合法性校驗結果異常統一處理

 

通過前面我們所講的各種約束注解,我們實現了對Controller層接口以及業務方法參數對象的統一數據校驗。而為了保持校驗異常處理的統一處理和錯誤報文統一輸出,我們還可以定義通用的異常處理機制,來保證各類數據校驗錯誤都能以統一錯誤格式反饋給調用方。具體代碼如下:

 

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 統一處理參數校驗錯誤異常(非Spring接口數據綁定驗證)
     *
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public ResponseResult<?> processValidException(HttpServletResponse response, BindException e) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        //獲取校驗錯誤結果信息,並將信息組裝
        List<String> errorStringList = e.getBindingResult().getAllErrors()
                .stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
        String errorMessage = String.join("; ", errorStringList);
        response.setContentType("application/json;charset=UTF-8");
        log.error(e.toString() + "_" + e.getMessage(), e);
        return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),
                errorMessage);
    }

    /**
     * 統一處理參數校驗錯誤異常
     *
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public ResponseResult<?> processValidException(HttpServletResponse response, IllegalArgumentException e) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        String errorMessage = String.join("; ", e.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        log.error(e.toString() + "_" + e.getMessage(), e);
        return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),
                errorMessage);
    }

    ...
}

 

 

 

如上所示,我們定義了針對前面兩種數據校驗方式的統一異常處理機制,這樣數據校驗的錯誤信息就能通過統一的報文格式反饋給調用端,從而實現接口數據報文的統一返回!

 

其中通用的接口參數對象ResponseResult的代碼定義如下:

 

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "data"})
public class ResponseResult<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 返回的對象
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;
    /**
     * 返回的編碼
     */
    private Integer code;
    /**
     * 返回的信息
     */
    private String message;

    /**
     * @param data 返回的數據
     * @param <T>  返回的數據類型
     * @return 響應結果
     */
    public static <T> ResponseResult<T> OK(T data) {
        return packageObject(data, GlobalCodeEnum.GL_SUCC_0);
    }

    /**
     * 自定義系統異常信息
     *
     * @param code
     * @param message 自定義消息
     * @param <T>
     * @return
     */
    public static <T> ResponseResult<T> systemException(Integer code, String message) {
        return packageObject(null, code, message);
    }
}

 

 

 

當然,這樣的統一報文格式也不僅僅只處理異常返回,正常的數據報文格式也可以通過該對象來進行統一封裝!

 

本文內容從實用的角度給大家演示了,如何在日常工作中編寫通用的數據校驗邏輯,希望能對大家有所幫助

 

 

有完整的Java初級,高級對應的學習路線和資料!專注於java開發。分享java基礎、原理性知識、JavaWeb實戰、spring全家桶、設計模式、分布式及面試資料、開源項目,助力開發者成長!

 

關注微信公眾號:碼邦主,獲取更對技術干貨


 

作者:無敵碼農

來源:https://mp.weixin.qq.com/s/9kKIDZYB7bR7jiC5vj6qMg

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM