前言部分主要是一些说明,会有点啰嗦,若是着急,自行选择跳过。
0. 前言
日常业务开发中,有时不可避免的需要进行请求参数的校验,对于一些与业务无关的、简单的参数校验(如非空校验、长度限制等),则可以采用javax.validation 和 hibernate-validator通过注解的方式实现校验。
web开发中,前端的参数校验是为了用户体验,而后端的参数校验更多是为了安全,防止非法参数对业务造成影响。
此外,入参校验其实也是后端代码规范的一个重要体现。
1)@Validated和@Valid的区别
SpringBoot中常用的校验注解是@Validated和@Valid,@Validated是对@Valid进行了二次封装,它们的区别如下
不同 | @Valid | @Validated |
---|
来源 | Hibernate validation 提供的校验注解 | Spring Validator 提供的校验注解,是 Hibernate validation 基础上的增强版 |
注解位置 | 可用在构造函数、方法、方法参数 和 成员属性上 | 用在 类、方法和方法参数上。但不能用于成员属性 |
嵌套验证 | 可用在级联对象的成员属性上面(后面会有使用示例) | 不支持 |
分组 | 无此功能 | 提供分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制 |
可以看到,@Validated和@Valid两者功能上最大的不同就是嵌套验证和分组的功能,除此之外,它们的功能差不多。因此在下面的代码示例中,将发现有些地方用@Validated或@Valid,实现的入参校验效果是一样的。
2)Hibernate validation与Spring Validator区别
- javax的validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),已经经历了三个版本。
需要注意的是,Java API规范 (JSR303) 只是定义了Bean校验的标准validation-api,它规定了一些校验注解的规范,但没有提供实现!比如@Null、@NotNull、@Pattern等,它们位于 javax.validation.constraints这个包下。
- Hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如 @NotBlank、@NotEmpty、@Length等,它们位于org.hibernate.validator.constraints这个包下。
- 而Spring Validator又是对Hibernate validation的二次封装,用于支持spring mvc参数自动校验。
3)常用的校验注解
这里主要介绍在SpringBoot中的几种参数校验方式。常用的用于参数校验的注解如下:
注解 | 说明 | 适用类型 |
---|
@Null | 所注解的元素值为null | 所有对象 |
@NotNul | 所注解的元素值不能为null(注意:postman测试时,无法发送null的参数) | 所有对象 |
@NotBlank | 被注解的元素不能为null且trim()之后的size大于0 | String |
@NotEmpty | 被注解的元素不能为null或者长度为0的(String Collection Map的isEmpty()方法) | String、集合、数组 |
@Size | 所注解元素的长度大小需保证在给定范围[n,m]之内,如@Size(min=1, max=10) | String、集合、数组 |
@AssertFalse | 所注解的元素值为false | Boolean |
@AssertTrue | 所注解的元素值为true | Boolean |
@Past | 所注解的元素必须是过去的日期 | 日期 |
@PastOrPresent | 所注解的元素必须是过去或现在日期 | 日期 |
@Future | 所注解的元素必须是将来的日期 | 日期 |
@FutureOrPresent | 所注解的元素必须是现在或将来的日期 | 日期 |
@Digits | 所注解的元素的值必须是指定的位数。如@Digits(integer=3,fraction=2)表示数字最多可以有3位整数部分,最多可以有2位小数部分 | 数字 |
@Min | 所注解的元素必须小于等于给定的值 | 数字 |
@Max | 所注解的元素必须大于等于给定的值 | 数字 |
@DecimalMin | 所注解的元素必须大于等于给定的浮点数 | 数字 |
@DecimalMax | 所注解的元素必须小于等于给定的浮点数` | 数字 |
@Negative | 所注解的元素必须是负整数 | 数字 |
@NegativeOrZero | 所注解的元素必须是负整数或零 | 数字 |
@Positive | 所注解的元素必须是正整数 | 数字 |
@PositiveOrZero | 所注解的元素必须是正整数或零 | 数字 |
@Range | 检查数字是否在范围之间,如@Range(min=1, max=100, message="ID必须在1到100之间") | 数字 |
@Pattern | 所注解的元素必须满足给定的正则表达式 | String |
@Email | 所注解的元素需满足Email格式 | String |
4)接口定义方式
下文将结合接口的几种定义方式,来看如何在定义SpringBoot接口的时候,做参数校验。
在文章【SpringBoot接收接口入参的几种方式】中,是从调用方的角度来看在调用接口时有哪几种传参方式。本文则是从接口定义方式的角度,去看看如何在Controller中书写接口方法,同时合理添加对应的参数校验。
- 接口定义方式1:在方法形参的位置,把每个参数平铺开来
- 接口定义方式2:用占位符的方式把入参封装到请求路径中
- 接口定义方式3:把入参封装到一个实体中
- 接口定义方式4:用原生的HttpServletRequest接收参数
- 接口定义方式5:用Map来接收入参
由于定义方式4和方式5,都不方便结合@Validated或@Valid进行入参的校验,所以本文将在最后进行简单的介绍即可。
5)关于代码的说明
5.1) 如何引入校验的相关pom依赖?
如果SpringBoot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖(实测2.0.4.RELEASE和2.1.9.RELEASE版本是这样的)。如果Springboot版本大于2.3.x,则需要手动引入依赖。
javax.validation
validation-api
2.0.1.Final
org.hibernate.validator
hibernate-validator
6.2.0.Final
或者,直接在pom.xml中引入springboot的starter场景启动器(推荐这种方式):
org.springframework.boot
spring-boot-starter-validation
5.2)关于入参校验时的异常捕获
在接口定义时如果加入了入参校验,那么当调用方在调用接口时如果传入的参数不符合校验规则,一般来说后端代码是会抛出相应的异常的,并且返回给到调用方的结果也会不太友好,如下所示:
从后端代码的控制台,可以看到抛出了相应的异常,下图是示例:
如果你希望你的接口无论如何都返回一个友好的返回结果格式,如下所示:
想要实现这样的效果,可以利用@ControllerAdvice+@ExceptionHandler的方式,捕获对应的异常,然后在捕获异常的方法内返回相应的ResultVO对象。
下面是我利用@ControllerAdvice+@ExceptionHandler,做的关于入参校验的异常捕获处理,仅供参考:
package com.su.demo.exception;
import com.su.demo.bean.dto.*;
import com.su.demo.bean.vo.base.ResultCodeEnum;
import com.su.demo.bean.vo.base.ResultVO;
import com.su.demo.controller.param_validate.ParameterValidateController;
import com.su.demo.controller.param_validate.CheckParamController2;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 用统一异常处理来返回一个更友好的提示
*
比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。
*
*
当参数校验异常的时候,该统一异常处理类在控制台打印信息的同时把bad request的字符串和HttpStatus.BAD_REQUEST所表示的状态码400返回给调用方(用@ResponseBody注解实现,表示该方法的返回结果直接写入HTTP response body 中)。其中:
*
* - {@link ControllerAdvice}控制器增强,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。
* - {@link ExceptionHandler}异常处理器,此注解的作用是当出现其定义的异常时进行处理的方法,此例中处理ValidationException异常。
*
*
*
*/
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
// 参数校验异常1
/**
*
该ExceptionHandler对应的情况:接口定义处添加了如{@link javax.validation.constraints.NotNull}等校验注解,但是调用接口时却没有传参
*
此时接口会报{@link javax.validation.ConstraintViolationException}异常且返回如下的
* 不友好的结果 给接口调用方
*
{
*
"timestamp": "2024-01-31T06:07:15.183+00:00",
*
"status": 500,
*
"error": "Internal Server Error",
*
"path": "/param_validate/parameter/test2"
*
}
*
参考接口:{@link ParameterValidateController#test2(String, Integer, Date, HttpServletRequest)}
*/
@ExceptionHandler(javax.validation.ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 加上这个之后,返回的状态码就不是200了,而是400 Bad Request
public ResultVO constrainViolationException(javax.validation.ConstraintViolationException e) {
// return e.getMessage(); // 这种方式只返回第一个错误(其实也可以)
// 下面这种方式,可以返回所有字段的查检不通过的结果
String res = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, res);
}
/**
*
该ExceptionHandler对应的情况,当如下接口定义中必填参数不传或者传入的参数格式错误时,报当前exception
*
参考接口:{@link com.su.demo.controller.param_validate.EntityValidateController#test2_validated_requestbody(User11, HttpServletRequest)}
*/
@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO methodArgNotValidException(org.springframework.web.bind.MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
if (!bindingResult.hasErrors()) {
e.printStackTrace();
return new ResultVO<>(ResultCodeEnum.IllEGAL_PARAMETER, "系统异常,入参校验失败");
}
/**方式 1 */
// return "接口入参校验2:" + bindingResult.getFieldError().getDefaultMessage(); // 这种方式只返回第一个错误(其实也可以)
/**方式 2 */
// 下面这种方式,可以返回所有字段的查检不通过的结果
// String res = bindingResult.getAllErrors().stream()
// .map(ObjectError::getDefaultMessage)
// .collect(Collectors.joining(";"));
// return "接口入参校验2:" + res;
/**方式 3 */
String res2 = bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
return new ResultVO<>(ResultCodeEnum.IllEGAL_PARAMETER, res2);
}
/**
*
该ExceptionHandler对应的情况:调用接口时请求类型错误
*
此时如果不进行对应的异常处理,则会报{@link org.springframework.web.HttpRequestMethodNotSupportedException}异常且返回如下的
* 不友好的结果 给接口调用方
*
{
*
"timestamp": "2024-01-02T09:06:04.511+0000",
*
"status": 405,
*
"error": "Method Not Allowed",
*
"message": "Request method 'POST' not supported",
*
"path": "/test_response_advice/string"
*
}
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ResultVO methodNotSupportException(HttpRequestMethodNotSupportedException e) {
return new ResultVO(ResultCodeEnum.METHOD_NOT_ALLOWED, "接口仅支持如下请求方式: " + Arrays.toString(e.getSupportedMethods()));
}
/**
*
该ExceptionHandler对应的情况:在调用【入参为格式requestBody】的接口时:
*
* - 1) 不传body请求体
* - 2) 或传的body请求体格式错误,如:{ "age": 130, ""}
* - 3) 或传的body请求体内的日期格式错误
*
* 此时如果不进行对应的异常处理,则会报{@link org.springframework.http.converter.HttpMessageNotReadableException}异常且返回如下的
*
参考方法 {@link com.su.demo.controller.param_validate.EntityValidateController#test3_valid_requestbody(User11, HttpServletRequest)}
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO httpMsgNotReadableExp(HttpMessageNotReadableException e) {
return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, e.getMessage());
}
/**
*
该ExceptionHandler对应的情况:在调用【入参设置为如(@RequestParam("name") String name)】的接口时 ,入参name却不传(为null)的时候报的异常
*
因为 {@link RequestParam} 注解的required默认是true,因此被这个注解标记的入参如果为空,
* 则会报{@link org.springframework.web.bind.MissingServletRequestParameterException}异常
*
如果不做此异常捕获处理,则接口返回如下的 不友好的结果 给调用方:
*
{
*
"timestamp": "2023-12-29T03:10:57.961+0000",
*
"status": 400,
*
"error": "Bad Request",
*
"message": "Required String parameter 'name' is not present",
*
"path": "/param_type/get_reqestparam"
*
}
*
参考方法: {@link ParameterValidateController#test1(String, Integer, HttpServletRequest)}
*/
@ExceptionHandler(org.springframework.web.bind.MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO missingRequestParamException(MissingServletRequestParameterException e) {
e.printStackTrace();
return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, e.getMessage());
}
/**
*
该ExceptionHandler对应的是类似如下的情况入参类型错误:
*
* - 1) 字段age类型是Integer,但是入参却是test这样的字符串
* - 2) 字段enable类型是Boolean,但是入参却是666这样的数值
* - 3) 字段birth类型是Date且@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"),但是入参却是20230101这样的错误格式
* - 4) 等等
*
*
参考方法: {@link ParameterValidateController#test2(String, Integer, Date, HttpServletRequest)}
*/
@ExceptionHandler(org.springframework.web.method.annotation.MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO methodArgTypeMismatchException(MethodArgumentTypeMismatchException e) {
// e.printStackTrace();
String fieldName = e.getName();
String errorStr = e.getCause().toString();
HashMap map = new HashMap<>();
map.put(fieldName, errorStr);
// return "参数类型(格式)错误:" + fieldName + ": " + errorStr;
return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, map);
}
/**
* "BindException" 错误通常是以下原因引起的:
*
* - 绑定参数错误:如果您的绑定参数错误,则可能会出现此错误。在这种情况下,您需要检查您的绑定参数并确保它们正确。
* - 绑定参数类型不正确:如果传入的参数类型不正确,则可能会出现此错误
*
* 参考方法 {@link com.su.demo.controller.param_validate.EntityValidateController#test2_validated(User1, HttpServletRequest)}
*/
@ExceptionHandler(org.springframework.validation.BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO
那为了满足上面的需求,我们在定义User3对象的时候,就可以使用上@Validated 提供的分组功能了。
如上面代码所示,可以在id字段的@NotNull注解中,设置它的groups属性:@NotNull(message = "用户id不能为空", groups = OnUpdate.class),这个就表示只有当groups = OnUpdate.class时,才会应用该@NotNull校验注解。
同样的也可以自行去关注上面其他字段的分组设置,如remark字段的@NotNull注解使用的是groups = OnSave.class分组。
在定义好上面的bean对象之后,我们在定义接口时,做如下操作:
- 在定义保存接口的时候,使用OnSave分组的校验规则:@Validated(User3.OnSave.class)
- 在定义修改接口的时候,使用OnUpdate分组的校验规则:@Validated(User3.OnUpdate.class)
示例代码如下:
package com.su.demo.controller.param_validate;
import com.su.demo.bean.dto.*;
import com.su.demo.validate.CompanyEmail;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
@Validated
@RestController
@RequestMapping("/param_validate/entity")
public class EntityValidateController {
/**
* 在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,
* 而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。
* 因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存User的时候,
* UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。
*/
/**
* 校验分组 OnSave
* save时,校验的规则是OnSave,要求必须填写name和remark,但是id可以为空
*/
@RequestMapping(value = "test6_group_save")
public String test6_group_save(@RequestBody @Validated(User3.OnSave.class) User3 user3, HttpServletRequest request) {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
System.out.println("CheckParamController.test_e1_save ---> " + user3);
return requestMetaInfo + "===> SUCCESS! ---> " + user3;
}
/**
* 校验分组 OnUpdate
* update时,校验的规则是OnUpdate,要求必须填写id和name,但是remark可以为空
*/
@RequestMapping(value = "test6_group_update")
public String test6_group_update(@RequestBody @Validated(User3.OnUpdate.class) User3 user3, HttpServletRequest request) {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
System.out.println("CheckParamController.test_e1_update ---> " + user3);
return requestMetaInfo + "===> SUCCESS! ---> " + user3;
}
}
上面提供了两个接口:
第1个接口是保存接口,使用OnSave分组:@Validated(User3.OnSave.class);
第2个接口是修改接口,使用OnUpdate分组:@Validated(User3.OnUpdate.class);
并且注意,此时上面接口定义时,就只能使用@Validated了,不能使用@Valid,因为@Valid不提供分组的功能!
3.4.1 用例1:测试OnSave分组
调用接口时如果传了id字段,或者没有传remark字段, 则调用失败
3.4.2 用例2:测试OnUpdate分组
调用接口时如果不传id字段, 则调用失败
3.5(自定义校验) @RequestBody + @Validated(或@Valid) + 实体,接收入参
有时候,如果内置的校验注解不能满足我们的需求,那我们可以自行定义校验用的注解,作为示例,下面我将自定义一个用于校验指定邮箱格式的注解,要求邮箱格式必须是QQ邮箱格式:
先定义一个名为CompanyEmail的注解:
package com.su.demo.validate;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义的【QQ邮箱】格式校验的注解
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CompanyEmailValidator.class})
public @interface CompanyEmail {
String message() default "必须是QQ邮箱,如123456@qq.com";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
注意上面代码,跟普通的注解不一样的是,我们的这个注解中还通过@Constraint(validatedBy = {CompanyEmailValidator.class}),去绑定它对应的验证器为CompanyEmailValidator,验证器CompanyEmailValidator中就定义了具体的验证规则!
其中CompanyEmailValidator的代码如下:
package com.su.demo.validate;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义的QQ邮箱的的验证器
*/
public class CompanyEmailValidator implements ConstraintValidator {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 超简单的验证,如果value不为空且以@qq.com结尾,则返回true
return null != value && value.endsWith("@qq.com");
}
}
接下来,在接口中使用该自定义的校验注解:
package com.su.demo.controller.param_validate;
import com.su.demo.bean.dto.*;
import com.su.demo.validate.CompanyEmail;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
@Validated
@RestController
@RequestMapping("/param_validate/entity")
public class EntityValidateController {
/**
* 使用自定义的校验
*
必须在Controller上添加{@link Validated}注解,否则自定义的校验不起作用
*/
@RequestMapping(value = "/test7_custom_validator")
public String test7_custom_custom_validator(@CompanyEmail String email, HttpServletRequest request) {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
return requestMetaInfo + "===> SUCCESS! email = " + email;
}
}
在入参email前面加上了自定义的@CompanyEmail注解
3.5.1 用例
email入参不是QQ邮箱,调用失败
3.6快速失败校验配置
在上面的代码示例中,可以看到会校验所有字段,并将所有不符合校验规则的字段检测出来。这种校验策略是实体类参数校验的普通校验模式(默认的校验模式)。
实际上有两种校验模式:
- 普通模式(默认是这个模式): 会校验完所有的属性,然后返回所有的验证失败信息
- 快速失败模式:只要有一个验证失败,则返回。
如果想要配置第二种模式快速失败模式,需要添加如下配置类
package com.su.demo.config;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* 在上面实体类参数校验的例子中,可以看到会校验所有字段,并将所有不符合校验规则的字段打印出来。这种校验策略是实体类参数校验的普通校验模式(默认的校验模式)。实际上有两种校验模式:
*
①普通模式(默认是这个模式): 会校验完所有的属性,然后返回所有的验证失败信息
*
②快速失败模式: 只要有一个验证失败,则返回。
*
*
如果想要配置第二种模式,需要添加如下配置类
*/
@Configuration
public class ValidateConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast( true ) // 快速失败模式: 只要有一个验证失败,则返回
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
添加这个配置类之后,重启项目,此时即便入参中有多个入参都不符合校验规则,当程序检测到第一个验证失败,就立即返回了,不再继续检测其他字段了:
快速失败模式可以根据自己项目的实际需求,决定是否配置
4. 方式4:用原生的HttpServletRequest接收参数
用原生的HttpServletRequest,既可以接收query入参,也可以接收body入参。
但是这种用原生的HttpServletRequest来接收入参方式,没有办法结合@NotNull等校验注解,对入参进行校验。
如果一定想要进行入参的校验,那就只能在代码中手动的进行判断了。
因此,一般都很少用这种方式来接收入参了。
package com.su.demo.controller.param_validate;
import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson.JSON;
import com.su.demo.bean.dto.*;
import com.su.demo.bean.vo.base.ResultCodeEnum;
import com.su.demo.bean.vo.base.ResultVO;
import com.su.demo.validate.CompanyEmail;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.IOException;
@Validated
@RestController
@RequestMapping("/param_validate/entity")
public class EntityValidateController {
/**
*
* - 入参类别:body参数方式
*
- 定义方式: 在方式形参的位置,用原生的{@link HttpServletRequest}接收
*
- 调用方式: 参数放在请求体 body 当中
*
- 兼容的请求方式:GET POST PUT DELETE
* - 适用场景:从技术上来说,这种传参方式同时适用于GET POST PUT DELETE等请求方式,但是正常情况下比较少用这种方式来获取body参数了
* - 优点:{@link HttpServletRequest}是整个请求,可以获取到所有的数据.且HttpServletRequest、HttpServletResponse都是内置对象,可以使用
* - 缺点:代码中再去从request中拿到参数,老麻烦,并且还没有办法直接将校验的注解应用过来
*
*/
@RequestMapping(value = "/test10_request")
public ResultVO test10(HttpServletRequest request) throws IOException {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
ServletInputStream inputStream = request.getInputStream();
// 用hutool工具包的read方式,将inputStream读取成string
String body = IoUtil.read(inputStream, "UTF-8");
System.out.println("body = " + body);
// 用fastjson将json字符串转换成bean
User1 user = JSON.parseObject(body, User1.class); //虽然User1的属性上加了@NotNull等注解,但是并不起作用
// 只能在代码中手动校验
// if (null == user.getName()) {
// return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, "入参name不能为空");
// }
return new ResultVO(requestMetaInfo + " ---> SUCCESS! user = " + user);
}
}
上面代码示例是用原生的HttpServletRequest来接收body中的application/json入参,其实HttpServletRequest同样也可以用来接收query入参,只需要在代码中通过request.getParameter("参数名称")的方式来获取入参即可。
为了节约篇幅,这里就不再举例了。
用例:
5. 方式6:用String结合@RequestBody来接收body入参
这种方式的示例代码如下,它跟上面那种方式一样的缺点——没有办法结合@NotNull等校验注解,对入参进行校验。
因此,一般也很少用这种方式来接收入参。
/**
* 直接用一个String接收body参数
*/
@RequestMapping(value = "/test11_str")
public ResultVO test11(@RequestBody String body, HttpServletRequest request) throws IOException {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
System.out.println("body = " + body);
// 用fastjson将json字符串转换成bean
User1 user = JSON.parseObject(body, User1.class); //虽然User1的属性上加了@NotNull等注解,但是并不起作用
// 只能在代码中手动校验
// if (null == user.getName()) {
// return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, "入参name不能为空");
// }
return new ResultVO(requestMetaInfo + " ---> SUCCESS! user = " + user);
}
这种方式,其实就是直接以String格式,拿到body的内容,然后再自行将body中的json串转换成对应的bean对象
调用用例:
6. 方式5:用Map来接收RequestBody入参
这种方式的示例代码如下,它跟上面那种方式一样的缺点——没有办法结合@NotNull等校验注解,对入参进行校验。
因此,同样也很少用这种方式来接收入参。
但是如果你的接口入参个数多,你又懒得定义对应的实例来接收入参,并且也不想对入参进行校验,那你图方便的话,也可以选择使用这种方式来接收入参。自行决定。
/**
* 直接用一个Map接收body参数
*/
@RequestMapping(value = "/test12_map")
public ResultVO test12(@RequestBody Map bodyMap, HttpServletRequest request) throws IOException {
// 从request中获取一些接口请求时的元数据信息,包括请求方式,Content-type等
String requestMetaInfo = String.format("method: [%s], uri: [%s], Content-Type: [%s] ", request.getMethod(), request.getRequestURI(), request.getHeader("Content-Type"));
// 只能在代码中手动校验
if (null == bodyMap.get("name")) {
return new ResultVO(ResultCodeEnum.IllEGAL_PARAMETER, "入参name不能为空");
}
return new ResultVO(requestMetaInfo + " ---> SUCCESS! bodyMap = " + bodyMap);
}
调用用例:
结束。