之前也写过一篇关于Spring Validation利用的文章,不外自我觉得仍是浮于外表,本次筹算彻底搞懂Spring Validation。本文会详细介绍Spring Validation各类场景下的更佳理论及其实现原理,死磕到底!项目源码:spring-validation:https://github.com/chentianming11/spring-validation
简单利用Java API标准 (JSR303) 定义了Bean校验的尺度validation-api,但没有供给实现。hibernate validation是对那个标准的实现,并增加了校验注解如@Email、@Length等。Spring Validation是对hibernate validation的二次封拆,用于撑持spring mvc参数主动校验。接下来,我们以spring-boot项目为例,介绍Spring Validation的利用。
引入依赖若是spring-boot版本小于2.3.x,spring-boot-starter-web会主动传入hibernate-validator依赖。若是spring-boot版本大于2.3.x,则需要手动引入依赖:
org.hibernate hibernate-validator 6.0.1.Final关于web办事来说,为避免不法参数对营业形成影响,在Controller层必然要做参数校验的!大部门情况下,恳求参数分为如下两种形式:
POST、PUT恳求,利用requestBody传递参数;
GET恳求,利用requestParam/PathVariable传递参数。
下面我们简单介绍下requestBody和requestParam/PathVariable的参数校验实战!
requestBody参数校验POST、PUT恳求一般会利用requestBody传递参数,那种情况下,后端利用** DTO 对象**停止领受。只要给 DTO 对象加上@Validated注解就能实现主动参数校验。好比,有一个保留User的接口,要求userName长度是2-10,account和password字段长度是6-20。若是校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)恳求。
DTO 暗示数据传输对象(Data Transfer Object),用于办事器和客户端之间交互传输利用的。在 spring-web 项目中能够暗示用于领受恳求参数的Bean对象。
在DTO字段上声明约束注解
@Datapublic class UserDTO { private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @NotNull @Length(min = 6, max = 20) private String account; @NotNull @Length(min = 6, max = 20) private String password;}在办法参数上声明校验注解
@PostMapping("/save")public Result saveUser(@RequestBody @Validated UserDTO userDTO) { return Result.ok();}那种情况下,利用@Valid和@Validated都能够。
requestParam/PathVariable参数校验GET恳求一般会利用requestParam/PathVariable传参。若是参数比力多 (好比超越 6 个),仍是保举利用DTO对象领受。不然,保举将一个个参数平铺到办法入参中。在那种情况下,必需在Controller类上标注@Validated注解,并在入参上声明约束注解 (如@Min等)。若是校验失败,会抛出ConstraintViolationException异常。代码示例如下:
@RequestMapping("/api/user")@RestController@Validatedpublic class UserController { @GetMapping("{userId}") public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) { UserDTO userDTO = new UserDTO(); userDTO.setUserId(userId); userDTO.setAccount("11111111111111111"); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } @GetMapping("getByAccount") public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) { UserDTO userDTO = new UserDTO(); userDTO.setUserId(10000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); }}同一异常处置前面说过,若是校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在现实项目开发中,凡是会用同一异常处置来返回一个更友好的提醒。好比我们系统要求无论发送什么异常,http的形态码必需返回200,由营业码去区分系统的异常情况。
@RestControllerAdvicepublic class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder sb = new StringBuilder("校验失败:"); for (FieldError fieldError : bindingResult.getFieldErrors()) { sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); return Result.fail(BusinessCode.参数校验失败, msg); } @ExceptionHandler({ConstraintViolationException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleConstraintViolationException(ConstraintViolationException ex) { return Result.fail(BusinessCode.参数校验失败, ex.getMessage()); }}进阶利用分组校验在现实项目中,可能多个办法需要利用统一个DTO类来领受参数,而差别办法的校验规则很可能是纷歧样的。那个时候,简单地在DTO类的字段上加约束注解无法处理那个问题。因而,spring-validation撑持了分组校验的功用,专门用来处理那类问题。仍是上面的例子,好比保留User的时候,UserId是可空的,但是更新User的时候,UserId的值必需>=10000000000000000L;其它字段的校验规则在两种情况下一样。那个时候利用分组校验的代码示例如下:
约束注解上声明适用的分组信息groups
@Datapublic class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; public interface Save { } public interface Update { }}@Validated注解上指定校验分组
@PostMapping("/save")public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) { return Result.ok();}@PostMapping("/update")public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { return Result.ok();}嵌套校验前面的示例中,DTO类里面的字段都是根本数据类型和String类型。但是现实场景中,有可能某个字段也是一个对象,那种情况先,能够利用嵌套校验。好比,上面保留User信息的时候同时还带有Job信息。需要留意的是,此时DTO类的对应字段必需标识表记标帜@Valid注解。
@Datapublic class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; @NotNull(groups = {Save.class, Update.class}) @Valid private Job job; @Data public static class Job { @Min(value = 1, groups = Update.class) private Long jobId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String jobName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String position; } public interface Save { } public interface Update { }}嵌套校验能够连系分组校验一路利用。还有就是嵌套集合校验会对集合里面的每一项都停止校验,例如List字段会对那个list里面的每一个Job对象都停止校验。
集合校验若是恳求体间接传递了json数组给后台,并希望对数组中的每一项都停止参数校验。此时,若是我们间接利用java.util.Collection下的list或者set来领受数据,参数校验其实不会生效!我们能够利用自定义list集合来领受参数:
包拆List类型,并声明@Valid注解
public class ValidationListimplements List{ @Delegate @Valid public Listlist = new ArrayList<>(); @Override public String toString() { return list.toString(); }}@Delegate注解受lombok版本限造,1.18.6以上版本可撑持。若是校验欠亨过,会抛出NotReadablePropertyException,同样能够利用同一异常停止处置。
好比,我们需要一次性保留多个User对象,Controller层的办法能够那么写:
@PostMapping("/saveList")public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationListuserList){ return Result.ok();}自定义校验营业需求老是比框架供给的那些简单校验要复杂的多,我们能够自定义校验来满足我们的需求。自定义spring validation十分简单,假设我们自定义加密id(由数字或者a-f的字母构成,32-256长度)校验,次要分为两步:
自定义约束注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy = {EncryptIdValidator.class})public @interface EncryptId { String message() default "加密id格局错误"; Class[] groups() default {}; Class[] payload() default {};}实现ConstraintValidator接口编写约束校验器
public class EncryptIdValidator implements ConstraintValidator{ private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value != null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; }}如许我们就能够利用@EncryptId停止参数校验了!
编程式校验上面的示例都是基于注解来实现主动校验的,在某些情况下,我们可能希望以编程体例挪用验证。那个时候能够注入javax.validation.Validator对象,然后再挪用其api。
@Autowiredprivate javax.validation.Validator globalValidator;@PostMapping("/saveWithCodingValidate")public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set现实上,不论是requestBody参数校验仍是办法级此外校验,最末都是挪用Hibernate Validator施行校验,Spring Validation只是做了一层封拆。
发表评论