提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
下面我们以新增一个员工为功能切入点,以常规写法为背景,慢慢烘托出 @Valid 注解用法详解。
那么,首先,我们会有一个员工对象 Employee,如下 :
/**
* 员工对象
*
* @author sunnyzyq
* @since 2019/12/13
*/
public class Employee {
/** 姓名 */
public String name;
/** 年龄 */
public Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
然后 Cotroller 中会有一个对应都新增方法 add(),如下:
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(Employee employee) {
// TODO 保存到数据库
return "新增员工成功";
}
}
现在要求:员工的名称不能为空,且长度不能超过10个字符,那么我们以前的做法大致如下:
除了名称外,我们规定年龄也是必填项,且范围在1到100岁,那么此时,我们需要增加对应判定代码如下:
那么问题来了,现在员工对象 Employee 就 2 个字段,我们就写了 10 多行的代码验证,要是有20个字段,岂不是要写 100多行代码?通常来说,当一个方法中的无效业务代码量过多时,往往代码设计有问题,当然这不是我们所想看到都结果。
如果你是 springboot 项目,那么可以不用引入了,已经引入了,他就存在于最核心的 web 开发包里面。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.0.5.RELEASEversion>
dependency>
如果你不是 springboot 项目,那么引入下面依赖即可:
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>1.1.0.Finalversion>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-validatorartifactId>
<version>5.4.1.Finalversion>
dependency>
那么针对上面情景,我们可以对我们的代码进行优化了。首先我们在 Employee 类的属性上打上如下注解:
package com.zyq.beans;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
/**
* 员工对象
*
* @author sunnyzyq
* @since 2019/12/13
*/
public class Employee {
/** 姓名 */
@NotBlank(message = "请输入名称")
@Length(message = "名称不能超过个 {max} 字符", max = 10)
public String name;
/** 年龄 */
@NotNull(message = "请输入年龄")
@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
public Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
然后再 Controller 对应方法上,对这个员工标上@Valid 注解,表示我们对这个对象属性需要进行验证
通过源码可以看出:
@Valid注解可以作用于:方法、属性(包括枚举中的常量)、构造函数、方法的形参上。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
所属的包
import javax.validation.Valid;
整个过程如下图所示,用户访问接口,然后进行参数效验,因为 @Valid 不支持平面的参数效验(直接写在参数中字段的效验)所以基于 GET 请求的参数还是按照原先方式进行效验,
POST 则可以以实体对象为参数,可以使用 @Valid 方式进行效验。
如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>版本号version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.0.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
@Data
@NoArgsConstructor
public class TestRequest {
@NotBlank(message = "name不为空")
private String name;
@Length(max = 3,message = "address最大长度是3")
private String address;
@Max(value = 5,message = "reqNo最大值是5")
private String reqNo;
}
@RestController
public class ValidTestController {
@RequestMapping("/valid/test")
public void test(@Valid @RequestBody TestRequest request){
System.out.println(request);
}
postman返回结果:
{
"timestamp": "2022-11-12T09:54:24.202+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public void com.example.controller.ValidTestController.test(com.example.domain.TestRequest) with 3 errors: [Field error in object 'testRequest' on field 'name': rejected value []; codes [NotBlank.testRequest.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testRequest.name,name]; arguments []; default message [name]]; default message [name不为空]] [Field error in object 'testRequest' on field 'reqNo': rejected value [8]; codes [Max.testRequest.reqNo,Max.reqNo,Max.java.lang.String,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testRequest.reqNo,reqNo]; arguments []; default message [reqNo],5]; default message [reqNo最大值是5]] [Field error in object 'testRequest' on field 'address': rejected value [gtyjh]; codes [Length.testRequest.address,Length.address,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testRequest.address,address]; arguments []; default message [address],3,0]; default message [address最大长度是3]] \r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:748)\r\n",
"message": "Validation failed for object='testRequest'. Error count: 3",
"errors": [
{
"codes": [
"NotBlank.testRequest.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"testRequest.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "name不为空",
"objectName": "testRequest",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"Max.testRequest.reqNo",
"Max.reqNo",
"Max.java.lang.String",
"Max"
],
"arguments": [
{
"codes": [
"testRequest.reqNo",
"reqNo"
],
"arguments": null,
"defaultMessage": "reqNo",
"code": "reqNo"
},
5
],
"defaultMessage": "reqNo最大值是5",
"objectName": "testRequest",
"field": "reqNo",
"rejectedValue": "8",
"bindingFailure": false,
"code": "Max"
},
{
"codes": [
"Length.testRequest.address",
"Length.address",
"Length.java.lang.String",
"Length"
],
"arguments": [
{
"codes": [
"testRequest.address",
"address"
],
"arguments": null,
"defaultMessage": "address",
"code": "address"
},
3,
0
],
"defaultMessage": "address最大长度是3",
"objectName": "testRequest",
"field": "address",
"rejectedValue": "gtyjh",
"bindingFailure": false,
"code": "Length"
}
],
"path": "/valid/test"
}
从后端返回给postman的结果可以看出,三个字段的校验都已经实现。但是,特别情况,RequestBody可能是嵌套的实体,这个时候,对于嵌套的实体类来说,嵌套必须加 @Valid,如果只在字段上添加校验注解嵌套中的验证不生效。
如果只在嵌套类字段上加上校验注解,如下:
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
@Data
@NoArgsConstructor
public class TestRequest {
@NotBlank(message = "name不为空")
private String name;
@Length(max = 3, message = "address最大长度是3")
private String address;
@Max(value = 5, message = "reqNo最大值是5")
private String reqNo;
private TestRequestInner inner;
@Data
@NoArgsConstructor
public static class TestRequestInner {
@Length(max = 3, message = "最大长度是3")
private String sonName;
private Integer sonAge;
private String schoolNo;
}
}
在嵌套类的外层加上@Valid注解,如下:
@Data
@NoArgsConstructor
public class TestRequest {
@NotBlank(message = "name不为空")
private String name;
@Length(max = 3, message = "address最大长度是3")
private String address;
@Max(value = 5, message = "reqNo最大值是5")
private String reqNo;
@Valid
private TestRequestInner inner;
// 即使放在list集合里面仍然是需要加上 @Valid 注解
// @Valid
// private List inner;
@Data
@NoArgsConstructor
public static class TestRequestInner {
@Length(max = 3, message = "最大长度是3")
private String sonName;
private Integer sonAge;
private String schoolNo;
}
}
如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。
实体类如上不变,controller接口方法改为返回string:
@RequestMapping("/valid/test")
public String test(@Valid @RequestBody TestRequest request){
System.out.println(request);
return "success";
}
异常处理类:
我们可以根据上图抛出的异常结构,去get我们想要获得的内容。
MethodArgumentNotValidException
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 自定义验证异常
* MethodArgumentNotValidException 方法参数无效异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST) //设置状态码为 400
@ExceptionHandler({MethodArgumentNotValidException.class})
public String paramExceptionHandler(MethodArgumentNotValidException e) {
BindingResult exceptions = e.getBindingResult();
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (exceptions.hasErrors()) {
List errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
return fieldError.getDefaultMessage();
}
}
return "请求参数错误";
}
}
上述的异常处理只是一个简单的string返回,但是在实际项目中,返回结构是固定的,下面对于固定的返回结构,做异常处理。
(1)request实体类
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import java.util.List;
@Data
@NoArgsConstructor
public class TestRequest {
@NotBlank(message = "name不为空")
private String name;
@Length(max = 3, message = "address最大长度是3")
private String address;
@Max(value = 5, message = "reqNo最大值是5")
private String reqNo;
@Valid
private List<TestRequestInner> inner;
@Data
@NoArgsConstructor
public static class TestRequestInner {
@Length(max = 3, message = "最大长度是3")
private String sonName;
private Integer sonAge;
@NotBlank(message = "schoolNo不空")
private String schoolNo;
}
}
(2)结果返回实体类
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@Builder
@AllArgsConstructor
public class ResponseResult {
private List<ProvideInfo> provideInfos;
@Data
@NoArgsConstructor
@Builder
@AllArgsConstructor
public static class ProvideInfo {
private String code;
private String detail;
}
}
(3)controller接口方法
import com.example.domain.ResponseResult;
import com.example.domain.TestRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class ValidTestController {
@RequestMapping("/valid/test")
public ResponseEntity<ResponseResult> test(@Valid @RequestBody TestRequest request) {
System.out.println(request);
return null;
}
}
(5)全局异常处理类各种形式
对于全局异常类的处理,涉及到拦截器相关内容,这里不做多说。全局异常类的处理方式有很多种:
方式一:
// Enum枚举类
public enum CodeEnum {
// 根据自己的项目需求更改状态码,这里只是一个示范
UNKNOW_EXCEPTION(10000,"系统未知错误"),
VALID_EXCETIPON(10001,"参数格式校验错误");
private int code;
private String msg;
CodeEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
//异常处理类
@Slf4j
@RestControllerAdvice("com.cbj.db_work.controller") //表明需要处理异常的范围
public class GlobalExceptionHandler {
@ExceptionHandler(value = MethodArgumentNotValidException.class) // 参数异常抛出的异常类型为MethodArgumentNotValidException,这里捕获这个异常
// R为统一返回的处理类
public R validExceptionHandler(MethodArgumentNotValidException e){
System.out.println("数据异常处理");
log.error("数据校验出现问题,异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach((item)->{
String message = item.getDefaultMessage();
// 获取错误的属性字段名
String field = item.getField();
map.put(field,message);
});
return R.error().code(CodeEnum.VALID_EXCETIPON.getCode()).message(CodeEnum.VALID_EXCETIPON.getMsg()).data("errorData",map);
}
}
方式二:
@ControllerAdvice
@RestControllerAdvice
@Slf4j
public class ValidExceptionHandler extends GlobalExceptionHandler {
// GET请求参数异常处理
@ExceptionHandler(value = ConstraintViolationException.class)
public Result<Object> constraintViolationExceptionHandler(ConstraintViolationException e) {
StringBuilder msg = new StringBuilder();
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
String message = constraintViolation.getMessage();
msg.append(message).append(";");
}
return ResultResponse.getFailResult(ResultCode.BODY_NOT_MATCH.getResultCode(), msg.toString());
}
@ExceptionHandler(ArithmeticException.class)
public Result<Object> arithmeticExceptionHandler(ArithmeticException e) {
e.printStackTrace();
return ResultResponse.getFailResult(ResultCode.NOT_FOUND.getResultCode(), "算术异常!"+e.getMessage());
}
// POST请求参数异常处理
@ExceptionHandler(BindException.class)
public Result<Object> bindExceptionHandler(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String msg;
if (Objects.isNull(fieldError)) {
msg = "POST请求参数异常:" + JSON.toJSONString(e.getBindingResult());
log.info(msg);
} else {
msg = fieldError.getDefaultMessage();
}
return ResultResponse.getFailResult(ResultCode.BODY_NOT_MATCH.getResultCode(), msg);
}
}
特别的,get请求校验:
@RestController
@RequestMapping(value = "/test")
@Slf4j
//@ApiIgnore
@Validated
public class TestController {
@GetMapping(value = "/test")
public Result<Object> test(@NotNull(message = "name必传")
@NotBlank(message = "name格式错误")String name) {
return ResultResponse.getSuccessResult("hello: " + name);
}
}