1. 使用注解的必要性
在项目的接口定义中,需要对接口的入参做必要的校验,以拦截不合法的请求。简单粗暴的使用if else校验虽然可以达到目的,但是代码看起来会繁琐、冗余,不直观,所以推荐使用注解进行校验,使得代码简单、优雅、方便维护。可以使用现成的校验组件,也可以进行自定义的注解校验。本篇文章主要讲解spring提供的Validated校验使用方式。
注:自定义注解可参考另外一边博文:自定义校验注解
2. 使用方式
2.1 依赖包
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.2.Final</version>
</dependency>
注:引入spring之后,依赖包是不需要手动引入的
2.2 校验@RequestBody入参
@RestController
public class ValidController {
/**
* 1.对@RequestBody入参进行校验
* 对于@RequestBody的入参,@Validated注解写在ValidController类上是不生效的,必须要写在参数前
* 此处的@Validated可以替换为@Valid
* @param user
* @return
*/
@PostMapping("/api/v1/user")
public String createUser(@Validated @RequestBody User user, BindingResult result) {
if (result.hasErrors()) {
result.getAllErrors().forEach((error) -> {
FieldError fieldError = (FieldError) error;
// 属性
String field = fieldError.getField();
// 错误信息
String message = fieldError.getDefaultMessage();
System.out.println(field + ":" + message);
});
return "failed";
}
System.out.println(user.toString());
return "create user success";
}
}
@Data
public class User {
@NotNull
@Min(value = 1, message = "id 必须大于0")
@Range(min = 1, max = 100, message = "id 值必须大于0且不大于100")
private Long id;
@NotBlank(message = "name 不能为空")
private String name;
//@Valid注解具有嵌套校验的功能
//此处不能使用@Validated,@Validated不能用在类属性上
@Valid
private Dog dog;
@Getter
@Setter
public static class Dog {
@NotBlank(message = "dog name 不能为空")
private String name;
}
}
需要注意的点:
- 对于@RequestBody的入参,@Validated注解写在ValidController类上是不生效的,必须要写在参数前。可以替换为@Valid。
- 对于入参module中的嵌套类校验,只能使用@Valid,@Validated不可以注解在类的属性字段上;
2.2 校验@RequestParam|@PathVariable入参
@Validated
@RestController
public class ValidController {
/**
* 2.对@RequestParam|@PathVariable入参进行校验
* 为了使得参数校验生效,在类ValidController上加上@Validated注解
* 注:把@Validated注解加在要校验的参数前面,是不会生效的
* @param id
* @param name
* @return
*/
@GetMapping("/api/v1/user/{id}")
public String getUser(@Range(min = 1, max = 100, message = "id值必须大于0且不大于100") @PathVariable("id") Integer id,
@NotBlank(message = "name不可以为空") @RequestParam String name) {
return "get user success";
}
}
需要注意的点:
- 把@Validated注解加在要校验的参数前面,是不会生效的,要加在类ValidController上;
2.3 参数校验失败处理
方式一:对于@RequestBody入参校验,Controller方法中可以传一个BindingResult或者Errors类型的参数,值得注意的是这个参数的位置必须是参数列表的被校验注解修饰的参数紧跟着的。具体示例可参考2.1中对BindingResult的处理。
方式二:对方式一的优化。由于这种异常处理在多个接口中处理方式基本一样,可以根据AOP原理将对BindingResult的处理抽象出来,具体示例:
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.test.controller.*.*(..))")
public void BindingResult() {
}
@Around("BindingResult()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg;
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
if(fieldError!=null){
return CommonResult.validateFailed(fieldError.getDefaultMessage());
}else{
return CommonResult.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}
方式三:对方式二继续优化。定义一个异常处理类,继承ResponseEntityExceptionHandler。其中有很多关于400,也就是参数错误的处理,有一个专门用来处理没有通过校验的参数的方法。我们重写这个类的这个方法即可。
@ControllerAdvice // Spring 的异常处理的注解
public class BadRequestExceptionHandler extends ResponseEntityExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, String> messages = new HashMap<>();
BindingResult result = ex.getBindingResult();
if (result.hasErrors()) {
List<ObjectError> errors = result.getAllErrors();
for (ObjectError error : errors) {
FieldError fieldError = (FieldError) error;
messages.put(fieldError.getField(), fieldError.getDefaultMessage());
}
logger.error(messages.toString());
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(messages);
}
}
方式四:对方式三的补充。对于不能再ResponseEntityExceptionHandler中处理掉的校验异常,在全局异常处理类中进行捕获处理;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult handle(Exception e) {
log.error("异常消息:{}", e.getMessage(), e);
if (e instanceof ApiException) {
ApiException tmp = (ApiException) e;
if (tmp.getErrorCode() != null) {
return CommonResult.failed(tmp.getErrorCode());
}
}
if (e instanceof ServiceException) {
ServiceException tmp = (ServiceException) e;
if (tmp.getErrorCode() != null) {
return CommonResult.failed(tmp.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
if (e instanceof HttpClientErrorException) {
HttpClientErrorException tmp = (HttpClientErrorException) e;
return CommonResult.failed(tmp.getStatusCode());
}
if (e instanceof IllegalArgumentException) {
return CommonResult.validateFailed(e.getMessage());
}
return CommonResult.failed();
}
}
3. 一些常用的校验注解
注解 | 适用的数据类型 | 作用 |
---|---|---|
@NotNull | Any type | 值不能为空 |
@Null | Any type | 值必须为空 |
@Pattern(regex=) | String | 字符串必须匹配正则表达式 |
@Size(min, max) | String, Collection, Map and arrays(一般用于集合) | 集合或者数组元素的数量必须在min和max之间 |
@CreditCardNumber(ignoreNonDigitCharacters=) | String | 字符串必须是信用卡号,按找美国的标准验证 |
String | 字符串必须是Email地址 | |
@Length(min, max) | String | 检查字符串的长度 |
@NotBlank | String | 字符串不能为空串 |
@NotEmpty | String,Collection, Map and Arrays | 字符串不能为null, 集合或者数组的 size 不能为空 |
@Range(min, max) | String,Collection, Map and Arrays,BigDecimal, BigInteger, CharSequence, byte, short, int, long(一般用于数字) | 数字必须大于min, 小于max |
@SafeHtml | String | 字符串必须是安全的html |
@URL | String | 字符串必须是合法的URL |
@AssertFalse | Boolean, boolean | 值必须是false |
@AssertTrue | Boolean, boolean | 值必须是true |
@DecimalMax(value=, inclusive=) | BigDecimal, BigInteger, String, byte,short, int, long | 值必须小于等于(inclusive=true)/小于(inclusive=false)属性指定的值,也可以注释在字符串类型的属性上。 |
@DecimalMin(value=, inclusive=) | BigDecimal, BigInteger, String, byte,short, int, long | 值必须大于等于(inclusive=true)/小于(inclusive=false)属性指定的值,也可以注释在字符串类型的属性上。 |
@Digist(integer=,fraction=) | BigDecimal, BigInteger, String, byte,short, int, long | 数字格式检查。integer指定整数部分的最大长度,fraction指定小数部分的最大长度 |
@Future | java.util.Date, java.util.Calendar | 时间必须是未来的 |
@Past | java.util.Date, java.util.Calendar | 事件必须是过去的 |
@Max(value=) | BigDecimal, BigInteger, byte, short,int, long | 值必须小于等于value指定的值。不能注解在字符串类型属性上。 |
@Min(value=) | BigDecimal, BigInteger, byte, short,int, long | 值必须大于等于value指定的值。不能注解在字符串类型属性上。 |
4. 其他说明
4.1 @Valid与@Validated的区别
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
- @Valid是javax.validation里的,不提供分组功能;@Validated是@Valid 的一次封装,是Spring提供的校验机制使用,支持分组,可以在入参验证时,根据不同的分组采用不同的验证机制;
- @Validated可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上,所以不具备嵌套校验的功能;@Valid可以用在方法、构造函数、方法参数和成员属性(字段)上,可以进行嵌套校验;
4.2 分组校验
有的时候,开发者在某一个实体类中定义了很多校验规则,但是在某一次业务处理中,并不需要这么多校验规则,此时就可以使用分组校验。分组校验在普通校验的基础上增加了以下3项配置:
- 需要定义分组接口;
- 在入参的@Validated注解的value值指定要使用的分组;
- 在校验注解的groups上指定校验的分组
校验module
@Data
public class User {
@NotNull
@Range(min = 1, max = 100, message = "id值必须大于0且不大于100", groups = validGroup1.class)
private Long id;
@NotBlank(message = "name不能为空", groups = validGroup2.class)
private String name;
@Min(value = 18)
private Integer age;
@Email(groups = {validGroup1.class, validGroup2.class})
private String email;
//@Valid注解具有嵌套校验的功能
//此处不能使用@Validated,@Validated不能用在类属性上
@Valid
private Dog dog;
@Getter
@Setter
public static class Dog {
@NotBlank(message = "dog name不能为空")
private String name;
}
/*
* 定义两个分组校验的接口
* 分组校验规则:
* 使用:@Validated(User.validGroup2.class)指定使用哪组规则;校验module中每个字段校验注解指定groups分组
* 1. @Validated指定分组时,匹配的分组校验生效。校验注解的groups可以指定多个,多个则多个生效,不指定则不校验;
* 2. @Validated不指定分组时,则没有指定groups的校验注解生效,指定过groups的注解不校验
*/
public interface validGroup1 {
}
public interface validGroup2 {
}
}
Controller类接口定义
@PostMapping("/api/v1/user")
public String createUser(@Validated @RequestBody User user, BindingResult result) {
if (result.hasErrors()) {
result.getAllErrors().forEach((error) -> {
FieldError fieldError = (FieldError) error;
// 属性
String field = fieldError.getField();
// 错误信息
String message = fieldError.getDefaultMessage();
System.out.println(field + ":" + message);
});
return "failed";
}
System.out.println(user.toString());
return "create user success";
}
不同分组运行结果分析:
入参如下,均不合法
{
"id":111,
"name":"",
"age":1,
"email":"123",
"dog":{
"name":""
}
}
- createUser(@Validated @RequestBody User user, BindingResult result),@Validated注解不指定分组时,运行结果如下,即所有不指定分组的校验生效
age:最小不能小于18
dog.name:dog name不能为空
- createUser(@Validated(User.validGroup1.class) @RequestBody User user, BindingResult result),@Validated指定分组User.validGroup1.class时,运行结果如下,即所有指定了validGroup1分组的校验注解生效
id:id值必须大于0且不大于100
email:不是一个合法的电子邮件地址
- createUser(@Validated(User.validGroup2.class) @RequestBody User user, BindingResult result),@Validated指定分组User.validGroup2.class时,运行结果如下,即所有指定了validGroup2分组的校验注解生效
name:name不能为空
email:不是一个合法的电子邮件地址