概述
在 Web 应用中,客户端提交数据之前都会进行数据的校验,比如用户注册时填写的邮箱地址是否符合规范、用户名长度的限制等等,不过这并不意味着服务端的代码可以免去数据验证的工作,用户也可能使用 HTTP 工具直接发送违法数据。为了保证数据的安全性,服务端的数据校验是必须的。
先理清概念:
JSR-303 是 JavaEE 6 中的一项子规范,又称作 Bean Validation,提供了针对 Java Bean 字段的一些校验注解,如@NotNull,@Min等。JSR-349 是其升级版本,添加了一些新特性。
Hibernate Validator 是对这个规范的实现(与 ORM 框架无关),并在它的基础上增加了一些新的校验注解。
Spring 本身也有一个校验接口Validator,位于 org.springframework.validation 包下,但是使用这个接口需要进行硬编码,也就是手动校验,没有提供注解进行简化。为了给开发者提供便捷,Spring 也全面支持 JSR-303、JSR-349 规范,对 Hibernate Validation 进行二次封装,在 SpringMVC 模块中添加了自动校验机制,可以利用注解对 Java Bean 的字段的值进行校验,并将校验信息封装进特定的类中。
下面将介绍如何在 Spring 应用中使用 JSR-303 校验规范。
校验注解
JSR-303 包含的注解
注解名称 | 说明 |
---|---|
@Null | 被注解元素必须为 null |
@NonNull | 被注解元素必须不为 null |
@AssertTrue | 被注解元素必须为 true |
@AssertFalse | 被注解元素必须为 false |
@Min(value) | 被注解元素必须是一个值,并且不能小于指定的值 |
@Max(value) | 被注解元素必须是一个值,并且不能大于指定的值 |
@DecimalMin(value) | 被注解元素必须是一个数字,并且不能小于指定的值 |
@DecimalMax(value) | 被注解元素必须是一个数字,并且不能大于指定的值 |
@Size(max=,min=) | 被注解元素的大小必须在指定范围内 |
@Digits(integer,fraction) | 被注解元素必须是一个数字,其值必须在指定范围内 |
@Past | 被注解元素必须是一个过去的日期 |
@Future | 被注解元素必须是一个将来的日期 |
@Pattern(regex=,flag=) | 被注解元素必须符合指定的正则表达式 |
Hibernate Validator 扩展的注解
注解名称 | 说明 |
---|---|
@NotBlank(message=) | 被注解的字符串必须非 null 且trim()后长度大于 0 |
被注解元素必须是电子邮箱地址 | |
@Length(min=,max=) | 被注解的字符串的长度必须在指定范围内 |
@NotEmpty | 被注解元素(字符串、数组、集合等)必须非 null 且长度大于 0 |
@Range(min=,max=,message=) | 被注解元素必须在合适的范围内 |
@URL | 被注解元素必须是合法的 URL |
添加依赖
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.projectlombok
lombok
创建一个返回统一个是的类
@Data
@NoArgsConstructor
public class R {
private int code = 200;
private String msg = "成功";
private Object data;
public R(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static R success(){
return new R();
}
public static R error(){
return error(500, "出错拉");
}
public static R error(int code,String msg){
return new R(code,msg);
}
public static R success(Object data){
R r = new R();
r.setData(data);
return r;
}
public static R error(Object obj){
R r = error();
r.setData(obj);
return r;
}
}
编写一个 javaBean 使用 Validation 注解
@Data
public class UserInfo {
private Integer id;
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Range(min = 1,max = 100,message = "年龄必须1-100岁之间")
private Integer age;
@NotBlank(message = "邮件不能为空")
@Email(message = "邮件格式不正确")
private String email;
}
编写一个 controller 测试
- @Valid 和 BindingResult 是成对出现的
/**
* 临时方式输出力 每个方法都需要加 BindingResult
* @param userInfo
* @param result
* @return
*/
@PostMapping("/validationTest")
public R validationTest(@RequestBody @Valid UserInfo userInfo, BindingResult result){
if(result.hasErrors()){
List allErrors = result.getAllErrors();
String errors = "";
for (int i = 0; i < allErrors.size(); i++) {
if(i == allErrors.size()-1){
errors += allErrors.get(i).getDefaultMessage();
}else {
errors += allErrors.get(i).getDefaultMessage()+"---";
}
}
return R.error(errors);
}
return R.success("用户合法");
}
当我们参数全部正确填写时请求没有问题
一旦填写的参数不合法时就会返回校验的异常
上面这种方案适合临时方案在特殊的时候使用,因为每一个方法都需要加上一个 @Valid 注解 和 BindingResult ,类多的话想想都够烦的,下面我们使用 spring 统一异常处理机制,来编写一个全局异常处理
编写统一异常处理类
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* valid 异常处理
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handlerValidException(MethodArgumentNotValidException e){
// 获取单个错误
// String errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
// 获取所有错误信息
List allErrors = e.getBindingResult().getAllErrors();
String errors = "";
for (int i = 0; i < allErrors.size(); i++) {
if(i == allErrors.size()-1){
errors += allErrors.get(i).getDefaultMessage();
}else {
errors += allErrors.get(i).getDefaultMessage()+" | ";
}
}
log.info("data errors = {}",errors);
return R.error(errors);
}
}
在编写一 controller 方法
/**
* 使用全局异常处理
* @param userInfo
* @return
*/
@PostMapping("/validErrorsTest")
public R validErrorsTest(@RequestBody @Valid UserInfo userInfo){
return R.success(userInfo);
}
测试一下,效果是一样的,但是代码简洁了许多
自定义校验
- 在使用 @Email 注解校验邮箱的时候发现没有点也是合法的,但是我们需要必须要有点怎么实现呢?我们可以自定义一个注解校验
编写一个邮箱校验注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定真正的校验类
@Constraint(validatedBy = RequiredEmailFormatValidator.class)
public @interface RequiredEmailFormat {
String message() default "邮件格式不正确";
//分组
Class>[] groups() default {};
//负载
Class extends Payload>[] payload() default {};
//指定多个时使用,从而支持重复注解
@Target({ElementType.FIELD,ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
RequiredEmailFormat[] value();
}
}
- @Constraint 注解指定真正的校验类
编写校验类,需要实现 ConstraintValidator 接口
/**
* 校验邮箱是否合法
*/
public class RequiredEmailFormatValidator implements ConstraintValidator {
/**
* 初始化事件方法
* @param constraintAnnotation
*/
@Override
public void initialize(RequiredEmailFormat constraintAnnotation) {
}
/**
* 判断是否合法
* @param s
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if(StringUtils.isEmpty(s)){
return false;
}
// 邮件正则
String checkReg = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
Pattern regex = Pattern.compile(checkReg);
// 不匹配
if(!regex.matcher(s).matches()){
// 禁用默认提示信息
//constraintValidatorContext.disableDefaultConstraintViolation();
// 设置提示语
//constraintValidatorContext.buildConstraintViolationWithTemplate("email error").addConstraintViolation();
return false;
}
return true;
}
}
第一个泛型参数是表明校验的注解类型,第二个泛型参数是需要被校验的类型。
- initialize:初始化事件方法
- isValid:判断是否合法的方法
ConstraintValidatorContext这个上下文包含了校验中所有的信息,我们可以利用这个对象进行获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作
我们在原来的邮件字段上加上我们自定义的注解
@NotBlank(message = "邮件不能为空")
//@Email(message = "邮件格式不正确")
@RequiredEmailFormat(message = "邮件格式输入不正确")
private String email;
测试
- 这时邮箱校验就会使用我们自定义的校验,如果输入的邮箱中没有点的话校验也是不通过的
有时候我们后端需要的是一个数字,前台确传递了一个字符串,这时 jackson 会抛出类型转换异常,如果把这些信息返回个前端不是很友好,自己看着也不舒服,最好能够具体一点,比如那个字段,需要什么类型,输入的值什么等......
在统一异常处理类中添加一个统一的异常处理方法来通一处理
/**
* 处理统一的类型转换异常
* @param e
* @return
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public R httpMessageNotReadableException(HttpMessageNotReadableException e){
if(e.getCause() instanceof InvalidFormatException){
InvalidFormatException invalidFormatException = (InvalidFormatException)e.getCause();
String errors = "";
List path = invalidFormatException.getPath();
for(JsonMappingException.Reference reference : path){
errors += "参数名:"+reference.getFieldName()+
" 输入不合法,需要的是 "+invalidFormatException.getTargetType().getName() +
" 类型,"+"提交的值是:"+invalidFormatException.getValue().toString();
log.info("参数名:{}",reference.getFieldName());
}
log.info("提交的参数值:{}",invalidFormatException.getValue().toString());
log.info("需要的参数类型:{}",invalidFormatException.getTargetType().getName());
return R.error(errors);
}
return R.error();
}
这个时候我们再来测试
- 我们年龄依然写的是字符串,但是异常信息已经和明了了
group 分组校验
-
在我们添加数据和修改数据的时候,添加的时候是不能指定 id 的,但是修改的时候又必须有 id ,这是就可以使用分组校验,每一个校验注解都有一个 groups 属性,接收的是一个接口类型数组,用来表示分组
我们创建 addGroup 接口表示添加分组,updateGroup 接口表示更新分组,接口可以是空接口
然后在校验注解上引用
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@NotBlank(message = "id不能为空",groups = {UpdateGroup.class})
这是 Controller 中就不能使用 @Valid 注解了,需要使用 @Validated 注解来指定校验分组
比如我们这里是一个新增的方法 id 必须是空的,这是我们就使用添加分组,在添加数据的时候校验 id 必须为空,注意,默认没有分组的校验注解,在分组校验 @Validated(value = {AddGroup.class} 下不会生效
@RequestMapping("/save")
public R save(@RequestBody @Validated(value = {AddGroup.class}) BrandEntity brand){
brandService.save(brand);
return R.ok();
}
-
测试添加
Validator 国际化配置
yml 配置
spring:
messages:
basename: i18n/validations
encoding: UTF-8
resources 目录下新建 i18n 文件夹,并创建两个文件,一个是 valications.properties 默认读取的中文国际化文件,另一个是 valications_en.properties 英文国际化文件
- validations.properties 内容
userNameNotEmpty=用户名不能为空 - validations_en.properties 内容
userNameNotEmpty=username not empty !
i18n 配置类
@Configuration
public class I18nConfig {
@Autowired
private MessageSource messageSource;
@Bean
public Validator getValidator(){
LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
validatorFactoryBean.setValidationMessageSource(this.messageSource);
return validatorFactoryBean;
}
}
在实例中引用
@NotBlank(message = "{userNameNotEmpty}")
private String name;
测试
-
默认走的是 validations.properties
在请求头 headers 中加入 Accept-Language=en 就会切换到英文
参考 https://www.cnblogs.com/zzzt20/p/12482979.html