严谨的接口是必须要有参数校验的,回避掉一些不合理的请求。但是校验逻辑和正常业务逻辑掺杂在一起,固然能完成校验需求,但是在实现方式上却不那么优雅。比如现在有一个user注册接口:
1.直男癌一般的校验
@PostMapping("/user")
public Mono<String> insert(@RequestBody User user) {
if(StringUtils.isEmpty(user.getName())) {
return Mono.just("姓名不能为空");
}
if(StringUtils.isEmpty(user.getEnname())) {
return Mono.just("英文姓名不能为空");
}
// 处理业务逻辑
return Mono.just("注册成功");
}
这种最为无脑直接,也确实能完成校验需求,但是却有点憨憨,当一个对象中有很多字段需要校验的时候,很多if - else就显得很臃肿。我刚写代码的时候就经常这样干。。。。
2.借助spring的@validated注解来校验
@Data
public class User {
private int id;
@NotNull(message = "姓名不可为空")
private String name;
@NotNull(message = "英文姓名不可为空")
private String enname;
private String mobile;
}
@PostMapping("/user")
public Mono<String> insert(@RequestBody @Validated User user) {
// 处理业务逻辑
return Mono.just("注册成功");
}
借助了spring的力量之后,原先臃肿的if - else没有了,代码瞬间变的简洁了起来。但是这样子也是有一些问题的,比如我们现在要在Controller中新增添一个修改user的接口,既然是修改接口,我们可以选择只修改user的name或者只修改user的enname。那么就会出现一个问题,新增接口的校验和修改接口的校验冲突了。新增接口要求:name和enname都不能为空,修改接口要求:name和enname可以为空,这个时候用一个user对象就有点尴尬。
3.@validated 分组校验
@Data
public class User {
@Min(value = 1, message = "ID不可为空", groups = {Update.class})
private int id;
@NotNull(message = "姓名不可为空", groups = {Insert.class})
private String name;
@NotNull(message = "英文姓名不可为空", groups = {Insert.class})
private String enname;
private String mobile;
}
@PostMapping("/user")
public Mono<String> insert(@RequestBody @Validated(Insert.class) User user) {
// 处理新增逻辑
return Mono.just("注册成功");
}
@PutMapping("/user")
public Mono<String> update(@RequestBody @Validated(Update.class) User user) {
// 处理修改逻辑
return Mono.just("修改成功");
}
// 随便自定义2个接口 用于标识新增和修改
public interface Insert{
}
public interface Update{
}
通过@Validated的分组校验,可以指定在什么时候才验证字段的合法性。一切看起来都是这么的完美,spring为我们整理了一些内置的验证注解,部分如下:
@Null 被注释的元素必须为null
@NotNull 被注释的元素不能为null
@AssertTrue 被注释的元素必须为true
@AssertFalse 被注释的元素必须为false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min) 被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串必须非空
@Range 被注释的元素必须在合适的范围内
这些自带的验证注解包括验证长度,验证是否为true,验证是否为邮箱,验证最大值等等。但是现在我们的user对象又有一个新的需求,添加电话号码字段,但是spring并没有一个自带的注解是可以验证一个字符串是否是电话号码的。
4.自定义验证注解
第一步,新增一个注解类:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR,
ElementType.PARAMETER })
//约束注解应用的目标元素类型(METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER等)
@Retention(RetentionPolicy.RUNTIME) // 约束注解应用的时机
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface IsMobile {
boolean required() default true;
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二步,编写校验类,实现ConstraintValidator<A extends Annotation, T>接口。ConstraintValidator<A extends Annotation, T>是个泛型接口,第一个泛型是注解类,第二个泛型是需要验证的数据类型。接口有2个方法,一个initialize负责初始化,另外一个isValid负责具体的校验逻辑。
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MobileValidator implements ConstraintValidator<IsMobile, String>{
@Override
public void initialize(IsMobile constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 关于手机号的验证偷个懒 不为空且长度不是11位的字符串 就被认为是非法
if(value != null && value.length() != 11) {
System.out.println("手机号非法");
return false;
}
return true;
}
}
第三步, 使用自定义注解:
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import com.arvato.ann.Insert;
import com.arvato.ann.IsMobile;
import com.arvato.ann.Update;
import lombok.Data;
@Data
public class User {
@Min(value = 1, message = "ID不可为空", groups = {Update.class})
private int id;
@NotNull(message = "姓名不可为空", groups = {Insert.class})
private String name;
@NotNull(message = "英文姓名不可为空", groups = {Insert.class})
private String enname;
@IsMobile(message = "手机号格式非法", groups = {Update.class, Insert.class})
private String mobile;
}
至此,项目中大部分的参数验证都能通过注解来完成了,但是也不排除有一些业务场景是校验注解无法完成的,有时候还是需要一些直男癌的if-else的。