目前在项目中,参数校验的工做都在前端完成,然后端接口只处理业务逻辑,可是这种方式不太合理,绕过页面直接进行http请求,会有系统异常以及脏数据的风险,因此推荐使用Bean Validation 基于 JSR 303 - Bean Validation参数校验框架在后端接口作参数校验,格式化校验,以及参数可选范围的校验,这样既能规避大部分因参数缺失而产生的系统异常,也能在接口联调阶段,提升联调效率,减小先后端同窗在联调时排查问题的时间html
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下java
约束 | 限制 |
---|---|
@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 的最好实现,目前规范已升级到 JSRgit
约束 | 限制 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
Bean Validation 是JDK 1.6 +后内置的,包名为javax.validation.constraints正则表达式
Hibernate Validator 则须要引入jar包,包名为org.hibernate.validator.constraintsspring
POM.xmljson
<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的比较,根据实际需求需求选择bash
@Valid : 没有分组功能,能够用在方法、构造函数、方法参数和成员属性(field)上,若是一个待验证的pojo类,其中还包含了待验证的对象,须要在待验证对象上注解@valid,才能验证待验证对象中的成员属性app
@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
}
复制代码
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
}
复制代码
参考文档