五分鐘帶你了解Java的接口數(shù)據(jù)校驗(yàn)
本篇文章給大家分享平時(shí)開(kāi)發(fā)中總結(jié)的一點(diǎn)小技巧!在工作中寫過(guò)Java程序的朋友都知道,目前使用Java開(kāi)發(fā)服務(wù)最主流的方式就是通過(guò)Spring MVC定義一個(gè)Controller層接口,并將接口請(qǐng)求或返回參數(shù)分別定義在一個(gè)Java實(shí)體類中,這樣Spring MVC在接收到Http請(qǐng)求(POST/GET)后,就會(huì)自動(dòng)將請(qǐng)求報(bào)文自動(dòng)映射成一個(gè)Java對(duì)象。這樣的代碼通常是這樣寫的:
@RestController
public class OrderController {
@Autowired
private OrderService orderServiceImpl;
@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
return orderServiceImpl.createOrder(createOrderDTO);
}
}
這樣的代碼相信大家并不陌生,但在后續(xù)的邏輯實(shí)現(xiàn)過(guò)程中卻會(huì)遇到這樣的問(wèn)題:“在接收請(qǐng)求參數(shù)后如何實(shí)現(xiàn)報(bào)文對(duì)象數(shù)據(jù)值的合法性校驗(yàn)?”。一些同學(xué)也可能認(rèn)為這并不是什么問(wèn)題,因?yàn)榫唧w某個(gè)參數(shù)字段是否為空、值的取值是否在約定范圍、格式是否合法等等,在業(yè)務(wù)代碼中校驗(yàn)就好了。例如可以在Service實(shí)現(xiàn)類中對(duì)報(bào)文格式進(jìn)行各種if-else的數(shù)據(jù)校驗(yàn)。
從功能上說(shuō)冗余的if-else代碼沒(méi)啥毛病,但從代碼的優(yōu)雅性來(lái)說(shuō)冗長(zhǎng)的if-else代碼會(huì)顯得非常臃腫。接下來(lái)的內(nèi)容將給大家介紹一種處理此類問(wèn)題的實(shí)用方法。具體將從以下幾個(gè)方面進(jìn)行介紹:
- 使用@Validated注解實(shí)現(xiàn)Controller接口層數(shù)據(jù)直接綁定校驗(yàn);
- 擴(kuò)展約束性注解實(shí)現(xiàn)數(shù)據(jù)取值范圍的校驗(yàn);
- 更加靈活的對(duì)象數(shù)據(jù)合法性校驗(yàn)工具類封裝;
- 數(shù)據(jù)合法性校驗(yàn)結(jié)果異常統(tǒng)一返回處理;
Controller接口層數(shù)據(jù)綁定校驗(yàn)
實(shí)際上在Java開(kāi)發(fā)中目前普通使用的Bean數(shù)據(jù)校驗(yàn)工具是"hibernate-validator",它是一個(gè)hibernete獨(dú)立的jar包,所以使用這個(gè)jar包并不需要一定要集成Hibernete框架。該jar包主要實(shí)現(xiàn)并擴(kuò)展了javax.validation(是一個(gè)基于JSR-303標(biāo)準(zhǔn)開(kāi)發(fā)出來(lái)的Bean校驗(yàn)規(guī)范)接口。
由于Spring Boot在內(nèi)部默認(rèn)集成了"hibernate-validator",所以使用Spring Boot構(gòu)建的Java工程可以直接使用相關(guān)注解來(lái)實(shí)現(xiàn)Bean的數(shù)據(jù)校驗(yàn)。例如我們最常編寫的Controller層接口參數(shù)對(duì)象,可以在定義Bean類時(shí)直接編寫這樣的代碼:
@Data
public class CreateOrderDTO {
@NotNull(message = "訂單號(hào)不能為空")
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 = "用戶手機(jī)號(hào)不合法")
private String mobileNo;
private String orderType;
private String status;
}
如上所示代碼,我們可以使用@NotNull注解來(lái)約束該字段必須不能為空,也可以使用@Min注解來(lái)約束字段的最小取值,或者還可以通過(guò)@Pattern注解來(lái)使用正則表達(dá)式來(lái)約束字段的格式(如手機(jī)號(hào)格式)等等。

