前言
参数验证是一个应用中必不可少的一部分操作,参数验证又可以分为前端验证和后端验证。如果没有参数验证的话,我们的逻辑可能就会报错,例如:空指针异常,更严重的可能造成系统的瘫痪。
参数验证又可以分为前端验证和后端验证,前端验证可以通过各种手段进行跳过,例如直接调用接口等等。为了防护系统出现各种异常,后端验证是不可取少的一部分。本文将主要描述使用Spring Boot项目的时候,后端如何进行参数验证。
一、参数传递的方式
- 请求路径中携带的参数,一般是GET请求
- 在请求体中携带的参数,一般是POST、PUT请求
二、参数验证注解
我们主要是使用javax.validation.constraints
和org.hibernate.validator.constraints
包下的注解进行参数验证。常见的注解如下:
@NotNull
:表示参数不能为Null,如果为空的话,就无法通过验证@Max
:一般用于数字,表示不能超过最大值
三、参数验证——请求路径中携带的参数
一般用于GET请求中。
例1:希望name不能为空,age不能大于100的限制条件。
@RestController
@Validated
public class TestController {
@GetMapping("/testget")
public JSONObject testget(@NotNull(message = "姓名不能为空") String name,
@Max(value = 100, message = "不能大于100") Integer age) {
JSONObject jsonObject = new JSONObject();
return jsonObject;
}
}
如果我们访问localhost:8080/testget
或者localhost:8080/testget?age=101
或者localhost:8080/testget?age=1
,就会抛出一个ConstraintViolationException
的异常信息。前端就会收到一个500的错误信息。我们可以进行异常统一捕获,在后面进行描述,从而向前端返回统一的错误信息。
在使用路径中携带参数的注意事项:
- 必须在类上添加
@Validated
注解 - 在需要验证的参数中添加验证注解。
- 使用路径中携带参数如果没有通过验证,就会抛出
ConstraintViolationException
的异常信息。
四、参数验证——请求体中携带的参数
一般用于POST、PUT请求
我们使用Restful
的格式传递参数。参数使用JSON格式传递。
使用请求体中携带参数的方式主要有三种形式。
下面是使用的实体:包含一个嵌套实体的ChildDTO类
public class AreaDTO {
@NotNull(groups = {UpdateValidation.class}, message = "区域ID不能为空")
private Long id;
@NotNull(message = "父级ID不能为空", groups = {AddValidation.class})
private Long parentId;
@NotNull(message = "片区名称不能为空")
private String name;
@Valid
private ChildDTO childDTO;
}
public class ChildDTO {
@NotNull(message = "不能为空")
private String childName;
@NotNull(groups = {AddValidation.class}, message = "不能为空")
private Long childId;
@Max(groups = {UpdateValidation.class}, value = 100 ,message = "不能为空")
@Min(groups = {AddValidation.class}, value = 50, message = "最小值")
private Long age;
}
//分组一
public interface AddValidation {
}
//分组二
public interface UpdateValidation {
}
方式1:抛出异常方式
例2:
@PostMapping("/testpost")
public JSONObject testpost(@Validated @RequestBody AreaDTO areaDTO) {
JSONObject jsonObject = new JSONObject();
return jsonObject;
}
例3
@PostMapping("/testpost1")
public JSONObject testpost1(@Validated(value = {AddValidation.class}) @RequestBody AreaDTO areaDTO) {
JSONObject jsonObject = new JSONObject();
return jsonObject;
}
上面两个例子中,第一个例子使用了默认的分组(即Default
),而第二个例子使用了AddValidation
的分组。我们在添加验证条件的时候,可以为其指定分组限制,使用groups
属性,例如:@NotNull(message = "父级ID不能为空", groups = {AddValidation.class})
,这个验证限制只有在@Validated(value = {AddValidation.calss})
才会起作用。只有符合验证条件的分组才会进行验证。
例如:例2中,没有添加分组,默认使用Default
分组,就会验证AreaDTO.name
,AreaDTO.ChildDTO.childName
两个属性。例3中,添加了AddValidation
的验证分组,就会验证AreaDTO.parentId
,AreaDTO.ChildDTO.childId
,AreaDTO.ChildDTO.age
三个属性。
当无法通过验证条件的时候,就会抛出一个MethodArgumentNotValidException
的异常信息。里面包含了所有所有没有通过验证的属性。
注意:没有添加分组,则默认使用Default
的分组。
在使用请求体中抛出异常的注意事项:
- 在需要验证参数前面添加
@Validation
注解, - 在需要验证属性前面添加
@Max
等验证条件 - 如果验证无法通过,就会抛出一个
MethodArgumentValidException
的异常
方式二:使用实体封装方式
例4
@PostMapping("/testpost4")
public JSONObject testpost4(@Validated @RequestBody AreaDTO areaDTO, BindingResult result) {
//手动验证是否有没有通过的参数
if (result.hasErrors()) {
for (FieldError fieldError : result.getFieldErrors()) {
System.out.println(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
//这里返回我们自定义的返回结果
}
JSONObject jsonObject = new JSONObject();
return jsonObject;
}
在使用请求体中实体封装的注意事项:
- 在需要验证的参数前面添加
@Validated
注解 - 在需要验证的参数后面,添加
BindingResult
的实体 - 在需要验证的属性前面,添加
@Max
等验证属性 - 没有通过验证,就会绑定到
BindingResult
实体中,不会抛出异常,必须手动进行判断
方式三:手动验证
例5:
@Autowired
private Validator validator;
@PostMapping("/testpost5")
public JSONObject testpost5(@RequestBody AreaDTO areaDTO) {
Set<ConstraintViolation<AreaDTO>> validate = validator.validate(areaDTO, AddValidation.class, Default.class);
if (!validate.isEmpty()) {
String message = validate.iterator().next().getMessage();
logger.error(message);
}
JSONObject jsonObject = new JSONObject();
return jsonObject;
}
在使用请求体中手动验证的注意事项:
- 在需要验证的属性前面,添加
@Max
等验证属性 - 使用
Validator
实例调用validate
方法验证 - 可以在任何地方进行参数验证,更加灵活
到此,就完成了参数验证的全部内容。各种方式各有不同。
附录一:添加统一异常处理
@RestControllerAdvice
public class CommonRuntimeExceptionHandle {
@ExceptionHandler(ConstraintViolationException.class)
public JSONObject methodArgumentNotValidExceptionHandle(ConstraintViolationException methodArgumentNotValidException) {
Set<ConstraintViolation<?>> constraintViolations = methodArgumentNotValidException.getConstraintViolations();
String message = "";
for (ConstraintViolation<?> next : constraintViolations) {
message += next.getMessage()+" , ";
}
JSONObject responseResult = new JSONObject();
responseResult.put("message", "参数不正确:" + message);
responseResult.put("code", "-1");
return responseResult;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public JSONObject methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException exception) {
FieldError fieldError = exception.getBindingResult().getFieldError();
MethodParameter parameter = exception.getParameter();
logger.error("在执行{}.{}的时候出现绑定JSON数据参数异常,字段名称为:{},提示消息:{}",
parameter.getDeclaringClass().getName(), parameter.getMethod().getName(), fieldError.getField(),
fieldError.getDefaultMessage());
JSONObject responseResult = new JSONObject();
responseResult.put("message", "参数不正确:" + fieldError.getDefaultMessage());
responseResult.put("code", "-1");
return responseResult;
}
}
我们使用@RestControllerAdvice
的注解,标识这个类为控制器的增强类。
附录二:快速失败
默认情况下,Validator会验证所有需要验证的参数。如果有多个错误,就会出现多个字段异常信息。如果希望,当出现一个参数异常的时候,直接退出,不在继续验证其他参数的正确性。需要配置快速退出
@Configuration
public class ValidateConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure().failFast(true).buildValidatorFactory();
return validatorFactory.getValidator();
}
}