本文主要包括:基本注解使用及说明,全局异常捕捉,自定义注解的实现,@Validated与@Valid的简单对比及不同实现。
使用 Spring Boot 程序的话只需要spring-boot-starter-web 就够了,它的子依赖包含了我们所需要的东西。除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
示例
新建参数类
@Data
public class ValidationModel {
/**
* 主键
*/
@NotNull(message = "主键不能为空")
private Long id;
/**
* 名称
*/
@NotBlank(message = "名称不能为空")
private String name;
/**
* 邮箱
*/
@NotBlank(message = "email 不能为空")
@Email(message = "email 格式不正确")
private String email;
/**
* 年龄
*/
@Range(min = 18, max = 60, message = "年龄必须在 {min} 至 {max} 之间")
private String age;
}
-
文档地址: https://docs.oracle.com/javaee/7/api/javax/validation/constraints/package-frame.html
-
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation
JSR提供的校验注解
注解 | 说明 |
@AssertFalse | 被注释的元素只能为false |
@AssertTrue |
|
@DecimalMax | 被注释的元素必须小于或等于{value} |
@DecimalMin | 被注释的元素必须大于或等于{value} |
@Digits | 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内) |
@Email | 被注释的元素不是一个合法的电子邮件地址 |
@Future | 被注释的元素需要是一个将来的时间 |
@FutureOrPresent | 被注释的元素需要是一个将来或现在的时间 |
@Max | 被注释的元素最大不能超过{value} |
@Min | 被注释的元素最小不能小于{value} |
@Negative | 被注释的元素必须是负数 |
@NegativeOrZero | 被注释的元素必须是负数或零 |
@NotBlank | 被注释的元素不能为空 |
@NotEmpty | 被注释的元素不能为空 |
@NotNull | 被注释的元素不能为null |
@Null | 被注释的元素必须为null |
@Past | 被注释的元素需要是一个过去的时间 |
@PastOrPresent | 被注释的元素需要是一个过去或现在的时间 |
@Pattern | 被注释的元素需要匹配正则表达式"{regexp}" |
@Positive | 被注释的元素必须是正数 |
@PositiveOrZero | 被注释的元素必须是正数或零 |
@Size | 被注释的元素个数必须在{min}和{max}之间 |
Hibernate Validator提供的校验注解
@CreditCardNumber | 被注释的元素不合法的信用卡号码 |
@Currency | 被注释的元素不合法的货币 (必须是{value}其中之一) |
@EAN | 被注释的元素不合法的{type}条形码 |
@Email | 被注释的元素不是一个合法的电子邮件地址 (已过期) |
@Length | 被注释的元素长度需要在{min}和{max}之间 |
@CodePointLength | 被注释的元素长度需要在{min}和{max}之间 |
@LuhnCheck | 被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配 |
@Mod10Check | 被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配 |
@Mod11Check | 被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配 |
@ModCheck | 被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配 (已过期) |
@NotBlank | 被注释的元素不能为空 (已过期) |
@NotEmpty | 被注释的元素不能为空 (已过期) |
@ParametersScriptAssert | 被注释的元素执行脚本表达式"{script}"没有返回期望结果 |
@Range | 被注释的元素需要在{min}和{max}之间 |
@SafeHtml | 被注释的元素可能有不安全的HTML内容 |
@ScriptAssert | 被注释的元素执行脚本表达式"{script}"没有返回期望结果 |
@URL | 被注释的元素需要是一个合法的URL |
@DurationMax | 被注释的元素必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'} |
@DurationMin | 被注释的元素必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'} |
在Controller中校验数据
@Slf4j
@RestController
@RequestMapping(value = "/index")
public class IndexController {
@RequestMapping(value = "/save", method = RequestMethod.POST)
public R save(@Validated ValidationModel validationModel) {
log.info("接收到参数为 [{}]", validationModel);
return R.success(validationModel);
}
}
简单封装一个返回参数类
@Data
public class R<T> implements Serializable {
private Integer code;
private String message;
private T data;
public R() {
this.code = 200;
this.message = "SUCCESS";
}
public static <T> R<T> success(T data) {
R r = new R();
r.setData(data);
return r;
}
public static R fail() {
R r = new R();
r.setCode(500);
r.setMessage("FAIL");
return r;
}
public static R fail(String message) {
R r = new R();
r.setCode(500);
r.setMessage(message);
return r;
}
}
postman
相应参数
{
"timestamp": "2019-09-14T15:17:25.919+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Range.validationModel.age",
"Range.age",
"Range.java.lang.String",
"Range"
],
"arguments": [
{
"codes": [
"validationModel.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
},
60,
18
],
"defaultMessage": "年龄必须在 18 至 60 之间",
"objectName": "validationModel",
"field": "age",
"rejectedValue": "10",
"bindingFailure": false,
"code": "Range"
}
],
"message": "Validation failed for object='validationModel'. Error count: 1",
"path": "/index/save"
}
实现全局统一返回参数
Spring MVC 在 org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler类已经进行了异常拦截,我们只需要找到相应的处理方法,重写就可以了
重写ResponseEntityExceptionHandler类的handleBindException
@Slf4j
@RestControllerAdvice
public class ValidationInterceptor extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(getError(ex.getBindingResult().getAllErrors()) , status);
}
/**
* 解决 JSON 请求统一返回参数
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(getError(ex.getBindingResult().getAllErrors()) , status);
}
private R getError(List<ObjectError> allErrors) {
StringBuffer message = new StringBuffer();
for(ObjectError error: allErrors){
message.append(error.getDefaultMessage()).append(" & ");
}
log.error(message.substring(0, message.length() - 3)); // 因为&两边空格
return R.fail(message.substring(0, message.length() - 3));
}
}
postman 再次请求,返回参数
{
"code": 500,
"message": "年龄必须在 18 至 60 之间",
"data": null
}
自定义注解
虽然Bean Validation和Hibernate Validator已经提供了非常丰富的校验注解,但是在实际业务中,难免会碰到一些现有注解不足以校验的情况;这时,我们可以考虑自定义Validation注解。
创建自定义注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {PhoneValidator.class}) // 指定此注解的实现,即:验证器
public @interface PhoneValidatorAnnotation {
boolean required() default true;
String message() default "请输入正确的手机格式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
编写校验器 验证该注解
实现 ConstraintValidator 接口 并且重写 isValid 方法
public class PhoneValidator implements ConstraintValidator<PhoneValidatorAnnotation, String> {
private boolean required = false;
@Override
public void initialize(PhoneValidatorAnnotation phoneValidatorAnnotation) {
required = phoneValidatorAnnotation.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return RegexUtils.regCheck(value, RegularConstants.PHONE_REGEXP);
}
}
正则常量
public class RegularConstants {
/**
* 匹配电话
*/
public static final Pattern PHONE_REGEXP = Pattern.compile("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$");
}
正则工具类
public class RegexUtils {
/**
* @param str 被匹配的字符串
* @param pattern 正则表达式
* @return 是否匹配成功
*/
public static boolean regCheck(String str, Pattern pattern){
if (str == null || str.equals("")) {
return false;
}
Matcher matcher = pattern.matcher(str);
return matcher.matches();
}
}
使用自定义注解
@Data
public class ValidationModel {
/**
* 主键
*/
@NotNull(message = "主键不能为空")
private Long id;
/**
* 名称
*/
@NotBlank(message = "名称不能为空")
private String name;
/**
* 邮箱
*/
@NotBlank(message = "email 不能为空")
@Email(message = "email 格式不正确")
private String email;
/**
* 年龄
*/
@Range(min = 18, max = 60, message = "年龄必须在 {min} 至 {max} 之间")
private String age;
/**
* 手机号
*/
@PhoneValidatorAnnotation
private String phone;
}
postman测试
{
"code": 500,
"message": "请输入正确的手机格式",
"data": null
}
分组校验
当使用Validation校验框架的时候,一般都会将校验信息配置在对于的参数类中,如上面的ValidationModel 实体类。这样一来,所有使用该参数类的Controller类对应的方法都要进行一次校验。
直接定义在参数类中的校验注解,需要满足当参数类被多个Controller所共用时,每个Controller方法对该参数类有不同的校验规则。
创建两个接口,不需要任何参数
public interface AddGroups {
}
public interface DeleteGroups {
}
public interface UpdateGroups {
}
参数类
@Data
public class ValidationModel {
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {AddGroups.class, UpdateGroups.class, DeleteGroups.class})
private Long id;
/**
* 名称
*/
@NotBlank(message = "名称不能为空", groups = {AddGroups.class, UpdateGroups.class})
private String name;
/**
* 邮箱
*/
@NotBlank(message = "email 不能为空", groups = {AddGroups.class, UpdateGroups.class})
@Email(message = "email 格式不正确", groups = {AddGroups.class, UpdateGroups.class})
private String email;
/**
* 年龄
*/
@Range(min = 18, max = 60, message = "年龄必须在 {min} 至 {max} 之间", groups = {AddGroups.class, UpdateGroups.class})
private String age;
/**
* 手机号
*/
@PhoneValidatorAnnotation(groups = {AddGroups.class, UpdateGroups.class})
private String phone;
/**
* 描述
*/
@NotBlank(message = "描述不能为空")
@Length(min = 10, max = 100, message = "描述长度必须在 {min} 至 {max} 之间")
private String presentation;
}
在Controller中校验数据,只需要在该实体类前面的@Validated注解中添加一个value值即可,该value值指定校验规则所在的接口
@Slf4j
@RestController
@RequestMapping(value = "/index")
public class IndexController {
@RequestMapping(value = "/save", method = RequestMethod.POST)
public R save(@Validated(AddGroups.class) ValidationModel validationModel) {
log.info("save 接收到参数为 [{}]", validationModel);
return R.success(validationModel);
}
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
public R delete(@Validated(DeleteGroups.class) ValidationModel validationModel) {
log.info("delete 接收到参数为 [{}]", validationModel);
return R.success(validationModel);
}
@RequestMapping(value = "/update", method = RequestMethod.PUT)
public R update(@Validated(UpdateGroups.class) ValidationModel validationModel) {
log.info("delete 接收到参数为 [{}]", validationModel);
return R.success(validationModel);
}
@RequestMapping(value = "/jsonSave", method = RequestMethod.POST)
public R jsonSave(@Validated @RequestBody ValidationModel validationModel) {
log.info("jsonSave 接收到参数为 [{}]", validationModel);
return R.success(validationModel);
}
}
@Validated与@Valid的简单对比说明
@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:@Valid属于javax下的,而@Validated属于spring下;@Valid支持嵌套校验、而@Validated不支持,@Validated支持分组,而@Valid不支持。
@Validated分组上面已经写过就不再多写,下面为@Valid
@Valid
参数类 如果一个参数类中包含第二个bean,这时要检验第二个bean中某个字段,即嵌套校验,必须要在第一个参数类对象中使用@Valid标注到表示第二个bean对象的字段上,然后再第二个bean对象里面的字段上加上校验类型
@Data
public class ValidModel {
@NotNull(message = "ValidModel主键不能为空")
private Long id;
@Valid
private ValidBean validBean;
@Data
private class ValidBean {
@NotNull(message = "ValidBean主键不能为空")
private Long id;
@NotBlank(message = "ValidBean名称不能为空")
private String name;
}
}
在Controller中校验数据,只需要在该实体类前面加上@Valid即可
@Slf4j
@RestController
@RequestMapping(value = "/valid")
public class ValidController {
@RequestMapping(value = "/jsonSave", method = RequestMethod.POST)
public R jsonSave(@Valid @RequestBody ValidModel validModel) {
log.info("jsonSave 接收到参数为 [{}]", validModel);
return R.success(validModel);
}
}
项目已上传至 GitHub https://github.com/soul0928/liuyun-validator , 不足之处望各位海涵