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、

  • 相关阅读:
    移动端重构系列-移动端html页面优化
    response项目的各个写法
    收藏功能的写法
    浅谈文本溢出省略号代表修剪text-overflow
    几种display:table-cell的应用
    -webkit-transform:scale(1.04)放大缩小效果
    自学Python5.6-面向对象三大基本特征_多态
    自学Python5.5-面向对象三大基本特征_继承
    自学Python6.5-内置模块(re、collections )
    CISCO SMARTnet服务和SMB服务技术支持
  • 原文地址:https://www.cnblogs.com/luo630/p/15337347.html
Copyright © 2011-2022 走看看