zoukankan      html  css  js  c++  java
  • 补习系列(4)-springboot 参数校验详解

    目录

    目标

    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
     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 注解

    @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() + "
    ");
            }
            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篇" ,期待更多精彩内容^-^、

    作者:美码师

  • 相关阅读:
    C. Shaass and Lights 解析(思維、組合)
    D. Binary String To Subsequences(队列)(贪心)
    CodeForces 1384B2. Koa and the Beach (Hard Version)(贪心)
    CodeForces 1384B1. Koa and the Beach (Easy Version)(搜索)
    CodeForces 1384C. String Transformation 1(贪心)(并查集)
    CodeForces 1384A. Common Prefixes
    POJ-2516 Minimum Cost(最小费用最大流)
    POJ3261-Milk Patterns(后缀数组)
    HDU-1300 Pearls(斜率DP)
    HDU-4528 小明系列故事-捉迷藏(BFS)
  • 原文地址:https://www.cnblogs.com/2020-zhy-jzoj/p/13165593.html
Copyright © 2011-2022 走看看