javascript
@valid 校验_SpringBoot数据校验与优雅处理详解
本篇要點(diǎn)
JDK1.8、SpringBoot2.3.4release- 說明后端參數(shù)校驗(yàn)的必要性。
- 介紹如何使用validator進(jìn)行參數(shù)校驗(yàn)。
- 介紹@Valid和@Validated的區(qū)別。
- 介紹如何自定義約束注解。
- 關(guān)于Bean Validation的前世今生
后端參數(shù)校驗(yàn)的必要性
在開發(fā)中,從表現(xiàn)層到持久化層,數(shù)據(jù)校驗(yàn)都是一項(xiàng)邏輯差不多,但容易出錯(cuò)的任務(wù),
前端框架往往會采取一些檢查參數(shù)的手段,比如校驗(yàn)并提示信息,那么,既然前端已經(jīng)存在校驗(yàn)手段,后端的校驗(yàn)是否還有必要,是否多余了呢?
并不是,正常情況下,參數(shù)確實(shí)會經(jīng)過前端校驗(yàn)傳向后端,但如果后端不做校驗(yàn),一旦通過特殊手段越過前端的檢測,系統(tǒng)就會出現(xiàn)安全漏洞。
不使用Validator的參數(shù)處理邏輯
既然是參數(shù)校驗(yàn),很簡單呀,用幾個(gè)if/else直接搞定:
@PostMapping("/form")public String form(@RequestBody Person person) {if (person.getName() == null) {return "姓名不能為null";}if (person.getName().length() < 6 || person.getName().length() > 12) {return "姓名長度必須在6 - 12之間";}if (person.getAge() == null) {return "年齡不能為null";}if (person.getAge() < 20) {return "年齡最小需要20";}// service ..return "注冊成功!";}寫法干脆,但if/else太多,過于臃腫,更何況這只是區(qū)區(qū)一個(gè)接口的兩個(gè)參數(shù)而已,要是需要更多參數(shù)校驗(yàn),甚至更多方法都需要這要的校驗(yàn),這代碼量可想而知。于是,這種做法顯然是不可取的,我們可以利用下面這種更加優(yōu)雅的參數(shù)處理方式。
Validator框架提供的便利
Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone.如果依照下圖的架構(gòu),對每個(gè)層級都進(jìn)行類似的校驗(yàn),未免過于冗雜。
Jakarta Bean Validation 2.0 - defines a metadata model and API for entity and method validation. The default metadata source are annotations, with the ability to override and extend the meta-data through the use of XML.The API is not tied to a specific application tier nor programming model. It is specifically not tied to either web or persistence tier, and is available for both server-side application programming, as well as rich client Swing application developers.
Jakarta Bean Validation2.0定義了一個(gè)元數(shù)據(jù)模型,為實(shí)體和方法提供了數(shù)據(jù)驗(yàn)證的API,默認(rèn)將注解作為源,可以通過XML擴(kuò)展源。
SpringBoot自動配置ValidationAutoConfiguration
Hibernate Validator是Jakarta Bean Validation的參考實(shí)現(xiàn)。
在SpringBoot中,只要類路徑上存在JSR-303的實(shí)現(xiàn),如Hibernate Validator,就會自動開啟Bean Validation驗(yàn)證功能,這里我們只要引入spring-boot-starter-validation的依賴,就能完成所需。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>目的其實(shí)是為了引入如下依賴:
<!-- Unified EL 獲取動態(tài)表達(dá)式--><dependency><groupId>org.glassfish</groupId><artifactId>jakarta.el</artifactId><version>3.0.3</version><scope>compile</scope></dependency><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.1.5.Final</version><scope>compile</scope></dependency>SpringBoot對BeanValidation的支持的自動裝配定義在org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration類中,提供了默認(rèn)的LocalValidatorFactoryBean和支持方法級別的攔截器MethodValidationPostProcessor。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") @Import(PrimaryDefaultValidatorPostProcessor.class) public class ValidationAutoConfiguration {@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@ConditionalOnMissingBean(Validator.class)public static LocalValidatorFactoryBean defaultValidator() {//ValidatorFactoryLocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();factoryBean.setMessageInterpolator(interpolatorFactory.getObject());return factoryBean;}// 支持Aop,MethodValidationInterceptor方法級別的攔截器@Bean@ConditionalOnMissingBeanpublic static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,@Lazy Validator validator) {MethodValidationPostProcessor processor = new MethodValidationPostProcessor();boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);processor.setProxyTargetClass(proxyTargetClass);// factory.getValidator(); 通過factoryBean獲取了Validator實(shí)例,并設(shè)置processor.setValidator(validator);return processor;}}Validator+BindingResult優(yōu)雅處理
默認(rèn)已經(jīng)引入相關(guān)依賴。為實(shí)體類定義約束注解
/*** 實(shí)體類字段加上javax.validation.constraints定義的注解* @author Summerday*/@Data @ToString public class Person {private Integer id;@NotNull@Size(min = 6,max = 12)private String name;@NotNull@Min(20)private Integer age; }使用@Valid或@Validated注解
@Valid和@Validated在Controller層做方法參數(shù)校驗(yàn)時(shí)功能相近,具體區(qū)別可以往后面看。
@RestController public class ValidateController {@PostMapping("/person")public Map<String, Object> validatePerson(@Validated @RequestBody Person person, BindingResult result) {Map<String, Object> map = new HashMap<>();// 如果有參數(shù)校驗(yàn)失敗,會將錯(cuò)誤信息封裝成對象組裝在BindingResult里if (result.hasErrors()) {List<String> res = new ArrayList<>();result.getFieldErrors().forEach(error -> {String field = error.getField();Object value = error.getRejectedValue();String msg = error.getDefaultMessage();res.add(String.format("錯(cuò)誤字段 -> %s 錯(cuò)誤值 -> %s 原因 -> %s", field, value, msg));});map.put("msg", res);return map;}map.put("msg", "success");System.out.println(person);return map;} }發(fā)送Post請求,偽造不合法數(shù)據(jù)
這里使用IDEA提供的HTTP Client工具發(fā)送請求。
POST http://localhost:8081/person Content-Type: application/json{"name": "hyh","age": 10 }響應(yīng)信息如下:
HTTP/1.1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Sat, 14 Nov 2020 15:58:17 GMT Keep-Alive: timeout=60 Connection: keep-alive{"msg": ["錯(cuò)誤字段 -> name 錯(cuò)誤值 -> hyh 原因 -> 個(gè)數(shù)必須在6和12之間","錯(cuò)誤字段 -> age 錯(cuò)誤值 -> 10 原因 -> 最小不能小于20"] }Response code: 200; Time: 393ms; Content length: 92 bytesValidator + 全局異常處理
在接口方法中利用BindingResult處理校驗(yàn)數(shù)據(jù)過程中的信息是一個(gè)可行方案,但在接口眾多的情況下,就顯得有些冗余,我們可以利用全局異常處理,捕捉拋出的MethodArgumentNotValidException異常,并進(jìn)行相應(yīng)的處理。
定義全局異常處理
@RestControllerAdvice public class GlobalExceptionHandler {/*** If the bean validation is failed, it will trigger a MethodArgumentNotValidException.*/@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpStatus status) {BindingResult result = ex.getBindingResult();Map<String, Object> map = new HashMap<>();List<String> list = new LinkedList<>();result.getFieldErrors().forEach(error -> {String field = error.getField();Object value = error.getRejectedValue();String msg = error.getDefaultMessage();list.add(String.format("錯(cuò)誤字段 -> %s 錯(cuò)誤值 -> %s 原因 -> %s", field, value, msg));});map.put("msg", list);return new ResponseEntity<>(map, status);} }定義接口
@RestController public class ValidateController {@PostMapping("/person")public Map<String, Object> validatePerson(@Valid @RequestBody Person person) {Map<String, Object> map = new HashMap<>();map.put("msg", "success");System.out.println(person);return map;} }@Validated精確校驗(yàn)到參數(shù)字段
有時(shí)候,我們只想校驗(yàn)?zāi)硞€(gè)參數(shù)字段,并不想校驗(yàn)整個(gè)pojo對象,我們可以利用@Validated精確校驗(yàn)到某個(gè)字段。
定義接口
@RestController @Validated public class OnlyParamsController {@GetMapping("/{id}/{name}")public String test(@PathVariable("id") @Min(1) Long id,@PathVariable("name") @Size(min = 5, max = 10) String name) {return "success";} }發(fā)送GET請求,偽造不合法信息
GET http://localhost:8081/0/hyh Content-Type: application/json未作任何處理,響應(yīng)結(jié)果如下:
{"timestamp": "2020-11-15T15:23:29.734+00:00","status": 500,"error": "Internal Server Error","trace": "javax.validation.ConstraintViolationException: test.id: 最小不能小于1, test.name: 個(gè)數(shù)必須在5和10之間...省略","message": "test.id: 最小不能小于1, test.name: 個(gè)數(shù)必須在5和10之間","path": "/0/hyh" }可以看到,校驗(yàn)已經(jīng)生效,但狀態(tài)和響應(yīng)錯(cuò)誤信息不太正確,我們可以通過捕獲ConstraintViolationException修改狀態(tài)。
捕獲異常,處理結(jié)果
@ControllerAdvice public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {private static final Logger log = LoggerFactory.getLogger(CustomGlobalExceptionHandler.class);/*** If the @Validated is failed, it will trigger a ConstraintViolationException*/@ExceptionHandler(ConstraintViolationException.class)public void constraintViolationException(ConstraintViolationException ex, HttpServletResponse response) throws IOException {ex.getConstraintViolations().forEach(x -> {String message = x.getMessage();Path propertyPath = x.getPropertyPath();Object invalidValue = x.getInvalidValue();log.error("錯(cuò)誤字段 -> {} 錯(cuò)誤值 -> {} 原因 -> {}", propertyPath, invalidValue, message);});response.sendError(HttpStatus.BAD_REQUEST.value());} }@Validated和@Valid的不同
參考:@Validated和@Valid的區(qū)別?教你使用它完成Controller參數(shù)校驗(yàn)(含級聯(lián)屬性校驗(yàn))以及原理分析【享學(xué)Spring】
- @Valid是標(biāo)準(zhǔn)JSR-303規(guī)范的標(biāo)記型注解,用來標(biāo)記驗(yàn)證屬性和方法返回值,進(jìn)行級聯(lián)和遞歸校驗(yàn)。
- @Validated:是Spring提供的注解,是標(biāo)準(zhǔn)JSR-303的一個(gè)變種(補(bǔ)充),提供了一個(gè)分組功能,可以在入?yún)Ⅱ?yàn)證時(shí),根據(jù)不同的分組采用不同的驗(yàn)證機(jī)制。
- 在Controller中校驗(yàn)方法參數(shù)時(shí),使用@Valid和@Validated并無特殊差異(若不需要分組校驗(yàn)的話)。
- @Validated注解可以用于類級別,用于支持Spring進(jìn)行方法級別的參數(shù)校驗(yàn)。@Valid可以用在屬性級別約束,用來表示級聯(lián)校驗(yàn)。
- @Validated只能用在類、方法和參數(shù)上,而@Valid可用于方法、字段、構(gòu)造器和參數(shù)上。
如何自定義注解
Jakarta Bean Validation API定義了一套標(biāo)準(zhǔn)約束注解,如@NotNull,@Size等,但是這些內(nèi)置的約束注解難免會不能滿足我們的需求,這時(shí)我們就可以自定義注解,創(chuàng)建自定義注解需要三步:
創(chuàng)建一個(gè)constraint annotation
/*** 自定義注解* @author Summerday*/@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) @Retention(RUNTIME) @Constraint(validatedBy = CheckCaseValidator.class) //需要定義CheckCaseValidator @Documented @Repeatable(CheckCase.List.class) public @interface CheckCase {String message() default "{CheckCase.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};CaseMode value();@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})@Retention(RUNTIME)@Documented@interface List {CheckCase[] value();} }實(shí)現(xiàn)一個(gè)validator
/*** 實(shí)現(xiàn)ConstraintValidator** @author Summerday*/ public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {private CaseMode caseMode;/*** 初始化獲取注解中的值*/@Overridepublic void initialize(CheckCase constraintAnnotation) {this.caseMode = constraintAnnotation.value();}/*** 校驗(yàn)*/@Overridepublic boolean isValid(String object, ConstraintValidatorContext constraintContext) {if (object == null) {return true;}boolean isValid;if (caseMode == CaseMode.UPPER) {isValid = object.equals(object.toUpperCase());} else {isValid = object.equals(object.toLowerCase());}if (!isValid) {// 如果定義了message值,就用定義的,沒有則去// ValidationMessages.properties中找CheckCase.message的值if(constraintContext.getDefaultConstraintMessageTemplate().isEmpty()){constraintContext.disableDefaultConstraintViolation();constraintContext.buildConstraintViolationWithTemplate("{CheckCase.message}").addConstraintViolation();}}return isValid;} }定義一個(gè)default error message
在ValidationMessages.properties文件中定義:
CheckCase.message=Case mode must be {value}.這樣,自定義的注解就完成了,如果感興趣可以自行測試一下,在某個(gè)字段上加上注解:@CheckCase(value = CaseMode.UPPER)。
源碼下載
總結(jié)
以上是生活随笔為你收集整理的@valid 校验_SpringBoot数据校验与优雅处理详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bootstrap tab 模拟点击_【
- 下一篇: 小旋风泛目录_小旋风泛目录v5.4_使用