目前在新乐才以及餐学院的项目中,参数校验的工作都在前端完成,而后端接口只处理业务逻辑,但是这种方式不太合理,绕过页面直接进行http请求,会有系统异常以及脏数据的风险,所以推荐使用Bean Validation 基于 JSR 303 - Bean Validation 参数校验框架在后端接口做参数校验,格式化校验,以及参数可选范围的校验,这样既能规避大部分因参数缺失而产生的系统异常,也能在接口联调阶段,提高联调效率,减少前后端同学在联调时排查问题的时间
Hibernate Validator是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,目前已升级到 Bean Validation 2.0 / JSR - 380 ,除此之外还有一些附加的 constraint。 该Hibernate不是ORM的Hibernate
举例Bean Validation 中的 constraint (约束,限制),Bean Validation 的注解在javax.validation.constraints下
约束 | 限制 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 附加的 constraint / Hibernate Validator是JSR - 303 的最好实现,目前规范已升级到 JSR
约束 | 限制 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
Bean Validation 是JDK 1.6 +后内置的,包名为javax.validation.constraints
Hibernate Validator 则需要引入jar包,包名为org.hibernate.validator.constraints
POM.xml
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency> 复制代码
实体类
import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Past; import java.util.Date; public class ValidationDemo { private String id; @Length(min = 2, max = 6, message = "用户名长度要求在{min}-{max}之间") @NotNull(message = "用户名不可为空") private String userName; @Email(message = "邮箱格式错误") private String email; @Past(message = "出生日期错误") private Date birthDay; @Min(value = 18, message = "年龄错误") @Max(value = 80, message = "年龄错误") private Integer age; @Range(min = 0, max = 1, message = "性别选择错误") private Integer sex; } 复制代码
关于@Valid和Validated的比较,根据实际需求需求选择
@Valid : 没有分组功能,可以用在方法、构造函数、方法参数和成员属性(field)上,如果一个待验证的pojo类,其中还包含了待验证的对象,需要在待验证对象上注解@valid,才能验证待验证对象中的成员属性
@Validated :提供分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,用在类型、方法和方法参数上。但不能用于成员属性(field)。
Controller
-- @Valid 表示对该实体进行校验 -- BindingResult 则保存对参数的校验结果 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Valid @RequestBody ValidationDemo demo, BindingResult result) { JsonResult jsonResult = new JsonResult(); if (result.hasErrors()) { result.getAllErrors().forEach(err -> { jsonResult.setCode(ApiConstants.JsonResult.FAIL); jsonResult.setMsg(err.getDefaultMessage()); }); } return jsonResult; } 复制代码
RequestBody
{ "age": 19, "birthDay": "2019-04-14T09:05:39.604Z", "email": "string", "id": "string", "sex": 0, "userName": "string" } 复制代码
Response
{ "code": 1, "msg": "邮箱格式错误", "total": 0, "totalpage": 0 } 复制代码
由此可见,参数的校验已经生效,因为email不符合@Email的校验规则,具体校验规则可以查看@Email的实现EmailValidator.java
RequestBody
{ "age": 19, "birthDay": "2019-04-14T09:05:39.604Z", "email": "string", "id": "string", "sex": 0, "userName": "" } 复制代码
Response
{ "code": 1, "msg": "用户名长度要求在2-6之间", "total": 0, "totalpage": 0 } 复制代码
Hibernate Validator 通过EL表达式获取到了在@length中定义的min以及max属性的值
在上面的Controller中,需要在在接口参数中,增加一个BindingResult来接收校验的结果,每一个BindingResult与@Valid是一一对应的,如果有多个@Valid,那么需要对个BindResult来保存校验结果
在 ResponseEntityExceptionHandler (Line 162) 中,如果验证出现异常的时候是抛出了MethodArgumentNotValidException
MethodArgumentNotValidException 描述: Exception to be thrown when validation on an argument annotated with {@code @Valid} fails. 当使用@Valid注解的参数验证失败是抛出异常 复制代码
所以在BaseController中对MethodArgumentNotValidException进行处理
Controller
-- 对接口进行简化,通过异常捕获的方式对校验结果返回给前端 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Valid @RequestBody ValidationDemo demo) { return null; } 复制代码
BaseController
if (e instanceof MethodArgumentNotValidException) { res.setCode(ApiConstants.JsonResult.FAIL); res.setMsg(JSONArray.toJSONString(((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()))); } 复制代码
Response
{ "code": 1, "msg": "[/"年龄错误/",/"邮箱格式错误/"]", "total": 0, "totalpage": 0 } 复制代码
在实际使用中,有可能我们针对一个属性,有多个校验规则,这时候就要使用到分组校验了
改造实体
public class ValidationDemo { private String id; @Length(min = 2, max = 6, message = "用户名长度要求在{min}-{max}之间") @NotNull(message = "用户名不可为空") private String userName; // 表示分组为Adult时使用该校验规则 @Email(message = "邮箱格式错误") @NotBlank(message = "邮箱不可为空", groups = {ValidationDemo.Adult.class}) private String email; @Past(message = "出生日期错误") private Date birthDay; @Min(value = 18, message = "年龄错误") @Max(value = 80, message = "年龄错误") private Integer age; @Range(min = 0, max = 1, message = "性别选择错误") private Integer sex; // 添加两个分组 public interface Adult { } public interface Minor { } } 复制代码
测试一下
// 这里将分组设置为Minor,目的是不校验邮箱字段 @RequestMapping(value = "validation", method = RequestMethod.POST) public JsonResult validation(@Validated({ValidationDemo.Adult.class}) @RequestBody ValidationDemo demo) { return null; } RequestBody: { "age": 0, "birthDay": "2019-04-14T10:39:08.501Z", "email": "", "id": "string", "sex": 0, "userName": "string" } Response: { "code": 1, "msg": "[/"邮箱不可为空/"]", "total": 0, "totalpage": 0 } 复制代码
如果是接口使用Minor分组呢?
RequestBody: { "age": 0, "birthDay": "2019-04-14T10:39:08.501Z", "email": "", "id": "string", "sex": 0, "userName": "string" } Response: { "code": 0, "data": [ {} ], "extra": "string", "msg": "string", "result": {}, "total": 0, "totalpage": 0 } 复制代码
例如新建一个自定义日期格式的校验
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Constraint(validatedBy = {DateFormatByPatternValidator.class}) public @interface DateFormatByPattern { String pattern() default "yyyy-MM-dd HH:mm"; //默认错误消息 String message() default "日期格式错误"; //分组 Class<?>[] groups() default {}; //负载 Class<? extends Payload>[] payload() default {}; } 复制代码
同时新建一个对应的校验器
public class DateFormatByPatternValidator implements ConstraintValidator<DateFormatByPattern, String> { private DateFormatByPattern dateFormatByPattern; @Override public void initialize(DateFormatByPattern constraintAnnotation) { dateFormatByPattern = constraintAnnotation; } @Override public boolean isValid(String value, ConstraintValidatorContext context) { //假如参数为空的话,返回true,如果要对参数值进行非空校验的话,通过@NotNull来校验,这样与日期格式校验解耦 if (StringUtils.isNotBlank(value)) { String pattern = dateFormatByPattern.pattern(); SimpleDateFormat dateFormat = new SimpleDateFormat(pattern); try { dateFormat.parse(value); } catch (ParseException e) { return false; } } return true; } } 复制代码
改造实体
//使用自定义规则校验前端参数 @DateFormatByPattern(pattern = "yyyy-MM-dd") //因为同时用到了分组校验,所以在stringDate上添加@Valid,使校验生效 @Valid private String stringDate; 复制代码
测试一下
RequestBody: { "age": 0, "birthDay": "2019-04-15T08:23:21.683Z", "email": "", "id": "string", "sex": 0, "stringDate": "string", "userName": "string" } Response: { "code": 1, "msg": "[/"日期格式错误/",/"邮箱不可为空/",/"年龄错误/"]", "total": 0, "totalpage": 0 } 复制代码