在平常的开发中,参数校验是很是重要的一个环节,严格参数校验会减小不少出bug的几率,增长接口的安全性。在此以前写过一篇SpringBoot统一参数校验主要介绍了一些简单的校验方法。而这篇则是介绍一些进阶的校验方式。好比说:在某个接口编写的过程当中确定会遇到,当xxType值为A,paramA值必传。xxType值为B,paramB值必须传。对于这样的,一般的作法就是在controller加上各类if判断。显然这样的代码是不够优雅的,而分组校验及自定义参数校验,就是来解决这个问题的。正则表达式
Restful的接口,在如今来说应该是比较常见的了,经常使用的地址栏的参数,咱们都是这样校验的。spring
/** * 获取电话号码信息 */ @GetMapping("/phoneInfo/{phone}") public ResultVo phoneInfo(@PathVariable("phone") String phone){ // 验证电话号码是否有效 String pattern = "^[1][3,4,5,7,8][0-9]{9}$"; boolean isValid = Pattern.matches(pattern, phone); if(isValid){ // 执行相应逻辑 return ResultVoUtil.success(phone); } else { // 返回错误信息 return ResultVoUtil.error("手机号码无效"); } }
很显然上面的代码不够优雅,因此咱们能够在参数后面,添加对应的正则表达式phone:正则表达式来进行验证。这样就省去了在controller编写校验代码了。数据库
/** * 获取电话号码信息 */ @GetMapping("/phoneInfo/{phone:^[1][3,4,5,7,8][0-9]{9}$}") public ResultVo phoneInfo(@PathVariable("phone") String phone){ return ResultVoUtil.success(phone); }
虽然这样处理后代码更精简了。可是若是传入的手机号码,不符合规则会直接返回404。而不是提示手机号码错误。错误信息以下:安全
咱们以校验手机号码为例,虽然validation提供了@Pattern这个注解来使用正则表达式进行校验。若是被使用在多处,一旦正则表达式发生更改,则须要一个一个的进行修改。很显然为了不作这样的无用功,自定义校验注解就是你的好帮手。app
@Data public class PhoneForm { /** * 电话号码 */ @Pattern(regexp = "^[1][3,4,5,7,8][0-9]{9}$" , message = "电话号码有误") private String phone; }
要实现一个自定义校验注解,主要是有两步。一是注解自己,二是校验逻辑实现类。jvm
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) public @interface Phone { String message() default "手机号码格式有误"; Class[] groups() default {}; Class[] payload() default {}; }
public class PhoneValidator implements ConstraintValidator{ @Override public boolean isValid(Object telephone, ConstraintValidatorContext constraintValidatorContext) { String pattern = "^1[3|4|5|7|8]\\d{9}$"; return Pattern.matches(pattern, telephone.toString()); } }
@Data public class CustomForm { /** * 电话号码 */ @Phone private String phone; }
@PostMapping("/customTest") public ResultVo customTest(@RequestBody @Validated CustomForm form){ return ResultVoUtil.success(form.getPhone()); }
注解是指定当前自定义注解可使用在哪些地方,这里仅仅让他可使用属性上。但还可使用在更多的地方,好比说方法上、构造器上等等。ide
指定当前注解保留到运行时。保留策略有下面三种:函数
指定了当前注解使用哪一个校验类来进行校验。spring-boot
@Data public class UserForm { /** * id */ @Null(message = "新增时id必须为空", groups = {Insert.class}) @NotNull(message = "更新时id不能为空", groups = {Update.class}) private String id; /** * 类型 */ @NotEmpty(message = "姓名不能为空" , groups = {Insert.class}) private String name; /** * 年龄 */ @NotEmpty(message = "年龄不能为空" , groups = {Insert.class}) private String age; }
public interface Insert { }
public interface Update { }
/** * 添加用户 */ @PostMapping("/addUser") public ResultVo addUser(@RequestBody @Validated({Insert.class}) UserForm form){ // 选择对应的分组进行校验 return ResultVoUtil.success(form); } /** * 更新用户 */ @PostMapping("/updateUser") public ResultVo updateUser(@RequestBody @Validated({Update.class}) UserForm form){ // 选择对应的分组进行校验 return ResultVoUtil.success(form); }
在@GroupSequence内能够指定,分组校验的顺序。好比说@GroupSequence({Insert.class, Update.class, UserForm.class})先执行Insert校验,而后执行Update校验。若是Insert分组,校验失败了,则不会进行Update分组的校验。测试
@Data @GroupSequence({Insert.class, Update.class, UserForm.class}) public class UserForm { /** * id */ @Null(message = "新增时id必须为空", groups = {Insert.class}) @NotNull(message = "更新时id不能为空", groups = {Update.class}) private String id; /** * 类型 */ @NotEmpty(message = "姓名不能为空" , groups = {Insert.class}) private String name; /** * 年龄 */ @NotEmpty(message = "年龄不能为空" , groups = {Insert.class}) private String age; }
/** * 编辑用户 */ @PostMapping("/editUser") public ResultVo editUser(@RequestBody @Validated UserForm form){ return ResultVoUtil.success(form); }
哈哈哈,测试结果实际上是个死循环,无论你咋输入都会报错,小伙伴能够尝试一下哦。上面的例子只是个演示,在实际中仍是别这样作了,须要根据具体逻辑进行校验。
对于以前提到了当xxType值为A,paramA值必传。xxType值为B,paramB值必须传这样的场景。单独使用分组校验和分组序列是没法实现的。须要使用@GroupSequenceProvider才行。
@Data @GroupSequenceProvider(value = CustomSequenceProvider.class) public class CustomGroupForm { /** * 类型 */ @Pattern(regexp = "[A|B]" , message = "类型没必要须为 A|B") private String type; /** * 参数A */ @NotEmpty(message = "参数A不能为空" , groups = {WhenTypeIsA.class}) private String paramA; /** * 参数B */ @NotEmpty(message = "参数B不能为空", groups = {WhenTypeIsB.class}) private String paramB; /** * 分组A */ public interface WhenTypeIsA { } /** * 分组B */ public interface WhenTypeIsB { } }
public class CustomSequenceProvider implements DefaultGroupSequenceProvider{ @Override public List<Class> getValidationGroups(CustomGroupForm form) { List<Class> defaultGroupSequence = new ArrayList<>(); defaultGroupSequence.add(CustomGroupForm.class); if (form != null && "A".equals(form.getType())) { defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class); } if (form != null && "B".equals(form.getType())) { defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class); } return defaultGroupSequence; } }
/** * 自定义分组 */ @PostMapping("/customGroup") public ResultVo customGroup(@RequestBody @Validated CustomGroupForm form){ return ResultVoUtil.success(form); }
GroupSequence注解是一个标准的Bean认证注解。正如以前,它可以让你静态的从新定义一个类的,默认校验组顺序。然而GroupSequenceProvider它可以让你动态的定义一个校验组的顺序。
SpringBoot 2.3.x 移除了validation依赖须要手动引入依赖。
org.springframework.bootspring-boot-starter-validation
我的的一些小经验,参数的非空判断,这个应该是校验的第一步了,除了非空校验,咱们还须要作到下面这几点:
参数校验越严格越好,严格的校验规则不只能减小接口出错的几率,同时还能避免出现脏数据,从而来保证系统的安全性和稳定性。