在软件开发中,通常需要对参数进行校验,比如某些参数不能为空,而且不光前端要校验,后端也要校验。
那么后端在哪里校验参数?是在控制层还是service层?参数校验有多种实现方式,采用哪一种校验方式呢? 一般来说,把无业务语义的校验放在action层,用validation做校验,比如数据类型,是否为空,数据长度、格式等,有业务语义的校验放在service层。
当然实际中往往并没有那么清晰,当校验足够简单时,可能只在前端校验就行了,或者简单的在service层校验一下,而当校验较复杂时,则可能需要多层校验。
2024年7月更新
参数校验规范与实现
Java中参数校验规范为Bean Validation
,官网地址: https://beanvalidation.org/。
Bean Validation先后经历了1.0(JSR 303)、1.1(JSR 349)、2.0(JSR 380)这3个版本,目前使用比较多的是Bean Validation 2.0。当然,现在改名叫Jakarta Bean Validation了。
Maven坐标为:
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
Bean Validation的官方参考实现是hibernate Validator
(与Hibernate ORM没有关系),maven坐标如下:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
注意,hibernate-validator中已经包含了validation-api,因此项目中如果引入了hibernate-validator,就没必要重复引入validation-api了。
如果是Springboot项目,可以引入spring-boot-starter-validation即可,其中包含了这两个包。
Bean Validation中原生的注解如下:
具体的每个注解的用法这里就不多说了。
Valid和Validated
@Valid 是Bean Validation中的标准注解,可以进行级联和递归校验。下面是一个示例:
// 要校验的bean
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class User {
@NotNull(message = "姓名不能为空")
@Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间")
private String name;
@NotNull(message = "电子邮件不能为空")
private String email;
// 递归校验Address对象属性
@Valid
private Address address;
// Getters and Setters
}
// 控制器类
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Valid @RequestBody User user) {
// 如果验证通过,处理用户数据
return "用户有效";
}
}
当调用createUser接口而参数user中属性不符合要求时,系统会抛出异常MethodArgumentNotValidException
。一般情况下,我们需要通过全局异常处理器来捕获和处理该异常:
@RestControllerAdvice
public class RestApiValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> toResponse(final HttpServletRequest request, MethodArgumentNotValidException ex) {
...
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return errors;
}
}
经过异常处理器处理后,系统将返回一个400错误,并提示错误消息。当然,这里可以根据业务需要来封装返回的数据结构。
这是Bean Validation的最常用的方式。
事实上在项目中,你可能还会经常看到另外一个注解,那就是@Validated
。这个Spring的注解,是标准JSR-303的一个补充,主要提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
// 要校验的bean
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class User {
@NotNull(message = "姓名不能为空")
@Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间")
private String name;
@NotNull(message = "电子邮件不能为空", groups = {AddUser.class})
private String email;
// 级联校验Address对象属性
@Valid
private Address address;
// Getters and Setters
}
// 控制器类
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Validated(AddUser.class) @RequestBody User user) {
// 如果验证通过,处理用户数据
return "用户有效";
}
@PutMapping
public String updateUser(@Validated(UpdateUser.class) @RequestBody User user) {
// 如果验证通过,处理用户数据
return "更新成功";
}
}
AddUser和UpdateUser只需要定义成两个接口,标识参数验证时的分组即可。
Valid和Validated的区别
除了Valid不支持分组校验之外,其他的两者功能基本一样。主要的差异点在于:
Valid | Validated | |
---|---|---|
规范 | JSR 303 | JSR 303 补充 |
使用范围 | 方法,参数,字段和构造器 | 类,方法和参数 |
是否支持级联校验 | 支持 | 不能用在字段上,不支持 |
是否支持分组校验 | 不支持 | 支持 |
但是这两个注解是可以混合使用的,因此两者结合起来既可以支持级联校验,又可以支持分组校验。
参数校验异常
在上文中提到,当参数校验失败时,会抛出MethodArgumentNotValidException
异常。事实上,除此之外,还可能抛出ConstraintViolationException
异常。前者是Spring框架中定义的异常,而后者是Bean Validation规范中定义的异常。
通常情况下,当接口的入参为引用类型时,校验不通过会抛出MethodArgumentNotValidException
异常。该异常继承于BindException,校验结果保存在bindingResult中;而当接口的入餐类型为基本类型时,校验不通过会抛出ConstraintViolationException
异常。这与Spring Web框架的参数解析器有关,表单、普通类型和引用类型参数在解析时会使用不同的参数解析器,当参数解析失败时抛出不同的异常。
另外,直接使用Validator校验bean时也会抛出ConstraintViolationException
异常。
一个正常的业务系统中这两种异常应该都会存在,在全局异常处理时捕获并处理即可。