一、参数校验的注解
Java中参数校验的注解来自三方面,分别是
- javax.validation:validation-api,对应包javax.validation.constraints
- org.springframework:spring-context,对应包org.springframework.validation
- org.hibernate:hibernate-validator,对应包org.hibernate.validator.constraints
1.validation-api中的注解
注解 | 说明 | 适用类型 |
---|---|---|
@AssertFalse | 限制必须是false | boolean Boolean:not null时才校验 |
@AssertTrue | 限制必须是true | boolean Boolean:not null时才校验 |
@Max(value) | 限制必须为一个小于等于value指定值的整数,value是long型 | byte/short/int/long/float/double及其对应的包装类;包装类对象not null时才校验 |
@Min(value) | 限制必须为一个大于等于value指定值的整数,value是long型 | byte/short/int/long/float/double及其对应的包装类;包装类对象not null时才校验 |
@DecimalMax(value) | 限制必须小于等于value指定的值,value是字符串类型 | byte/short/int/long/float/double及其对应的包装类;包装类对象not null时才校验 |
@DecimalMin(value) | 限制必须大于等于value指定的值,value是字符串类型 | byte/short/int/long/float/double及其对应的包装类;包装类对象not null时才校验 |
@Digits(integer, fraction) | 限制必须为一个小数(其实整数也可以),且整数部分的位数不能超过integer,小数部分的位数不能超过fraction。integer和fraction可以是0。 | byte/short/int/long/float/double及其对应的包装类;包装类对象必须not null时才校验 |
@Null | 限制只能为null | 任意对象类型(比如基本数据类型对应的包装类、String、枚举类、自定义类等);不能是8种基本数据类型 |
@NotNull | 限制必须不为null | 任意类型(包括8种基本数据类型及其包装类、String、枚举类、自定义类等);但是对于基本数据类型,没有意义 |
@Size(min, max) | 限制Collection类型或String的长度必须在min到max之间,包含min和max |
|
@Pattern(regexp) | 限制必须符合regexp指定的正则表达式 | String |
@Future | 限制必须是一个将来的日期 | Date/Calendar |
@Past | 限制必须是一个过去的日期 | Date/Calendar |
@Valid | 校验任何非原子类型,标记一个对象,表示校验对象中被注解标记的对象(不支持分组功能) | 需要校验成员变量的对象,比如@ModelAttribute标记的接口入参 |
2. hibernat
注解 | 说明 | 适用类型 |
---|---|---|
@Length(min,max) | 限制String类型长度必须在min和max之间,包含min和max | String, not null时才校验 |
@NotBlank | 验证注解的元素值不是空白(即不是null,且包含非空白字符) | String |
@NotEmpty | 验证注解的元素值不为null且不为空(即字符串非null且长度不为0、集合类型大小不为0) |
|
@Range(min,max) | 验证注解的元素值在最小值和最大值之间 |
|
@Email(regexp) | 验证注解的字符串符合邮箱的正则表达式,可以使用regexp自定义正则表达式 | String |
@CreditCardNumber | 验证银行借记卡、信用卡的卡号 | String(不能包含空格等特殊字符) |
3.spring-context中的注解
注解 | 说明 | 适用类型 |
---|---|---|
@Validated | 校验非原子类型对象,或启用类中原子类型参数的校验(支持分组校验;只校验包含指定分组的注解参数) |
|
二、注解的启用
- 方法中对象参数中成员变量校验注解的生效条件
- @ModelAttribute标记的查询条件类参数,需要同时用@Valid或@Validated标记,类中的注解校验才会生效
- @RequestBody标记的请求体对象参数,需要同时用@Valid或@Validated标记,类中的注解校验才会生效
- @Valid或@Validated标记在方法或方法所属类上无效
- 方法中原子类型/集合类型参数校验注解的生效条件
- @Validated标记在方法所属类上
- 类A中成员是另一个类B,类B中成员校验注解生效条件
- 类A中的成员(即类B对象)使用@Valid修饰
- 按照分组启用
- 在注解中使用groups添加启用注解的分组
- 在@Validated中指定启用的分组
三、注解使用的几个注意事项
- 注意选传变量的默认值
- 选传变量没有传值时,校验会使用变量的默认值
- 如果有指定的默认值,会使用指定的默认值进行校验
- 如果没有指定默认值,那么使用类型默认值进行校验
- 注意默认值能否通过校验
- 选传变量使用默认值时,注意实例化到db时是否为null及对null值的处理(StringUtils.defaultString)
- 注意基本数据类型与对应包装类型之间,类型默认值的区别
- 注意非请求体入参中的枚举类型
- 枚举类型不能直接接受传参绑定
- 使用String或int类型先接受绑定值,再转成枚举变量
- 配合统一异常处理
/**
* 与@Valid/@Validated @ModelAttribute注解配合使用,处理查询条件对象类参数中成员变量校验没有通过的异常
* @param e
* @return
*/
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.OK)
public Response handleBindException(BindException e) {
LOG.warn("GlobalExceptionHandler BindException", e);
List<ObjectError> allErrors = e.getAllErrors();
return Response.fail(ErrorCodeEnum.INVALID_PARAMS.getCode(), allErrors.get(0).getDefaultMessage());
}
/**
* 与@Valid/@Validated @RequestBody配合使用,处理请求体对象类参数中成员变量校验没有通过的异常
* @param e
* @param request
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public Response handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request){
LOG.warn("handleMethodArgumentNotValidException={}", request.getRequestURI(), e);
BindingResult bindingResult = e.getBindingResult();
if(bindingResult.hasErrors()){
List<ObjectError> errors = bindingResult.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
return Response.fail(ErrorCodeEnum.INVALID_PARAMS.getCode(), fieldError.getDefaultMessage());
}
}
return Response.fail(ErrorCodeEnum.INVALID_PARAMS.getCode(), ErrorCodeEnum.INVALID_PARAMS.toString());
}
/**
* 当@Validated修饰类时,处理接口原子类型参数没有通过校验的异常
* @param e
* @return
*/
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
public Response handleConstraintViolationException(ConstraintViolationException e) {
LOG.warn("GlobalExceptionHandler ConstraintViolationException", e);
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator();
ConstraintViolation<?> next = iterator.next();
return Response.fail(ErrorCodeEnum.INVALID_PARAMS.getCode(), next.getMessage());
}
四、具体场景示例
- 用户的id
- 创建时不需要传参
- 编辑时必须传参
- 数据库中,该字段属性是bigint primary not null auto_increment comment 'id';
- 用户的name
- 创建时name必传,不能是空或空白字符
- 编辑时name不可修改,不需要传参
- 数据库中,该字段属性是varchar(30) not null default '' comment '姓名';
- 用户的电话phone
- 创建时phone必传,不能是空或空白字符
- 编辑时phone可以修改,必传
- 数据库中,该字段属性是char(11) not null default '' comment '手机';
- 用户的地址address
- 无论创建、编辑,都是选传
- 数据库中,该字段属性是varchar(30) not null default '' comment '地址';
- 用户的出生年份year
- 无论创建、编辑,都是选传
- 数据库中,该字段属性是int not null default 0 comment '出生年份';
- 假设用户出生年份在1970-2000年之间
- 用户的性别gender
- 无论创建、编辑,都是选传
- 数据库中,该字段属性是tinyint not null default 0 comment '性别,1:男,2:女,0:未知';
Person代码示例
public class Person{
@Min(value = 1, message = "缺少id", groups = {Update.class})
private int id;
@NotBlank(message = "缺少姓名", groups = {Insert.class})
@Length(min = 2, max = 30, message = "姓名长度必须在2-30之间", groups = {Insert.class})
private String name;
@NotBlank(message = "缺少手机号", groups = {Insert.class, Update.class})
@Pattern(regexp = "^1[0-9]{10}$", message = "手机号不合法", groups = {Insert.class, Update.class})
private String phone;
@Length(min = 3, max = 30, message = "地址长度必须在3-30之间", groups = {Insert.class, Update.class})
private String address;
@Range(min = 1970, max = 2000, message = "出生年份必须在1970-2000之间", groups = {Insert.class, Update.class})
private Integer year;
@ApiModelProperty(value = "性别")
private GenderEnum genderEnum;
}
Person接口示例代码如下
@RestController
@RequestMapping("/person")
@Validated
public class PersonController extends BaseController {
@Autowired
private PersonLogic personLogic;
@PostMapping("/createPerson")
public Response<Void> createPerson(@Validated(Insert.class) @RequestBody Person person){
personLogic.createPerson(person);
return Response.ok();
}
@PostMapping("/editPerson")
public Response<Void> editPerson(@Validated(Update.class) @RequestBody Person person){
personLogic.editPerson(person);
return Response.ok();
}
@GetMapping("/info")
public Response<Person> getPersonById(@Min(value = 1, message = "缺少id") @RequestParam int id){
return Response.ok().setData(personLogic.getPersonById(id));
}
@GetMapping("/listPersons")
public Response<List<Person>> listPersons(@Valid @ModelAttribute PersonQueryParam queryParam){
if(StringUtils.isNotBlank(queryParam.getGender())){
queryParam.setGenderEnum(GenderEnum.findByString(queryParam.getGender()));
}
return Response.ok().setData(personLogic.listPersons(queryParam));
}
}
查询条件类代码示例如下
@Data
public class PersonQueryParam {
@Length(min = 1, max = 1, message = "性别错误")
@ApiModelProperty(value = "性别,值域:[男,女]")
private String gender;
@ApiModelProperty(value = "性别", hidden = true)
private GenderEnum genderEnum;
@Length(min = 1, max = 30, message = "模糊查询姓名长度必须在1-30之间")
private String namePattern;
}
以上