目标

  1. 对于几种常见的入参方式,了解如何进行校验以及该如何处理错误消息;
  2. 了解springboot 内置的参数异常类型,并能利用拦截器实现自定义处理;
  3. 能实现简单的自定义校验规则

一、PathVariable 校验

在定义 Restful 风格的接口时,通常会采用 PathVariable 指定关键业务参数,如下:

@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 必须是以大小写字母、数字或下划线组成的字符串。

我们试着访问一个错误的路径:

GET /path/testIllegal.get/10000

此时会得到 404的响应,因此对于PathVariable 仅由正则表达式可达到校验的目的

二、方法参数校验

类似前面的例子,大多数情况下,我们都会直接将HTTP请求参数映射到方法参数上。

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

上面的代码中,@RequestParam 声明了映射,此外我们还为 group 定义了一个规则(复合Email格式)

这段代码是否能直接使用呢?答案是否定的,为了启用方法参数的校验能力,还需要完成以下步骤:

  • 声明 MethodValidationPostProcessor
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
  • Controller指定@Validated注解
@Controller
@RequestMapping("/validate")
@Validated
public class ValidateController {

如此之后,方法上的@Email规则才能生效。

校验异常

如果此时我们尝试通过非法参数进行访问时,比如提供非Email格式的 group

会得到以下错误:

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,会得到:

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=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"
}

三、表单对象校验

页面的表单通常比较复杂,此时可以将请求参数封装到表单对象中,

并指定一系列对应的规则,参考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框架扩展实现;

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

四、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转换过程中已经失败!

五、自定义校验规则

框架内预置的校验规则可以满足大多数场景使用,

但某些特殊情况下,你需要制作自己的校验规则,这需要用到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; }
}

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

六、异常拦截器

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篇" ,期待更多精彩内容-

补习系列(4)-springboot 参数校验详解的更多相关文章

  1. springboot 参数校验详解

    https://www.jianshu.com/p/89a675b7c900 在日常开发写rest接口时,接口参数校验这一部分是必须的,但是如果全部用代码去做,显得十分麻烦,spring也提供了这部分 ...

  2. SpringBoot Validation参数校验 详解自定义注解规则和分组校验

    前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...

  3. Springboot mini - Solon详解(六)- Solon的校验框架使用、定制与扩展

    Springboot min -Solon 详解系列文章: Springboot mini - Solon详解(一)- 快速入门 Springboot mini - Solon详解(二)- Solon ...

  4. SpringBoot系列(十二)过滤器配置详解

    SpringBoot(十二)过滤器详解 往期精彩推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配置文件 ...

  5. DAX/PowerBI系列 - 查询参数用法详解(Query Parameter)

    PowerBI  - 查询参数用法详解(Query Parameter) 很多人都不知道查询参数用来干啥,下面总结一下日常项目中常用的几个查询参数的地方.(本人不太欢hardcode的东西) 使用查询 ...

  6. Springboot mini - Solon详解(三)- Solon的web开发

    Springboot min -Solon 详解系列文章: Springboot mini - Solon详解(一)- 快速入门 Springboot mini - Solon详解(二)- Solon ...

  7. Springboot mini - Solon详解(七)- Solon Ioc 的注解对比Spring及JSR330

    Springboot min -Solon 详解系列文章: Springboot mini - Solon详解(一)- 快速入门 Springboot mini - Solon详解(二)- Solon ...

  8. Spring全家桶——SpringBoot之AOP详解

    Spring全家桶--SpringBoot之AOP详解 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP). OOP中模块化的关键单元是类,而在AOP中,模块化单元是方 ...

  9. Springboot mini - Solon详解(二)- Solon的核心

    Springboot min -Solon 详解系列文章: Springboot mini - Solon详解(一)- 快速入门 Springboot mini - Solon详解(二)- Solon ...

随机推荐

  1. Java 平时作业四

    编写一个Java程序实现返回指定目录及其子目录下扩展名为*.pdf的所有文件名. 扩展: isFile public boolean isFile() 测试此抽象路径名表示的文件是否为普通文件. 如果 ...

  2. MQTT之Mosquitto

    https://mosquitto.org/ Eclipse Mosquitto是一个开源(EPL / EDL许可)消息代理,它实现了MQTT协议版本3.1和3.1.1.Mosquitto重量轻,适用 ...

  3. android BLE Peripheral 做外设模拟设备,供ios、android 连接通讯。

    为了能让其它设备可以发现其设备,先启动特定广播.看自己需要什么广播格式. 对于广播可见的mac address: 在调用startAdvertising();时,mac address 就会改变. 并 ...

  4. JUC笔记

      3个售票员,卖30张票   package com.javase.thread;   import java.util.concurrent.locks.Lock; import java.uti ...

  5. vue-router的学习

    一.路由的概述. vue-router是vue.js官方的路由插件,它和vue.js是深度集成的,适用于构建单页面.vue的单页面应用是基于路由和组件的,路由是用于设定访问路径,并将路径和组件映射起来 ...

  6. PyCharm下载及使用

    PyCharm教育版是一款能够对你编写Python程序的工作有所帮助的免费编译器. PyCharm-community下载链接:https://pan.baidu.com/s/1Hwd_TOVA3en ...

  7. BUAA-OO-第一单元总结

    BUAA-OO第一单元博客总结 第一次作业总结 (1)类关系图 第一次作业类图关系简单,仅有一个Poly封装类以及一个Main主类调用Poly,Poly封装类内部完成了包括对象构造,求导,生成字符串的 ...

  8. windows10下Kafka环境搭建

    内容小白,包含JDK+Zookeeper+Kafka三部分.JDK:1)   安装包:Java SE Development Kit 9.0.1      下载地址:http://www.oracle ...

  9. linux操作命令之压缩命令

    常用的压缩格式:    .zip     .gz     .bz2 一..zip格式压缩 zip 压缩文件名 源文件 压缩文件 zip -r 压缩文件名 源目录 压缩目录 解压缩 unzip 压缩文件 ...

  10. partial 的随笔

    partial class Dmeos { public int Ager { get; set; } public void Run() { Console.WriteLine(Ager); } } ...