本篇文章給大家分享平時開發中總結的一點小技巧!在作業中寫過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 =https://www.cnblogs.com/hehe199316/p/ 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/231721.html
標籤:Java
上一篇:[Go]字串轉int64數值型
