Pre
SpringBoot - 优雅的实现【自定义参数校验】高级进阶
SpringBoot - 优雅的实现【参数分组校验】高级进阶
SpringBoot - 使用Assert校验让业务代码更简洁
在开发中,为了保证接口的稳定安全,一般需要在接口逻辑中进行校验,比如 上面几篇都是 【参数校验】,一般我们都是使用Bean Validation校验框架。
校验规则 | 规则说明 |
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
那【业务规则校验】大部分情况下为了简单都是 if else
,那怎么玩的更优雅一些呢?
Tips: 参考 Bean Validation 的标准方式,借助自定义校验注解进行业务规则校验
需求
- 新增用户 , 用户名+手机号码+邮箱 唯一
- 修改用户, 修改后的 【用户名+手机号码+邮箱】不能与库中的用户信息冲突
实现三部曲
当然了, 简单的写就是整个if else return 嘛 查查DB 搞个判断 。 今天晚点看起来有点不一样的
实体类
package com.artisan.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; /** * @author 小工匠 * @version 1.0 * @mark: show me the code , change the world */ @Data @AllArgsConstructor @NoArgsConstructor public class Artisan { private String id; @NotEmpty(message = "Code不能为空") private String code; @NotBlank(message = "名字为必填项") private String name; @Length(min = 8, max = 12, message = "password长度必须位于8到12之间") private String password; @Email(message = "请填写正确的邮箱地址") private String email; private String sex; private String phone; }
Step1 搞两个自定义注解
创建两个自定义注解,用于业务规则校验
package com.artisan.annos; import com.artisan.validate.ArtisanValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; /** * * 自定义 "用户唯一" 校验注解 .唯一包含 -----------> 用户名+手机号码+邮箱 * @author artisan */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE}) @Constraint(validatedBy = ArtisanValidator.UniqueArtisanValidator.class) public @interface UniqueArtisan { String message() default "用户名、手机号码、邮箱不允许与现存用户重复"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
package com.artisan.annos; import com.artisan.validate.ArtisanValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 表示一个用户的信息是无冲突的 * “无冲突”是指该用户的敏感信息与其他用户不重合,比如将一个注册用户的邮箱、手机,修改成与另外一个已存在的注册用户一致的值,这样不行 * @author artisan */ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = ArtisanValidator.NotConflictArtisanValidator.class) public @interface NotConflictArtisan { String message() default "用户名称、邮箱、手机号码与现存用户产生重复"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Step2 搞自定义校验器
package com.artisan.validate; import com.artisan.annos.NotConflictArtisan; import com.artisan.annos.UniqueArtisan; import com.artisan.bean.Artisan; import com.artisan.repository.ArtisanDao; import lombok.extern.slf4j.Slf4j; import javax.annotation.Resource; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.function.Predicate; /** * @author 小工匠 * @version 1.0 * @mark: show me the code , change the world */ @Slf4j public class ArtisanValidator<T extends Annotation> implements ConstraintValidator<T, Artisan> { protected Predicate<Artisan> predicate = c -> true; @Resource protected ArtisanDao artisanDao; @Override public boolean isValid(Artisan artisan, ConstraintValidatorContext constraintValidatorContext) { return artisanDao == null || predicate.test(artisan); } /** * 校验用户是否唯一 * 即判断数据库是否存在当前新用户的信息,如用户名,手机,邮箱 */ public static class UniqueArtisanValidator extends ArtisanValidator<UniqueArtisan> { @Override public void initialize(UniqueArtisan uniqueArtisan) { predicate = c -> !artisanDao.existsByNameOrEmailOrPhone(c.getName(), c.getEmail(), c.getPhone()); } } /** * 校验是否与其他用户冲突 * 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 */ public static class NotConflictArtisanValidator extends ArtisanValidator<NotConflictArtisan> { @Override public void initialize(NotConflictArtisan notConflictUser) { predicate = c -> { log.info("user detail is {}", c); Collection<Artisan> collection = artisanDao.findByNameOrEmailOrPhone(c.getName(), c.getEmail(), c.getPhone()); // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId())); }; } } }
自定义验证注解需要实现 ConstraintValidator
接口。
- 第一个参数是 自定义注解类型
- 第二个参数是 被注解字段的类
因为需要校验多个参数, 直接传入用户对象。
需要提到的一点是 ConstraintValidator 接口的实现类无需添加 @Component 它在启动的时候就已经被加载到容器中了。
使用Predicate函数式接口对业务规则进行判断.
Step3 搞验证
package com.artisan.controller; import com.artisan.annos.NotConflictArtisan; import com.artisan.annos.UniqueArtisan; import com.artisan.bean.Artisan; import com.artisan.repository.ArtisanDao; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * */ @RestController @RequestMapping("/buziVa/artisan") @Slf4j @Validated public class ArtisanController { @Autowired private ArtisanDao artisanDao; // POST 方法 @PostMapping public Artisan createUser(@UniqueArtisan @Valid Artisan user) { Artisan savedUser = artisanDao.save(user); log.info("save user id is {}", savedUser.getId()); return savedUser; } // PUT @SneakyThrows @PutMapping public Artisan updateUser(@NotConflictArtisan @Valid @RequestBody Artisan artisan) { Artisan editUser = artisanDao.save(artisan); log.info("update artisan is {}", editUser); return editUser; } }
只需要在方法上加入自定义注解即可,业务逻辑中不需要添加任何业务规则的代码。
小结
通过上面几步操作,业务校验便和业务逻辑就完全分离开来,在需要校验时用@Validated注解自动触发,或者通过代码手动触发执行。
这些注解应用于控制器、服务层、持久层等任何层次的代码之中。
在开发时可以将不带业务含义的格式校验注解放到 Bean 的类定义之上,将带业务逻辑的校验放到 Bean 的类定义的外面。
区别是放在类定义中的注解能够自动运行,而放到类外面则需要明确标出@Validated注解时才会运行。
源码
https://github.com/yangshangwei/boot2