spring-boot整合validate_litong131219的博客-CSDN博客

spring-boot整合validate**

在Contrller中进行验证**

创建spring-boot整合validate工程,在Controller中验证形参**

springboot版本是2.0.0.RELEASE已经内置hibernate validate好的,隶属于jsr303规范

官网:(以官网为准)

http://hibernate.org/validator/

api doc

https://docs.jboss.org/hibernate/stable/validator/api/

创建工程,添加依赖

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId></dependency><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-devtools</artifactId></dependency><dependency>  <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId></dependency>

spring-boot-starter-web已将依赖添加了

<dependency>  <groupId>org.hibernate</groupId>  <artifactId>hibernate-validator</artifactId></dependency><dependency>  <groupId>com.fasterxml.jackson.core</groupId>  <artifactId>jackson-databind</artifactId></dependency>

测试在不进行验证时候的返回值

编写Controler代码

@RestController@RequestMapping("validate")@Slf4jpublic class ValidateController {  @RequestMapping("validate")  public String validateTest(String address) {    log.info("address={}", address);    return "success";  }}

访问http://localhost:8080/validate/validate返回如下

{  "timestamp": "2019-11-20T08:06:15.728+0000",  "status": 400,  "error": "Bad Request",  "message": "Required String parameter 'address' is not present",  "path": "/user/validate"}

访问http://localhost:8080/validate/validate?address返回success

测试在进行进行验证的时候的返回值

请求方法中的请求参数上直接添加验证规则 如:@NotNull ,需要在该类上面需要添加@Validated

import org.hibernate.validator.constraints.NotBlank;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import lombok.extern.slf4j.Slf4j; @RestController@RequestMapping("validate")@Slf4j@Validatedpublic class ValidateController {  @RequestMapping("validate")  public String validateTest(@NotBlank(message = "地址不能为空!") String address) {    log.info("address={}", address);    return "success";  }}

再次访问http://localhost:8080/validate/validate?address如下.成功的进行了验证

{  "timestamp": "2019-11-20T08:13:53.382+0000",  "status": 500,  "error": "Internal Server Error",  "message": "validateTest.address: 地址不能为空!",  "path": "/user/validate"}

验证实体类**

编写一个实体类,@Past验证生日字段

import java.util.Date;import javax.validation.constraints.Past;import org.springframework.format.annotation.DateTimeFormat;import com.fasterxml.jackson.annotation.JsonFormat;import lombok.Data; @Datapublic class User {  private String name;  @Past(message = "生日必须是一个过去的日期")  @JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")  @DateTimeFormat(pattern = "yyyy/MM/dd")  private Date birthday;}

在Controller中进行验证

Contrller的类上添加@Validated,方法的形参的实体类上添加@Valid注解,后面紧跟着BindingResult result(必须强制性的),校验的结果放在了result中。

import javax.validation.Valid; import org.springframework.validation.BindingResult;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import com.biillrobot.study.spring.validte.dataobject.User; import lombok.extern.slf4j.Slf4j; @RestController@RequestMapping("user")@Slf4j@Validatedpublic class UserController {  @RequestMapping("add")  public String add(@Valid User user, BindingResult result) {    log.info("user={}", user);    return "success";  }}

测试访问

http://localhost:8080/user/add?name=litong&birthday=2019/11/21

出现下面的错误提示

{  "timestamp": "2019-11-20T08:28:32.600+0000",  "status": 500,  "error": "Internal Server Error",  "message": "add.user.birthday: 生日必须是一个过去的日期",  "path": "/user/add"}

但是如果格式不合法吗?

http://localhost:8080/user/add?name=litong&birthday=2019-11-21

因为在实体类中没有对生日格式进行验证,所以在这里生日格式可以不合法,上面的访问结果是success,日志中显示的是

2019-11-20 16:32:42.888 INFO  UserController.add:21 - user=User(name=litong, birthday=null)

国际化**

添加国际化验证提示**

在resources目录下新建一个ValidationMessages_zh_CN.properties的文件,内容如下

user.birthday.past=\u751F\u65E5\u5FC5\u987B\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65E5\u671F

只需要在实体类上message指定用哪个消息key就行了

@Datapublic class User {  private String name;  @Past(message = "{user.birthday.past}")  @JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")  @DateTimeFormat(pattern = "yyyy/MM/dd")  private Date birthday;}

发送请求,进行验证,如果验证失败会返回错误的信息如下

同时日志中也会出现一个ConstraintViolationException异常

javax.validation.ConstraintViolationException: add.user.birthday xxx  at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:109)  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)  at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)  at com.biillrobot.study.spring.validte.controller.UserController$$EnhancerBySpringCGLIB$$2465c54c.add(<generated>)

