zoukankan      html  css  js  c++  java
  • spring boot项目18:请求参数校验

    Java 8

    Spring Boot 2.5.3

    ---

    授人以渔

    1、Spring Framework官方文档(有PDF下载)

    Core文档下的:Chapter 3. Validation, Data Binding, and Type Conversion

    2、Spring Boot官方文档(有PDF下载)

    章节:4.17. Validation

    本文介绍在Spring Boot应用中对请求参数进行校验。

    正确的数据校验,可以避免脏数据、非法数据写入系统,也可以阻断一些不正确请求的操作。

    Spring框架、Spring Boot对数据校验提供了相应的支持,可以大大简化数据校验过程,除了对请求参数进行检查,还可以对应用中方法的参数进行检查。

    目录

    试验1:硬编码

    试验2:DTO + javax.validation.Valid注解

    试验3:DTO + org.springframework.validation.annotation.Validated注解

    试验4:org.springframework.validation.annotation.Validated注解 到 Controller

    试验5:POST请求的参数校验

    试验6:返回参数校验失败信息给调用方

    方式1:拦截异常

    方式2:使用BindingResult

    试验1:硬编码

    	@GetMapping(value="/hello")
    	public String hello(@RequestParam String name) {
    		// 校验
    		if (!StringUtils.hasText(name)) {
    			// name为空
    			throw new RuntimeException("name不能为空");
    		}
    		final int nameMaxLen = 100;
    		if (name.length() > nameMaxLen) {
    			// 最大长度校验
    			// 抛出异常
    			throw new RuntimeException("name长度超过" + nameMaxLen);
    		}
    		
    		return "Hello, " + name;
    	}

    调用接口 /web/hello(试验Postman),输入触发校验的参数。

    得到下面的响应结果:Internal Server Error

    {
        "timestamp": "2021-09-26T01:59:32.938+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "path": "/web/hello"
    }

    应用日志显示下面的错误:来自博客园

    # @RequestParam 的 required 属性 默认为 true导致
    Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'name' for method parameter type String is not present]
    
    # 代码中校验失败抛出异常
    java.lang.RuntimeException: name不能为空
    java.lang.RuntimeException: name长度超过100

    试验2:DTO + javax.validation.Valid注解

    为了方便参数校验,Spring 框架整合了数据校验的功能——不仅仅包含请求参数校验,通过使用注解大大简化参数的校验。

    在S.B.应用中,添加下面的依赖包即可使用相关功能:spring-boot-starter-validation

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    注,暂时没有找到不使用DTO即可对Get请求的参数进行校验的方式。

    改造接口:

    这里的NameDTO前面没有 @RequestParam等注解;来自博客园

    使用 javax.validation.Valid 注解;

    	@GetMapping(value="/hello2")
    	public String hello2(@Valid NameDTO dto) {
    		return "Hello, " + dto.getName();
    	}

    添加NameDTO:

    // @Data 为 lombok注解
    @Data
    public class NameDTO {
    
    	@NotBlank(message="name不能为空")
    	@Size(max=100, message="name长度不能超过100")
    	private String name;
    	
    }

    调用 接口 /web/hello2,输入触发校验的参数。得到下面的响应:之前因为抛出异常,status是500,现在变为400,更符合参数校验失败状态码了。来自博客园

    {
        "timestamp": "2021-09-26T02:51:05.988+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web/hello2"
    }

    应用错误日志如下:出现异常 org.springframework.validation.BindException,和之前的不同了

    # 不输入任何参数
    .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotEmpty.nameDTO.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]
    
    # 输入参数 name为空或由空字符组成
    Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'nameDTO' on field 'name': rejected value [     ]; codes [NotBlank.nameDTO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]
    
    # 输入参数 name长度超过100
    Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'nameDTO' on field 'name': rejected value [...省略参数值...]; codes [Size.nameDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name],100,0]; default message [name长度不能超过100]]

    Valid注解 源码:来自博客园

    @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface Valid {
    }

    试验3:DTO + org.springframework.validation.annotation.Validated注解

    改造接口: 把 试验2 的 @Valid 改为 @Validated

    	@GetMapping(value="/hello3")
    	public String hello3(@Validated NameDTO dto) {
    		return "Hello, " + dto.getName();
    	}

    调用 接口 /web/hello3,输入触发校验的参数。得到下面的响应:和试验2相同

    {
        "timestamp": "2021-09-26T03:03:20.457+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web/hello3"
    }

    应用的错误日志:和试验2相同(下面仅展示其中1条)来自博客园

    # 其中一条错误日志
    Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotBlank.nameDTO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]]

    Validated 注解源码:

    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Validated {
    
    	Class<?>[] value() default {};
    
    }

    试验4:org.springframework.validation.annotation.Validated注解 到 Controller

    前面提到,没有找到直接在 请求方法的参数中使用校验注解进行校验的方法,现在,找到了!

    改造接口:

    /web2/ 开头的接口;

    @Validated 的位置,在Controller类上,而不是 方法上;

    @RestController
    @RequestMapping(value="/web2")
    @Validated
    public class Web2Controller {
    
    	@GetMapping(value="/hello")
    	public String hello(@NotBlank(message="name不能为空") @Size(max=100, message="name长度超过100") String name) {
    		return "Hello, " + name;
    	}
    	
    }

    调用 /web2/hello 接口,输入触发校验的规则。

    得到的响应如下:status不是 400,又变成 500了。

    {
        "timestamp": "2021-09-26T03:18:40.083+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "path": "/web2/hello"
    }

    应用的错误日志:此时的异常 是 javax.validation.ConstraintViolationException

    # 输入name长度超过100
    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: hello.name: name长度超过100] with root cause
    javax.validation.ConstraintViolationException: hello.name: name长度超过100
    	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.9.jar:5.3.9]
    
    # 不输入name 或 输入name由空字符组成
    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: hello.name: name不能为空] with root cause
    javax.validation.ConstraintViolationException: hello.name: name不能为空
    	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.9.jar:5.3.9]
    

    在本试验中,没有给参数name加上@RequestParam,因此,不传name时,提示的是 “name不能为空”。

    加上@RequestParam会怎样?

    # 没有name参数
    GET localhost:8080/web2/hello
    
    # 响应 400
    {
        "timestamp": "2021-09-26T03:43:26.725+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web2/hello"
    }
    
    # 应用日志 WARN级别
    Resolved [org.springframework.web.bind.MissingServletRequestParameterException: 
    Required request parameter 'name' for method parameter type String is not present]

    加上了是有效的,这样的话,就不影响了——在前面 试验2、3 中使用了 DTO方式,那时是不能加 @RequestParam 注解的,否则要传入的是参数 dto。

    Get请求多参数(2个)校验

    开发 /web2/hello2 接口:新增参数 age

    	@GetMapping(value="/hello2")
    	public String hello2(@RequestParam @NotBlank(message="name不能为空") @Size(max=100, message="name长度超过100") String name,
    			@RequestParam @Min(value=0, message="age必须大于等于0") @Max(value=150, message="age必须小于等于150") Integer age) {
    		return "Hello, " + name + ", you are " + age;
    	}

    校验结果:符合预期。

    GET请求的参数放入DTO中

    试验4这种校验方式 可以使用 @RequestParam注解,但是,在方法签名中给每个参数添加注解显得比较臃肿,将这些参数及校验注解放到DTO中,会让接口显得很清爽

    采用试验4 这种 @Validated 放到Controller类上,如下面这样改造接口,测试失败——未执行校验:

    	// Web2Controller.java
        @GetMapping(value="/hello3")
    	public String hello3(Name2DTO dto) {
    		return "Hello, " + dto.getName() + ", you are " + dto.getAge();
    	}
    
    // Name2DTO.java
    @Data
    public class Name2DTO {
    
    	@NotBlank(message="name不能为空")
    	@Size(max=100, message="name长度不能超过100")
    	private String name;
    	
    	@NotNull(message="age不能为null")
    	@Min(value=0, message="age必须≥0")
    	@Max(value=150, message="age必须≤150")
    	private Integer age;
    	
    }

    上面的接口未执行校验:

    GET localhost:8080/web2/hello3
    
    响应:
    Hello, null, you are null

    前面试验2、试验3中,给dto参数直接添加 @Validated、@Valid 可以进行校验,这里是否可以呢?

    改造:2、3都是可行的——执行了指定的校验,并且响应的status为400,符合预期。

    @GetMapping(value="/hello3")
    	// 1、未做校验
    //	public String hello3(Name2DTO dto) {
    	// 2、@Validated 做了校验,返回400
    //	public String hello3(@Validated Name2DTO dto) {
    	// 3、@Valid 做了校验,返回400
    	public String hello3(@Valid Name2DTO dto) {
    		return "Hello, " + dto.getName() + ", you are " + dto.getAge();
    	}

    疑问:2、3两种方式都可以,两者有什么区别呢?TODO

    GET请求试验DTO方式时,没有使用@RequestParam注解,此时,除了请求参数可以放到url中,还可以放到form表单中。

    怎么限制——不允许表单方式提交数据——呢?TODO

    试验配置 @GetMapping 的 consumes=“text/plain”,但提交Get请求时失败了:

    [org.springframework.web.HttpMediaTypeNotSupportedException: Content type '' not supported]

    ---210926 1221---

    试验5:POST请求的参数校验

    其实和前面GET请求的校验一样,只不过,请求参数是DTO形式,并且参数dto使用了@RequestBody注解。

    // WebController.java
        @PostMapping(value="/hello4")
    	public String hello4(@RequestBody @Validated Hello4DTO dto) {
    		return "Hello, " + dto.getName();
    	}
    
    // Hello4DTO.java
    @Data
    public class Hello4DTO {
    
    	@NotBlank(message="name不能为空")
    	@Size(max=100, message="name长度不能超过100")
    	private String name;
    	
    }

    调用 /web/hello4 接口,使用POST,传入错误的参数触发校验:

    产生 org.springframework.web.bind.MethodArgumentNotValidException 异常,这和 试验2、3的GET请求时不同(之前是BindException) 

    POST localhost:8080/web/hello4
    参数:
    {
        "name": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"
    }
    响应:
    {
        "timestamp": "2021-09-26T05:11:59.298+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web/hello4"
    }
    错误日志:
    Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public 
    java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO): 
    [Field error in object 'hello4DTO' on field 'name': rejected value [    ]; codes [NotBlank.hello4DTO.name,NotBlank.name,
    NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes 
    [hello4DTO.name,name]; arguments []; default message [name]]; default message [name不能为空]] ]
    
    Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public 
    java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO): 
    [Field error in object 'hello4DTO' on field 'name': rejected value [..省略...]; codes [Size.hello4DTO.name,Size.name,
    Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: 
    codes [hello4DTO.name,name]; arguments []; default message [name],100,0]; default message [name长度不能超过100]] ]

    试验6:返回参数校验失败信息给调用方

    在前面的试验中,返回给调用方的信息是400、500等,没有提示到底出了什么错误。

    本节介绍两种方式来将具体的参数错误信息返回给调用方

    本节仅处理 试验2、3 和 试验5 的异常(org.springframework.validation.BindException、org.springframework.web.bind.MethodArgumentNotValidException)。

    检查发现,MethodArgumentNotValidException 继承了 BindException:

    方式1:拦截异常

    拦截参数校验中的异常,再将异常的信息返回给调用方。

    第一次尝试:返回信息太多,不符合预期

    // AppExceptionHandler.java
    // 方式1:1个注解
    //@RestControllerAdvice
    // 方式2:2个注解
    @ControllerAdvice
    @ResponseBody
    @Slf4j
    public class AppExceptionHandler {
    
    	@ExceptionHandler(value = {BindException.class})
    	public String handleRequestValid(BindException be) {
    		log.warn("请求参数异常:be={}, {}", be.getClass(), be.getMessage());
    		return "参数异常:" + be.getMessage();
    	}
    }
    
    测试 GET localhost:8080/web/hello3,响应:
    参数异常:org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'nameDTO' on field 'name': rejected value [null]; codes [NotBlank.nameDTO.name,NotBlank.name,
    NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: 
    codes [nameDTO.name,name]; arguments []; default message [name]]; default message [name不能为空]

    第二次尝试:改造handleRequestValid函数的返回值

    	@ExceptionHandler(value = {BindException.class})
    	public String handleRequestValid(BindException be) {
    		log.warn("请求参数异常:be={}, {}", be.getClass(), be.getMessage());
    		
    		BindingResult br = be.getBindingResult();
    		List<ObjectError> oel = br.getAllErrors();
    		StringBuffer sb = new StringBuffer();
    		sb.append("参数错误:");
    		oel.forEach(oe->{
    			sb.append(oe.getDefaultMessage() + ";");
    		});
    		return sb.toString();
    	}

    此时访问 localhost:8080/web/hello3,返回:错误信息总算出来了

    参数错误:name不能为空;

    访问 localhost:8080/web/hello4:

    - 不传 @请求体 时,返回 错误信息,日志发生 HttpMessageNotReadableException 异常:还需要完善异常拦截

    {
        "timestamp": "2021-09-26T05:51:00.154+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web/hello4"
    }
    
    此时的日志错误:
    Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: 
    public java.lang.String org.lib.webvalidation.controller.WebController.hello4(org.lib.webvalidation.dto.Hello4DTO)]

    - 传入请求体,但没有任何参数:返回了 参数错误的信息,符合预期

    POST localhost:8080/web/hello4
    参数:
    {
    }
    响应:
    参数错误:name不能为空;

    要是存在多个参数存在错误呢?上面的拦截方式会把 所有参数的错误信息返回。

    // 增加参数
    @Data
    public class Hello4DTO {
    
    	@NotBlank(message="name不能为空")
    	@Size(max=100, message="name长度不能超过100")
    	private String name;
    	
    	@NotNull(message="age不能为null")
    	@Range(min=0, max=150, message="age范围:[0,150]")
    	private Integer age;
    	
    }
    
    // 更新接口返回值
    	@PostMapping(value="/hello4")
    	public String hello4(@RequestBody @Validated Hello4DTO dto) {
    		return "Hello, " + dto.getName() + ", you are " + dto.getAge();
    	}

    执行 传入请求体,但没有任何参数,返回:两个参数的错误原因都返回给调用方了

    参数错误:age不能为null;name不能为空;

    不拦截 HttpMessageNotReadableException等异常 

    这是由于 @RequestBody 默认的 required=true 导致的——/web/hello4 没有传请求体。

    这时要怎么拦截呢?拦截了要返回什么信息呢?

    在使用 @RequestParam 时,此时不传参数,会产生下面的异常:MissingServletRequestParameterException

    Resolved [org.springframework.web.bind.MissingServletRequestParameterException: 
    Required request parameter 'name' for method parameter type String is not present]

    上面两种异常都要拦截的话,怎么做? 

    从两者的类继承来看,几乎没有关系,要一个一个处理吗?

    public class HttpMessageNotReadableException extends HttpMessageConversionException {
    
    public class MissingServletRequestParameterException extends MissingRequestValueException {

    除了上面的 @RequestBody、 @RequestParam会导致异常外,还有其它几个,每一个都要处理吗?代码就太多了

    这种情况可以不处理,不拦截。

    已经要求传参数了,可调用方就是不传,发生了错误,就返回错误信息好了

    {
        "timestamp": "2021-09-26T06:09:12.663+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/web/hello4"
    }

    方式2:使用BindingResult

    前面使用拦截异常控制了返回的请求,其中,返回的信息来自异常的一个BindingResult对象。

    也可以在方法中直接使用 BindingResult来返回具体校验信息。

    注意,使用方法2时,先注释掉 方法1 的 AppExceptionHandler。

    新增接口:/web/hello5,参数中增加 BindingResult bresult

    @PostMapping(value="/hello5")
    	public String hello5(@RequestBody @Validated Hello4DTO dto, BindingResult bresult) {
    		if (bresult.hasErrors()) {
    			// 参数校验错误处理:返回所有错误信息
    			StringBuffer sb = new StringBuffer();
    			List<ObjectError> oel = bresult.getAllErrors();
    			sb.append("API中-参数错误:");
    			oel.forEach(oe->{
    				sb.append(oe.getDefaultMessage() + ";");
    			});
    			return sb.toString();
    		}
    		
    		return "Hello, " + dto.getName() + ", you are " + dto.getAge();
    	}

     调用结果:实现了校验,符合预期。

    关于BindingResult更多原理性的东西,可以看官文。来自博客园

    对了,除了返回所有校验错误信息外,也可以只返回一条错误信息。

    方式1、方式2 同时使用时,返回哪个的信息呢?

    方式2的!

    因为此时参数校验失败的异常已经被处理了,不会抛出到 全局异常拦截层。来自博客园

    更多参数校验注解

    前面使用了 @NotBlank、@Size 等注解,还有哪些注解可以使用呢?

    上面的注解来自 javax.validation包,在前面使用的 @Range注解 则来自 org.hibernate.validator.constraints 包,这个包下有哪些用来做参数校验的注解呢?

    除了上面的两个包中的注解,是否还有其它Spring框架自带的注解呢?TODO

    是否可以自定义校验注解呢?TODO

    是否可以自定义校验规则——非注解方式——呢?TODO

    关于这些问题,需要看看官方文档,里面还有更详细的介绍。来自博客园

    对于参数校验,还需要知道的是,spring框架是如何把 入参和参数做绑定的,官文中也有详细的介绍。

    》》》全文完《《《

    参考文档

    1、SpringBoot 参数校验的方法

    作者: 木白的菜园

    2、Spring基础系列-参数校验

    作者: 唯一浩哥

    3、

  • 相关阅读:
    array_map()与array_shift()搭配使用 PK array_column()函数
    Educational Codeforces Round 8 D. Magic Numbers
    hdu 1171 Big Event in HDU
    hdu 2844 poj 1742 Coins
    hdu 3591 The trouble of Xiaoqian
    hdu 2079 选课时间
    hdu 2191 珍惜现在,感恩生活 多重背包入门题
    hdu 5429 Geometric Progression 高精度浮点数(java版本)
    【BZOJ】1002: [FJOI2007]轮状病毒 递推+高精度
    hdu::1002 A + B Problem II
  • 原文地址:https://www.cnblogs.com/luo630/p/15337347.html
Copyright © 2011-2022 走看看