补习系列(4)-springboot 参数校验详解

JAVA 2023-07-05 17:29:38
78阅读

总体目标

  1. 针对几类普遍的入参方法,掌握怎样开展校验及其该怎样处理错误信息;
  2. 掌握springboot 内嵌的参数出现异常种类,并能利用拦截器完成自定义解决;
  3. 能完成简易的自定义校验标准

一、PathVariable 校验

  在界定 Restful 设计风格的插口时,一般会选用 PathVariable 特定重要业务流程参数,以下:

  @GetMapping("/path/{group:[a-zA-Z0-9_] }/{userid}")@ResponseBodypublic String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) { return group ":" userid;}

  {group:[a-zA-Z0-9_] } 那样的关系式特定了 group 务必是以英文大小写、数据或下划线构成的字符串数组。
大家尝试浏览一个不正确的途径:

  GET /path/>

  这时会获得 404的回应,因而针对PathVariable 仅由正则表达式可做到校验的目地

二、方式参数校验

  相近前边的事例,大部分状况下,大家都是会立即将HTTP要求参数投射到方式参数上。

  @GetMapping("/param")@ResponseBodypublic String param(@RequestParam("group")@Email String group, @RequestParam("userid") Integer userid) { return group ":" userid;}

  上边的编码中,@RequestParam 申明了投射,除此之外大家还为 group 界定了一个标准(复合型Email文件格式)