验证国际化乱码问题**

[乱码问题面描述]

ValidationMessages_zh_CN.properties的默认编码是ISO-8891-1,输入的汉字会经过unicode转码

user.birthday.past=\u751F\u65E5\u5FC5\u987B\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65E5\u671F

验证失败时返回的内容不会乱码

但是如果将ValidationMessages_zh_CN.properties编码改成UTF-8

返回的信息中就会包含乱码

[乱码问题原因]

笔者没有找到原因,一直没有解决

分组校验和自定校验规则**

分组校验**

笔者分组校验测试失败,测试过程如下

编写实体类

定义接口Update

在校验注解属性上使用groups指定class类

import java.util.Date; import javax.validation.constraints.NotNull;import javax.validation.constraints.Past;import org.springframework.format.annotation.DateTimeFormat;import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @Datapublic class User {  public interface Update{};   @NotNull(groups=Update.class ,message="更新时id不能为空")  private Integer id;    private String name;  @Past(message = "{user.birthday.past}")  @JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")  @DateTimeFormat(pattern = "yyyy/MM/dd")  private Date birthday;}

Controller中

在方法的形参上使用@Validated指明class类

import javax.validation.Valid; import org.springframework.validation.BindingResult;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import com.biillrobot.study.spring.validte.dataobject.User; import lombok.extern.slf4j.Slf4j; @RestController@RequestMapping("user")@Slf4j@Validatedpublic class UserController {  @RequestMapping("add")  public String add(@Valid User user, BindingResult result) {    log.info("user={}", user);    return "success";  }    @RequestMapping("update")  public String update(@Validated({User.Update.class}) User user,BindingResult result){    log.info("user={}",user);    return "success";  }}

奇怪的是测试失败,不发送id竟然也是成功

http://localhost:8080/user/update?name=litong&birthday=2019/11/22

使用分组校验,必须要手动获取错误,定义返回的消息格式,,修改后的Controller代码如下

  @RequestMapping("update")  public String update(@Validated({ User.Update.class }) User user, BindingResult bindingResult) {    log.info("user={}", user);    if (bindingResult.hasErrors()) {      String errorMsg = bindingResult.getFieldError().getDefaultMessage();      log.info("errorMsg={}", errorMsg);      return errorMsg;    }    return "success";  }

分组校验和其他校验不同的是,即使校验失败,在数据库中也不会出现异常

自定义校验规则**

自定义校验器

自定类,实现ConstraintValidator,在泛型中填入注解类名和校验的数据类型

重新isValid方法对接进行校验,校验通过返回true,校验失败返回false

下面的校验规则定义,String类型必须为空

import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext; import org.springframework.util.StringUtils; import lombok.extern.slf4j.Slf4j; @Slf4jpublic class MustEmptyValidator implements ConstraintValidator<MustEmpty, String> {   @Override  public boolean isValid(String input, ConstraintValidatorContext context) {    log.info("input={}", input);    // 验证通过返回true    if (StringUtils.isEmpty(input)) {      log.info("验证通过");      return true;    }    log.info("验证失败");    // 验证失败返回false    return false;  }}

自定义注解,指定校验器

@Constraint(validatedBy=MustEmptyValidator.class)@Documented@Target({ElementType.METHOD,ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface MustEmpty {  String message() default "属性必须为空";  Class<?>[] groups() default {};  Class<? extends Payload>[] payload() default {};}

使用自定义校规则

在实体类中使用自定义的校验注解

import java.util.Date; import javax.validation.constraints.NotNull;import javax.validation.constraints.Past; import org.springframework.format.annotation.DateTimeFormat; import com.biillrobot.study.spring.validte.annotation.MustEmpty;import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @Datapublic class User {  public interface Update{};  public interface Insert{};  @NotNull(groups=Update.class ,message="更新时id不能为空")  @MustEmpty(groups=Insert.class,message="添加时id必须为空")  private String id;    private String name;  @Past(message = "{user.birthday.past}")  @JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")  @DateTimeFormat(pattern = "yyyy/MM/dd")  private Date birthday;}

Controller变化不大,如下

import org.springframework.validation.BindingResult;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import com.biillrobot.study.spring.validte.dataobject.User; import lombok.extern.slf4j.Slf4j; @RestController@RequestMapping("user")@Slf4j@Validatedpublic class UserController {  @RequestMapping("add")  // public String add(@Valid User user, BindingResult result) {  public String add(@Validated(User.Insert.class) User user, BindingResult bindingResult) {    log.info("user={}", user);    if (bindingResult.hasErrors()) {      String errorMsg = bindingResult.getFieldError().getDefaultMessage();      log.info("errorMsg={}", errorMsg);      return errorMsg;    }    return "success";  }   @RequestMapping("update")  public String update(@Validated({ User.Update.class }) User user, BindingResult bindingResult) {    log.info("user={}", user);    return "success";  }}

发送请求测试

http://localhost:8080/user/add?id=1&name=litong&birthday=2019/11/22

返回如下

添加时id必须为空

全局异常拦截器**

使用全局异常拦截器**

编写实体类

import javax.validation.constraints.Email;import javax.validation.constraints.Max;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import javax.validation.constraints.Size; import lombok.Data; @Datapublic class Student {  @NotBlank(message = "用户名不能为空")  private String name;  @Max(value = 120, message = "年龄不能超过120岁")  private int age;  @NotNull  @Size(min = 8, max = 20, message = "密码必须大于8位并且小于20位")  private String password;  @Email(message = "请输入符合格式的邮箱")  private String email;}

编写Controller

import javax.validation.Valid; import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; import com.biillrobot.study.spring.validte.dataobject.Student; @RestController@RequestMapping("/stu")public class StudentController {   @RequestMapping("add")  public Student add(@Valid Student stu) {    // 仅测试验证过程,省略其他的逻辑    return stu;  }}

执行请求,发送一个错误的邮箱

http://localhost:8080/stu/add?name=litong&age=18&password=00000000&email=litongjava@

返回如下

{  "timestamp": "2019-11-22T06:22:02.768+0000",  "status": 400,  "error": "Bad Request",  "errors": [    {      "codes": [        "Email.student.email",        "Email.email",        "Email.java.lang.String",        "Email"      ],      "arguments": [        {            "codes": [                "student.email",                "email"            ],            "arguments": null,            "defaultMessage": "email",            "code": "email"        },        [],        {            "defaultMessage": ".*",            "arguments": null,            "codes": [                ".*"            ]        }      ],      "defaultMessage": "请输入符合格式的邮箱",      "objectName": "student",      "field": "email",      "rejectedValue": "litongjava@",      "bindingFailure": false,      "code": "Email"    }  ],  "message": "Validation failed for object='student'. Error count: 1",  "path": "/stu/add"}

日志中并没有异常堆栈,显示如下

2019-11-22 14:22:02.766 WARN  DefaultHandlerExceptionResolver.logException:193 - Resolved exception caused by Handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors

Field error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@538ddcc4,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@66098dbe]; default message [请输入符合格式的邮箱]

这是因为,SpringBoot配置了默认异常处理器DefaultHandlerExceptionResolver,而该处理器仅仅是将异常信息打印出来,显然,我们并不需要返回如此多的信息,只需要将对应属性中的message信息给调用者即可,解决的方法有两种。

1.在需要验证的方法中加入BindingResult参数,SpringBoot会自动将异常错误信息绑定到该参数上,然后处理对应的逻辑,如下

  @RequestMapping("add")  public Student add(@Valid Student stu, BindingResult bindingResult) {    if (bindingResult.hasErrors()) {      // 具体的处理逻辑,如封装错误信息等    }    return stu;  }

但是这种方式不是很优雅,因为对于每一个需要验证的方法,都需要进行这样的逻辑(虽然封装处理可以解决,但依旧每次需要手动调用以及加入BindingResult参数)

2.由于在验证失败的时候,会抛出异常,所以可以使用全局异常处理器来捕获该异常,然后进行统一处理即可,具体的异常类型是org.springframework.validation.BindException具体实现如下所示

import java.util.List;import java.util.stream.Collectors;import java.util.stream.Stream; import org.springframework.validation.BindException;import org.springframework.validation.BindingResult;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice; import lombok.extern.slf4j.Slf4j; @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {   @ExceptionHandler({ BindException.class })  public ResultInfo<?> validationErrorHandler(BindException exception) {    // 获取BindingResult对象    BindingResult bindingResult = exception.getBindingResult();    // 获取bindingResul中的所有错误    List<ObjectError> allErrors = bindingResult.getAllErrors();    //将List<ObjectError>转为Stream<ObjectError>    Stream<ObjectError> stream = allErrors.stream();    //获取Stream<ObjectError>中ObjectError.getDefaultMessage的返回值,组成新加的Stream<String>    Stream<String> map = stream.map(ObjectError::getDefaultMessage);    //Stream<String>转为List<String>    List<String> errorInformation = map.collect(Collectors.toList());    log.info("异常已将发现,获取到的错误信息是={}", errorInformation);    return new ResultInfo<>(400, errorInformation.toString(), null);  }}

发送和上面相同的请求,返回的消息语句如下

{    "code": 400,    "message": "[请输入符合格式的邮箱]",    "body": null}

日志中内容如下

2019-11-22 14:50:18.240 INFO  GlobalExceptionHandler.validationErrorHandler:28 - 异常已将发现,获取到的错误信息是=[请输入符合格式的邮箱]

2019-11-22 14:50:18.243 WARN  ExceptionHandlerExceptionResolver.logException:193 - Resolved exception caused by Handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors

Field error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@4ec9c0f5,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@1e0fa557]; default message [请输入符合格式的邮箱]

ObjectError中有很多字段,笔者可以按需获取

获取field和getMessage组合成map返回

import java.util.List;import java.util.Map;import java.util.stream.Collectors;import java.util.stream.Stream; import org.springframework.validation.BindException;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice; import lombok.extern.slf4j.Slf4j; @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {   @ExceptionHandler({ BindException.class })  public ResultInfo<?> validationErrorHandler(BindException exception) {    // 获取BindingResult对象    BindingResult bindingResult = exception.getBindingResult();    // 获取bindingResul中的所有错误    List<ObjectError> allErrors = bindingResult.getAllErrors();    //获取field和getMessage组合成map返回    Map<String, String> collect = allErrors.stream().collect(Collectors.toMap(item -> ((FieldError) item).getField(),      item -> item.getDefaultMessage(), (oldVal, currVal) -> oldVal));    return new ResultInfo<>(-400, exception.getMessage(), collect);  }}

返回的信息内容如下

{  "code": -400,  "message": "org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@5e24fd07,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@4b426803]; default message [请输入符合格式的邮箱]",  "body": {    "email": "请输入符合格式的邮箱"  }}

常用验证注解**

常用的注解主要有以下几个,作用及内容如下所示

@Null,标注的属性值必须为空

@NotNull,标注的属性值不能为空

@AssertTrue,标注的属性值必须为true

@AssertFalse,标注的属性值必须为false

@Min,标注的属性值不能小于min中指定的值

@Max,标注的属性值不能大于max中指定的值

@DecimalMin,小数值,同上

@DecimalMax,小数值,同上

@Negative,负数

@NegativeOrZero,0或者负数

@Positive,整数

@PositiveOrZero,0或者整数

@Size,指定字符串长度,注意是长度,有两个值,min以及max,用于指定最小以及最大长度

@Digits,内容必须是数字

@Past,时间必须是过去的时间

@PastOrPresent,过去或者现在的时间

@Future,将来的时间

@FutureOrPresent,将来或者现在的时间

@Pattern,用于指定一个正则表达式

@NotEmpty,字符串内容非空

@NotBlank,字符串内容非空且长度大于0

@Email,邮箱

@Range,用于指定数字,注意是数字的范围,有两个值,min以及max


原网址: 访问
创建于: 2020-12-23 17:47:34
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论