[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
概述
在构建 Web 应用程序时,确保进入应用程序的数据有效并满足您的业务需求非常重要。 实现此目的的一种方法是在服务器端验证输入数据。 在这篇博客中,我们将探讨如何在 Spring Boot 应用程序中进行输入数据验证,善用的话可以写出健壮而优美的代码。
让我们从一个实例开始吧
实例
邻家有女初长成,大名唤作牛翠华,家里催翠花找对象,无奈翠花深受互联网女拳师的影响,搞得翠花对另一半的要求非常高...
择偶标准
- 非王思聪类型不嫁
- 年龄大于30不嫁
- 身高矮于185cm不嫁
- 体重高于85kg不嫁
- 没有大别墅不嫁
- 没有大奔驰不嫁
- 父母建在且没有城市养老金不嫁
...
假如我们要写一个产生符合其要求的男朋友的API,如何来写呢?
@Slf4j
@RestController
@RequestMapping("/validation")
public class ValidateController {
@Autowired
private ValidationService validationService;
@PostMapping("/boy-friends")
public ResponseEntity<BoyFriend> createBoyFriend(@RequestBody BoyFriend boy) {
log.info("create:{}", boy);
return ResponseEntity.ok(boy);
}
}
于是我们今天的主角就登场了。
SpringBoot 验证概述
Spring Boot 使用 Jakarta Bean Validation API 为输入数据验证提供内置支持,Java Bean Validation API 是用于验证 Java 对象的标准 API。 此 API 允许您使用注释定义 Java 类属性的约束,并根据这些约束验证输入数据。
目前一般使用2.0版本,由JSR 380提出。Java提出了这个标准,却没有给出实现,我们使用的都是Hibernate 的实现版本: Hibernate Validator .
引入依赖
要使用Hibernate Validator,所以要引入其依赖。SpringBoot2.3以后必须手动引入如下依赖(2.3以前在web的依赖包中包含了)。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
使用相关注解标记
jakarta.validation-api
模块中提供了很多添加约束的注解,包括
@NotNull @NotEmpty @NotBlank @Min @Max @Email @Size @Pattern ...
使用时可以查阅源码,都很简单。
public class BoyFriend {
@Max(30)
private Integer age;
...
}
使用@Valid标记
使用@Valid
标记参数即可。
@PostMapping("/boy-friends")
public ResponseEntity<BoyFriend> createBoyFriend(@Valid @RequestBody BoyFriend boy) {
log.info("create:{}", boy);
return ResponseEntity.ok(boy);
}
完成以上三步其实已经可以了,不过在验证失败时会抛出MethodArgumentNotValidException
,对前端不友好,实际项目中我们都会对异常做统一处理,然后包装成统一的返回格式。
例如下面这样的格式:
{
"code": 10000,
"message": "错误消息",
"data": xxx
}
统一处理异常
@RestControllerAdvice
public class ExceptionHandlers {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse<String> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String errMsg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ":" + f.getDefaultMessage())
.collect(Collectors.joining("| "));
return new BaseResponse<>(10000, MessageFormat.format("Invalid param: [{0}]", errMsg), null);
}
}
高级用法
一般情况下,上面的方法已经可以应对,但是有些场景需要更多的努力
复杂对象参数验证
有时入参比较复杂,例如BoyFriend里面的house属性也是一个object,我们也需要对House这个对象属性进行验证。我们只需要在house属性申明处添加上@Valid
注解即可。
public class BoyFriend {
@Valid
@NotNull
private House house;
}
public class House {
@AssertTrue
private Boolean isVilla;
@DecimalMin("100000000")
private Integer price;
}
基本类型参数验证
有时我们的入参是基本类型,例如使用@RequestParam
或@PathVariable
标记的参数,这个怎么验证呢?例如下面的入参name。
@Validated
@RestController
@RequestMapping("/validation")
public class ValidateController {
...
@GetMapping("/boy-friends")
public ResponseEntity<BoyFriend> updateBoyFriend(@NotBlank @RequestParam("name") String name) {
...
}
}
我们只需要给参数添加上相应的约束注解,例如@NotBlank
,然后再给Controller类添加上@Validated
即可。 @Validated
是Spring提供的一个注解。
需要注意的是,这会抛出的是ConstraintViolationException
异常,不是MethodArgumentNotValidException
,所以需要将此异常也统一处理了,具体看文后源码。
Service方法参数验证
一般情况下我们都是在controller里就把参数验证做了,但是如果我们也想在Service里面的方法使用这套验证机制可以吗?答案是肯定的
- 使用
@Validated
标记Service类 - 使用
@Valid
注解标记方法入参,如果是基本类型的话只使用约束注解即可,与controller一样。
@Validated
@Service
public class ValidationService {
public BoyFriend queryBoyFriendByName(@Size(min = 1,max = 3) String name) {
return BoyFriend.builder().name(name).build();
}
}
手动验证
有时我们需要手动触发验证而不依赖框架,这也是可以的。
- 注入
Validator
的实例 - 调用其
validate
方法并处理结果
@RestController
@RequestMapping("/validation")
public class ValidateController {
@Autowired
private Validator validator;
@PatchMapping("/boy-friends")
public ResponseEntity<BoyFriend> updateBoyFriend(@Valid @RequestBody BoyFriend boy) {
Set<ConstraintViolation<House>> validateResults = validator.validate(boy.getHouse());
String errMsg = validateResults.stream().map(e -> e.getPropertyPath() + ":" + e.getMessage()).collect(Collectors.joining("| "));
throw new BusinessException(errMsg,10002);
...
}
}
分组验证
有时一个类被多个方法使用,而每个方法对入参的要求却不一样,这种情况怎么办呢?例如我们的BoyFriend
,里面有一个体重的属性,创建时要求不能高于85kg,由于条件苛刻,于是修改的时候要求不能高于100kg。
Spring提供了一种解决方法,那就是使用分组。每个注解里面都可以设置其属于哪些分组,在验证的时候只验证属于自己分组的那些约束。
例如我们这里设置两个分组:创建和更新,当调用创建方法的时候就只验证属于创建分组的约束,不高于85kg...
public class BoyFriend {
@Max(groups = BoyFriendCreate.class, value = 85)
@Max(groups = BoyFriendUpdate.class, value = 100)
private Integer weight;
}
如何实现呢?
- 定义分组
分组必须是接口,例如我们这里定义了两个分组
public interface BoyFriendCreate {
}
public interface BoyFriendUpdate {
}
- 给约束添加相应的分组
public class BoyFriend {
@Max(groups = BoyFriendCreate.class, value = 85)
@Max(groups = BoyFriendUpdate.class, value = 100)
private Integer weight;
}
- 给Controller方法添加分组
先给Controller类添加@Validated
,然后给方法添加带有分组信息的@Validated
@Validated(BoyFriendUpdate.class)
@PatchMapping("/boy-friends")
public ResponseEntity<BoyFriend> updateBoyFriend(@Valid @RequestBody BoyFriend boy) {
return ResponseEntity.ok(boy);
}
注意,这种方法被认为是反模式的,因为其将代码耦合在了一起。本来创建和更新应该是两个不同的类,现在我们却将其耦合在了一起,通过一个分组的信息来区分
自定义约束注解
这是最后一招拉,每当框架提供的不能满足我们的需求时,我们就需要按照自己的需求自定义了,每个优秀的框架和类库都提供了这种能力
假设牛翠华对男朋友要求非常之高,要求必须是指定的某些人,例如王思聪,马化腾...,当然这个需求可以使用@Pattern
然后写正则表达式来实现,不过我们这里为了演示就写一个自定义的约束注解来实现。
- 定义一个约束注解
我们自定义一个注解@Target
,前三个属性都是必须的,最后一个value是我们自己的,我们用它来保存目标的名称。
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TargetManValidator.class)
public @interface TargetMan {
String message() default "错误的人";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value() default {};
}
可以看到我们使用了@Constraint(validatedBy = TargetManValidator.class)
来标记@TargerMan
注解,所以我们还需要实现一个TargetManValidator
。
- 实现一个
TargetManValidator
具体的验证逻辑就是在这个类里面的。
public class TargetManValidator implements ConstraintValidator<TargetMan,String> {
private List<String> values = new ArrayList<>();
@Override
public void initialize(TargetMan constraintAnnotation) {
//从TargetMan 注解中获取用户设置的姓名列表
values = Arrays.stream(constraintAnnotation.value()).collect(Collectors.toList());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return values.contains(value);
}
}
我们的验证逻辑就在isValid
方法中,如果用户输入的姓名不在设置的姓名集合之中就返回FALSE。
- 使用
public class BoyFriend {
@NotBlank
@TargetMan({"马化腾", "王思聪", "王二狗"})
private String name;
}
JPA Entity验证
你自己加几个约束注解试试...
概述
今天就到这啦,希望小朋友们都学头秃了... 对了,最后牛翠华嫁给了王二狗,至少都姓王,O(∩_∩)O哈哈~ 。
我在此为牛翠华正名:说翠花打拳只是为行文方便,翠花是一个温婉贤良,上孝下敬的好女子...
源码
一如既往,你可以在Github上找到本文源码:learn-springboot,小星星点一点,需要的时候方便找的到。
参考文章:Validation with Spring Boot - the Complete Guide
题外话
最近小区物业群里警察叔叔经常发送辖区被电信诈骗的案例。我从头到尾只有两个感觉,一个是这些人怎么这么有钱?二是这些人怎么这么傻?基本上老人妇女死于刷单理财,壮男死于约炮...。我觉得要是骗我估计有点困难,不是我聪明主要人到中年一没钱,二没啥兴趣,骗子都绕着走啊,o( ̄︶ ̄)o..
文章评论
受益匪浅