[TOC]html
目标
- 对于几种常见的入参方式,了解如何进行校验以及该如何处理错误消息;
- 了解springboot 内置的参数异常类型,并能利用拦截器实现自定义处理;
- 能实现简单的自定义校验规则
1、PathVariable 校验
在定义 Restful 风格的接口时,一般会采用 PathVariable 指定关键业务参数,以下:java
@GetMapping("/path/{group:[a-zA-Z0-9_]+}/{userid}") @ResponseBody public String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) { return group + ":" + userid; }
{group:[a-zA-Z0-9_]+} 这样的表达式指定了 group 必须是以大小写字母、数字或下划线组成的字符串。 咱们试着访问一个错误的路径:git
GET /path/testIllegal.get/10000
此时会获得 404的响应,所以对于PathVariable 仅由正则表达式可达到校验的目的web
2、方法参数校验
相似前面的例子,大多数状况下,咱们都会直接将HTTP请求参数映射到方法参数上。正则表达式
@GetMapping("/param") @ResponseBody public String param(@RequestParam("group")@Email String group, @RequestParam("userid") Integer userid) { return group + ":" + userid; }
上面的代码中,@RequestParam 声明了映射,此外咱们还为 group 定义了一个规则(复合Email格式) 这段代码是否能直接使用呢?答案是否认的,为了启用方法参数的校验能力,还须要完成如下步骤:spring
- 声明 MethodValidationPostProcessor
@Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }
- Controller指定**@Validated**注解
@Controller @RequestMapping("/validate") @Validated public class ValidateController {
如此以后,方法上的@Email规则才能生效。json
校验异常 若是此时咱们尝试经过非法参数进行访问时,好比提供非Email格式的 group 会获得如下错误:api
GET /validate/param?group=simple&userid=10000 ====> { "timestamp": 1530955093583, "status": 500, "error": "Internal Server Error", "exception": "javax.validation.ConstraintViolationException", "message": "No message available", "path": "/validate/param" }
而若是参数类型错误,好比提供非整数的 userid,会获得:spring-mvc
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,也将会致使失败:springboot
GET /validate/param?userid=10000 ====> { "timestamp": 1530954345877, "status": 400, "error": "Bad Request", "exception": "org.springframework.web.bind.MissingServletRequestParameterException", "message": "Required String parameter 'group' is not present", "path": "/validate/param" }
3、表单对象校验
页面的表单一般比较复杂,此时能够将请求参数封装到表单对象中, 并指定一系列对应的规则,参考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") @ResponseBody public 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框架扩展实现;
后者作了一些加强扩展,如支持分组校验,有兴趣可参考这里。
4、RequestBody 校验
对于直接Json消息体输入,一样能够定义校验规则:
@PostMapping("/json") @ResponseBody public 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\n 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\n 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转换过程当中已经失败!
5、自定义校验规则
框架内预置的校验规则能够知足大多数场景使用, 但某些特殊状况下,你须要制做本身的校验规则,这须要用到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 注解
@PasswordEquals public 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; } }
如此,咱们已经完成了自定义的校验工做。
6、异常拦截器
SpringBoot 框架中可经过 @ControllerAdvice 实现Controller方法的拦截操做。 能够利用拦截能力实现一些公共的功能,好比权限检查、页面数据填充,以及全局的异常处理等等。
在前面的篇幅中,咱们说起了各类校验失败所产生的异常,整理以下表:
异常类型 | 描述 |
---|---|
ConstraintViolationException | 违反约束,javax扩展定义 |
BindException | 绑定失败,如表单对象参数违反约束 |
MethodArgumentNotValidException | 参数无效,如JSON请求参数违反约束 |
MissingServletRequestParameterException | 参数缺失 |
TypeMismatchException | 参数类型不匹配 |
若是但愿对这些异常实现统一的捕获,并返回自定义的消息, 能够参考如下的代码片断:
@ControllerAdvice public 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() + "\n"); } 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篇" ,期待更多精彩内容^-^