这一段编码是不是能立即应用呢?回答是否认的,为了更好地开启方式参数的校验工作能力,还必须进行下列流程:

  • 申明 MethodValidationPostProcessor

  @Beanpublic MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor();}

  • Controller特定@Validated注释

  @Controller@RequestMapping("/validate")@Validatedpublic class ValidateController {

  这般以后,方式上的@Email标准才可以起效。

  校验出现异常
假如这时大家试着根据不法参数开展浏览时,例如出示非Email文件格式的 group
会获得下列不正确:

  GET /validate/param?group=simple&userid===>{ "timestamp": 1530955093583, "status": 500, "error": "Internal Server Error", "exception": "javax.validation.ConstraintViolationException", "message": "No message available", "path": "/validate/param"}

  而假如参数种类不正确,例如出示非整数金额的 userid,会获得:

  GET /validate/param?group=simple&userid=1f====>{ "timestamp": 1530954430720, "status": 400, "error": "Bad Request", "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "1f"", "path": "/validate/param"}

  当存有参数缺少时,因为界定的@RequestParam注释中,特性 required=true,也可能造成 不成功:

  GET /validate/param?userid===>{ "timestamp": 1530954345877, "status": 400, "error": "Bad Request", "exception": "org.springframework.web.bind.MissingServletRequestParameterException", "message": "Required String parameter 'group' is not present", "path": "/validate/param"}

三、表格目标校验

  网页页面的表格一般非常复杂,这时能够将要求参数封裝到表格目标中,
并特定一系列相匹配的标准,参照JSR-303

  public static class FormRequest { @NotEmpty @Email private String email; @Pattern(regexp = "[a-zA-Z0-9_]{6,30}") private String name; @Min(5) @Max(199) private int age;

  上边界定的特性中:

  • email务必非空、合乎Email文件格式标准;
  • name务必为英文大小写、数据及下划线构成,长短在6-30个;
  • age务必在5-199范畴内

  Controller方法中的界定:

  @PostMapping("/form")@ResponseBodypublic FormRequest form(@Validated FormRequest form) { return form;}

  @Validated特定了参数目标必须实行一系列校验。

  校验出现异常
这时大家试着结构一些违背标准的键入,会获得下列的結果:

  { "timestamp": 1530955713166, "status": 400, "error": "Bad Request", "exception": "org.springframework.validation.BindException", "errors": [ { "codes": [ "Email.formRequest.email", "Email.email", "Email.java.lang.String", "Email" ], "arguments": [ { "codes": [ "formRequest.email", "email" ], "arguments": null, "defaultMessage": "email", "code": "email" }, [], { "arguments": null, "codes": [ ".*" ], "defaultMessage": ".*" } ], "defaultMessage": "并不是一个合理合法的电子邮箱详细地址", "objectName": "formRequest", "field": "email", "rejectedValue": "tecom", "bindingFailure": false, "code": "Email" }, { "codes": [ "Pattern.formRequest.name", "Pattern.name", "Pattern.java.lang.String", "Pattern" ], "arguments": [ { "codes": [ "formRequest.name", "name" ], "arguments" : null, "defaultMessage": "name", "code": "name" }, [], { "arguments": null, "codes": [ "[a-zA-Z0-9_]{6,30}" ], "defaultMessage": "[a-zA-Z0-9_]{6,30}" } ], "defaultMessage": "需要匹配正则表达式"[a-zA-Z0-9_]{6,30}"", "objectName": "formRequest", "field": "name", "rejectedValue": "fefe", "bindingFailure": false, "code": "Pattern" }, { "codes": [ "Min.formRequest.age", "Min.age", "Min.int", "Min" ], "arguments": [ { "codes": [ "formRequest.age", "age" ], "arguments": null, "defaultMessage": "age", "code": "age" }, 5 ], "defaultMessage": "最小不能小于5", "objectName": "formRequest", "field": "age", "rejectedValue": 2, "bindingFailure": false, "code": "Min" } ], "message": "Validation failed for object='formRequest'. Error count: 3", "path": "/validate/form"}

  如果是参数类型不匹配,会得到:

  { "timestamp": 1530955359265, "status": 400, "error": "Bad Request", "exception": "org.springframework.validation.BindException", "errors": [ { "codes": [ "typeMismatch.formRequest.age", "typeMismatch.age", "typeMismatch.int", "typeMismatch" ], "arguments": [ { "codes": [ "formRequest.age", "age" ], "arguments": null, "defaultMessage": "age", "code": "age" } ], "defaultMessage": "Failed to convert property value of type 'java.lang.String' to required type 'int' for property 'age'; nested exception is java.lang.NumberFormatException: For input string: """, "objectName": "formRequest", "field": "age", "rejectedValue": "", "bindingFailure": true, "code": "typeMismatch" } ], "message": "Validation failed for object='formRequest'. Error count: 1", "path": "/validate/form"}

Form表单参数上,使用@Valid注解可达到同样目的,而关于两者的区别则是:

  @Valid 基于JSR303,即 Bean Validation 1.0,由Hibernate Validator实现;
@Validated 基于JSR349,是Bean Validation 1.1,由Spring框架扩展实现;

  后者做了一些增强扩展,如支持分组校验,有兴趣可参考这里

四、RequestBody 校验

  对于直接Json消息体输入,同样可以定义校验规则:

  @PostMapping("/json")@ResponseBodypublic JsonRequest json(@Validated @RequestBody JsonRequest request) { return request;}...public static class JsonRequest { @NotEmpty @Email private String email; @Pattern(regexp = "[a-zA-Z0-9_]{6,30}") private String name; @Min(5) @Max(199) private int age;

  校验异常
构造一个违反规则的Json请求体进行输入,会得到:

  { "timestamp": 1530956161314, "status": 400, "error": "Bad Request", "exception": "org.springframework.web.bind.MethodArgumentNotValidException", "errors": [ { "codes": [ "Min.jsonRequest.age", "Min.age", "Min.int", "Min" ], "arguments": [ { "codes": [ "jsonRequest.age", "age" ], "arguments": null, "defaultMessage": "age", "code": "age" }, 5 ], "defaultMessage": "最小不能小于5", "objectName": "jsonRequest", "field": "age", "rejectedValue": 1, "bindingFailure": false, "code": "Min" } ], "message": "Validation failed for object='jsonRequest'. Error count: 1", "path": "/validate/json"}

  此时与FormBinding的情况不同,我们得到了一个MethodArgumentNotValidException异常。
而如果发生参数类型不匹配,比如输入age=1f,会产生以下结果:

  { "timestamp": 1530956206264, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "Could not read document: Can not deserialize value of type int from String "ff": not a valid Integer value at [Source: java.io.PushbackInputStream@68dc9800; line: 2, column: 8] (through reference chain: org.zales.dmo.boot.controllers.ValidateController$JsonRequest["age"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type int from String "ff": not a valid Integer value at [Source: java.io.PushbackInputStream@68dc9800; line: 2, column: 8] (through reference chain: org.zales.dmo.boot.controllers.ValidateController$JsonRequest["age"])", "path": "/validate/json"}

  这表明在JSON转换过程中已经失败!

五、自定义校验规则

  框架内预置的校验规则可以满足大多数场景使用,
但某些特殊情况下,你需要制作自己的校验规则,这需要用到ContraintValidator接口。

  我们以一个密码校验的场景作为示例,比如一个注册表单上,
我们需要检查 密码输入密码确认 是一致的。

  **首先定义 PasswordEquals 注解

  @Documented@Constraint(validatedBy = { PasswordEqualsValidator.class })@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)public @interface PasswordEquals { String message() default "Password is not the same"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}

  在表单上声明@PasswordEquals 注解

  @PasswordEqualspublic class RegisterForm { @NotEmpty @Length(min=5,max=30) private String username; @NotEmpty private String password; @NotEmpty private String passwordConfirm;

  针对@PasswordEquals实现校验逻辑

  public class PasswordEqualsValidator implements ConstraintValidator<PasswordEquals, RegisterForm> { @Override public void initialize(PasswordEquals anno) { } @Override public boolean isValid(RegisterForm form, ConstraintValidatorContext context) { String passwordConfirm = form.getPasswordConfirm(); String password = form.getPassword(); boolean match = passwordConfirm != null ? passwordConfirm.equals(password) : false; if (match) { return true; } String messageTemplate = context.getDefaultConstraintMessageTemplate(); // disable default violation rule context.disableDefaultConstraintViolation(); // assign error on password Confirm field context.buildConstraintViolationWithTemplate(messageTemplate).addPropertyNode("passwordConfirm") .addConstraintViolation(); return false; }}

  如此,我们已经完成了自定义的校验工作。

六、异常拦截器

  SpringBoot 框架中可通过 @ControllerAdvice 实现Controller方法的拦截操作。
可以利用拦截能力实现一些公共的功能,比如权限检查、页面数据填充,以及全局的异常处理等等。

  在前面的篇幅中,我们提及了各种校验失败所产生的异常,整理如下表:

  如果希望对这些异常实现统一的捕获,并返回自定义的消息,
可以参考以下的代码片段:

  @ControllerAdvicepublic static class CustomExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { ConstraintViolationException.class }) public ResponseEntity<String> handle(ConstraintViolationException e) { Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); StringBuilder strBuilder = new StringBuilder(); for (ConstraintViolation<?> violation : violations) { strBuilder.append(violation.getInvalidValue() + " " + violation.getMessage() + ""); } String result = strBuilder.toString(); return new ResponseEntity<String>("ConstraintViolation:" + result, HttpStatus.BAD_REQUEST); } @Override protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>("BindException:" + buildMessages(ex.getBindingResult()), HttpStatus.BAD_REQUEST); } @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>("MethodArgumentNotValid:" + buildMessages(ex.getBindingResult()), HttpStatus.BAD_REQUEST); } @Override public ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>("ParamMissing:" + ex.getMessage(), HttpStatus.BAD_REQUEST); } @Override protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>("TypeMissMatch:" + ex.getMessage(), HttpStatus.BAD_REQUEST); } private String buildMessages(BindingResult result) { StringBuilder resultBuilder = new StringBuilder(); List<ObjectError> errors = result.getAllErrors(); if (errors != null && errors.size() > 0) { for (ObjectError error : errors) { if (error instanceof FieldError) { FieldError fieldError = (FieldError) error; String fieldName = fieldError.getField(); String fieldErrMsg = fieldError.getDefaultMessage(); resultBuilder.append(fieldName).append(" ").append(fieldErrMsg).append(";"); } } } return resultBuilder.toString(); }}

  默认情况下,对于非法的参数输入,框架会产生 HTTP_BAD_REQUEST(status=400) 错误码,
并输出友好的提示消息,这对于一般情况来说已经足够。

  更多的输入校验及提示功能应该通过客户端去完成(服务端仅做同步检查),
客户端校验的用户体验更好,而这也符合富客户端(rich client)的发展趋势。

  码云同步代码

参考文档

  springmvc-validation样例
使用validation api进行操作
hibernate-validation官方文档
Bean-Validation规范

  欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^

  更多精彩内容,请滑至顶部点击右上角关注小宅哦~

  


  

  来源:zale

the end
免责声明:本文不代表本站的观点和立场,如有侵权请联系本站删除!本站仅提供信息存储空间服务。