以上這些注解都是“hibernate-validator”依賴包默認(rèn)提供的,更多常用的注解還有很多,例如:
利用這些約束注解,我們就可以很輕松的搞定接口數(shù)據(jù)校驗(yàn),而不需要在業(yè)務(wù)邏輯中編寫大量的if-else來(lái)進(jìn)行數(shù)據(jù)合法性校驗(yàn)。而定義好Bean參數(shù)對(duì)象并使用相關(guān)注解實(shí)現(xiàn)參數(shù)值約束后,在Controller層接口定義中只需要使用@Validated注解就可以實(shí)現(xiàn)在接收參數(shù)后自動(dòng)進(jìn)行數(shù)據(jù)綁定校驗(yàn)了,具體代碼如下:
@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
return orderServiceImpl.createOrder(createOrderDTO);
}
如上所示,在Controller層中通過(guò)Spring提供的@Validated注解可以自動(dòng)實(shí)現(xiàn)數(shù)據(jù)Bean的綁定校驗(yàn),如果數(shù)據(jù)異常則會(huì)統(tǒng)一拋出校驗(yàn)異常!
約束性注解擴(kuò)展
在“hibernate-validator”依賴jar包中,雖然提供了很多很方便的約束注解,但是也有不滿足某些實(shí)際需要的情況,例如我們想針對(duì)參數(shù)中的某個(gè)值約定其值的枚舉范圍,如orderType訂單類型只允許傳“pay”、“refund”兩種值,那么現(xiàn)有的約束注解可能就沒(méi)有特別適用的了。此外,如果對(duì)這樣的枚舉值,我們還想在約束定義中直接匹配代碼中的枚舉定義,以更好地統(tǒng)一接口參數(shù)與業(yè)務(wù)邏輯的枚舉定義。那么這種情況下,我們還可以自己擴(kuò)展定義相應(yīng)地約束注解邏輯。
接下來(lái)我們定義新的約束注解@EnumValue,來(lái)實(shí)現(xiàn)上面我們所說(shuō)的效果,具體代碼如下:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
//默認(rèn)錯(cuò)誤消息
String message() default "必須為指定值";
//支持string數(shù)組驗(yàn)證
String[] strValues() default {};
//支持int數(shù)組驗(yàn)證
int[] intValues() default {};
//支持枚舉列表驗(yàn)證
Class<?>[] enumValues() default {};
//分組
Class<?>[] groups() default {};
//負(fù)載
Class<? extends Payload>[] payload() default {};
//指定多個(gè)時(shí)使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
/**
* 校驗(yàn)類邏輯定義
*/
class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
//字符串類型數(shù)組
private String[] strValues;
//int類型數(shù)組
private int[] intValues;
//枚舉類
private Class<?>[] enumValues;
/**
* 初始化方法
*
* @param constraintAnnotation
*/
@Override
public void initialize(EnumValue constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
enumValues = constraintAnnotation.enumValues();
}
/**
* 校驗(yàn)方法
*
* @param value
* @param context
* @return
*/
@SneakyThrows
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//針對(duì)字符串?dāng)?shù)組的校驗(yàn)匹配
if (strValues != null && strValues.length > 0) {
if (value instanceof String) {
for (String s : strValues) {//判斷值類型是否為Integer類型
if (s.equals(value)) {
return true;
}
}
}
}
//針對(duì)整型數(shù)組的校驗(yàn)匹配
if (intValues != null && intValues.length > 0) {
if (value instanceof Integer) {//判斷值類型是否為Integer類型
for (Integer s : intValues) {
if (s == value) {
return true;
}
}
}
}
//針對(duì)枚舉類型的校驗(yàn)匹配
if (enumValues != null && enumValues.length > 0) {
for (Class<?> cl : enumValues) {
if (cl.isEnum()) {
//枚舉類驗(yàn)證
Object[] objs = cl.getEnumConstants();
//這里需要注意,定義枚舉時(shí),枚舉值名稱統(tǒng)一用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約束注解,是一個(gè)非常實(shí)用的擴(kuò)展,通過(guò)該注解我們可以實(shí)現(xiàn)對(duì)參數(shù)取值范圍(不是大小范圍)的約束,它支持對(duì)int、string以及enum三種數(shù)據(jù)類型的約束,具體使用方式如下:
/**
* 定制化注解,支持參數(shù)值與指定類型數(shù)組列表值進(jìn)行匹配(缺點(diǎn)是需要將枚舉值寫死在字段定義的注解中)
*/
@EnumValue(strValues = {"pay", "refund"}, message = "訂單類型錯(cuò)誤")
private String orderType;
/**
* 定制化注解,實(shí)現(xiàn)參數(shù)值與枚舉列表的自動(dòng)匹配校驗(yàn)(能更好地與實(shí)際業(yè)務(wù)開(kāi)發(fā)匹配)
*/
@EnumValue(enumValues = Status.class, message = "狀態(tài)值不在指定范圍")
private String status;
如上所示代碼,該擴(kuò)展注解既可以使用strValues或intValues屬性來(lái)編程列舉取值范圍,也可以直接通過(guò)enumValues來(lái)綁定枚舉定義。但是需要注意,處于通用考慮,具體枚舉定義的屬性的名稱要統(tǒng)一匹配為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;
}
}
通過(guò)注解擴(kuò)展,就能實(shí)現(xiàn)更多方便的約束性注解!
更加靈活的數(shù)據(jù)校驗(yàn)工具類封裝
除了上面直接在Controller層使用@Validated進(jìn)行綁定數(shù)據(jù)校驗(yàn)外,在有些情況,例如你的參數(shù)對(duì)象中的某個(gè)字段是一個(gè)復(fù)合對(duì)象,或者業(yè)務(wù)層的某個(gè)方法所定義的入?yún)?duì)象也需要進(jìn)行數(shù)據(jù)合法性校驗(yàn),那么這種情況下如何實(shí)現(xiàn)像Controller層一樣的校驗(yàn)效果呢?
需要說(shuō)明在這種情況下@Validated已經(jīng)無(wú)法直接使用了,因?yàn)锧Validated注解發(fā)揮作用主要是Spring MVC在接收參數(shù)的過(guò)程中實(shí)現(xiàn)了自動(dòng)數(shù)據(jù)綁定校驗(yàn),而在普通的業(yè)務(wù)方法或者復(fù)合參數(shù)對(duì)象中是沒(méi)有辦法直接綁定校驗(yàn)的。這種情況下,我們可以通過(guò)定義ValidateUtils工具類來(lái)實(shí)現(xiàn)一樣的校驗(yàn)效果,具體代碼如下:
public class ValidatorUtils {
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
/**
* bean整體校驗(yàn),有不合規(guī)范,拋出第1個(gè)違規(guī)異常
*/
public static void validate(Object obj, Class<?>... groups) {
Set<ConstraintViolation<Object>> resultSet = validator.validate(obj, groups);
if (resultSet.size() > 0) {
//如果存在錯(cuò)誤結(jié)果,則將其解析并進(jìn)行拼湊后異常拋出
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());
}
}
}
如上所示,我們定義了一個(gè)基于"javax.validation"接口的工具類實(shí)現(xiàn),這樣就可以在非@Validated直接綁定校驗(yàn)的場(chǎng)景中通過(guò)校驗(yàn)工具類來(lái)實(shí)現(xiàn)對(duì)Bean對(duì)象約束注解的校驗(yàn)處理,具體使用代碼如下:
public boolean orderCheck(OrderCheckBO orderCheckBO) {
//對(duì)參數(shù)對(duì)象進(jìn)行數(shù)據(jù)校驗(yàn)
ValidatorUtils.validate(orderCheckBO);
return true;
}
而方法入?yún)?duì)象則還是可以繼續(xù)使用前面我們介紹的約束性注解進(jìn)行約定,例如上述方法的入?yún)?duì)象定義如下:
@Data
@Builder
public class OrderCheckBO {
@NotNull(message = "訂單號(hào)不能為空")
private String orderId;
@Min(value = 1, message = "訂單金額不能小于0")
private Integer orderAmount;
@NotNull(message = "創(chuàng)建人不能為空")
private String operator;
@NotNull(message = "操作時(shí)間不能為空")
private String operatorTime;
}
這樣在編程體驗(yàn)上就可以整體上保持一致!
數(shù)據(jù)合法性校驗(yàn)結(jié)果異常統(tǒng)一處理
通過(guò)前面我們所講的各種約束注解,我們實(shí)現(xiàn)了對(duì)Controller層接口以及業(yè)務(wù)方法參數(shù)對(duì)象的統(tǒng)一數(shù)據(jù)校驗(yàn)。而為了保持校驗(yàn)異常處理的統(tǒng)一處理和錯(cuò)誤報(bào)文統(tǒng)一輸出,我們還可以定義通用的異常處理機(jī)制,來(lái)保證各類數(shù)據(jù)校驗(yàn)錯(cuò)誤都能以統(tǒng)一錯(cuò)誤格式反饋給調(diào)用方。具體代碼如下:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 統(tǒng)一處理參數(shù)校驗(yàn)錯(cuò)誤異常(非Spring接口數(shù)據(jù)綁定驗(yàn)證)
*
* @param response
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public ResponseResult<?> processValidException(HttpServletResponse response, BindException e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//獲取校驗(yàn)錯(cuò)誤結(jié)果信息,并將信息組裝
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);
}
/**
* 統(tǒng)一處理參數(shù)校驗(yàn)錯(cuò)誤異常
*
* @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);
}
...
}
如上所示,我們定義了針對(duì)前面兩種數(shù)據(jù)校驗(yàn)方式的統(tǒng)一異常處理機(jī)制,這樣數(shù)據(jù)校驗(yàn)的錯(cuò)誤信息就能通過(guò)統(tǒng)一的報(bào)文格式反饋給調(diào)用端,從而實(shí)現(xiàn)接口數(shù)據(jù)報(bào)文的統(tǒng)一返回!
其中通用的接口參數(shù)對(duì)象ResponseResult的代碼定義如下:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "data"})
public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 返回的對(duì)象
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
/**
* 返回的編碼
*/
private Integer code;
/**
* 返回的信息
*/
private String message;
/**
* @param data 返回的數(shù)據(jù)
* @param <T> 返回的數(shù)據(jù)類型
* @return 響應(yīng)結(jié)果
*/
public static <T> ResponseResult<T> OK(T data) {
return packageObject(data, GlobalCodeEnum.GL_SUCC_0);
}
/**
* 自定義系統(tǒng)異常信息
*
* @param code
* @param message 自定義消息
* @param <T>
* @return
*/
public static <T> ResponseResult<T> systemException(Integer code, String message) {
return packageObject(null, code, message);
}
}
當(dāng)然,這樣的統(tǒng)一報(bào)文格式也不僅僅只處理異常返回,正常的數(shù)據(jù)報(bào)文格式也可以通過(guò)該對(duì)象來(lái)進(jìn)行統(tǒng)一封裝!
本文內(nèi)容從實(shí)用的角度給大家演示了,如何在日常工作中編寫通用的數(shù)據(jù)校驗(yàn)邏輯,希望能對(duì)大家有所幫助!
到此這篇關(guān)于五分鐘帶你了解Java的接口數(shù)據(jù)校驗(yàn)的文章就介紹到這了,更多相關(guān)Java 接口數(shù)據(jù)校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java?如何通過(guò)注解實(shí)現(xiàn)接口輸出時(shí)數(shù)據(jù)脫敏
- Java實(shí)現(xiàn)調(diào)用對(duì)方http接口得到返回?cái)?shù)據(jù)
- java開(kāi)發(fā)之基于Validator接口的SpringMVC數(shù)據(jù)校驗(yàn)方式
- Java 利用DeferredResult實(shí)現(xiàn)http輪詢實(shí)時(shí)返回?cái)?shù)據(jù)接口
- Java PriorityQueue數(shù)據(jù)結(jié)構(gòu)接口原理及用法
- java讀取其他服務(wù)接口返回的json數(shù)據(jù)示例代碼
- 淺析Java 數(shù)據(jù)結(jié)構(gòu)常用接口與類
- 如何使用java制作假數(shù)據(jù)接口
相關(guān)文章
Java子類實(shí)例化總是默認(rèn)調(diào)用父類的無(wú)參構(gòu)造操作
這篇文章主要介紹了Java子類實(shí)例化總是默認(rèn)調(diào)用父類的無(wú)參構(gòu)造操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
SpringBoot整合Drools規(guī)則引擎動(dòng)態(tài)生成業(yè)務(wù)規(guī)則的實(shí)現(xiàn)
本文主要介紹了SpringBoot整合Drools規(guī)則引擎動(dòng)態(tài)生成業(yè)務(wù)規(guī)則的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
Spring?MVC?前端控制器?(DispatcherServlet)處理流程解析
DispatcherServlet是前置控制器,配置在web.xml文件中的,這篇文章主要介紹了Spring?MVC?前端控制器?(DispatcherServlet)處理流程,需要的朋友可以參考下2022-05-05
解決IDEA service層跳轉(zhuǎn)實(shí)現(xiàn)類的快捷圖標(biāo)消失問(wèn)題
這篇文章主要介紹了解決IDEA service層跳轉(zhuǎn)實(shí)現(xiàn)類的快捷圖標(biāo)消失問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
java中json和對(duì)象之間相互轉(zhuǎn)換的運(yùn)用
本文主要介紹了java中json和對(duì)象之間相互轉(zhuǎn)換的運(yùn)用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Java實(shí)現(xiàn)excel動(dòng)態(tài)列導(dǎo)出的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)excel動(dòng)態(tài)列導(dǎo)出,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
Spring操作JdbcTemplate數(shù)據(jù)庫(kù)的方法學(xué)習(xí)
這篇文章主要為大家介紹了Spring操作JdbcTemplate數(shù)據(jù)庫(kù)方法學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05

