SpringBoot - 优雅的实现【参数校验】高级进阶
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;
}
创建两个自定义注解,用于业务规则校验
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 {};
}
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函数式接口对业务规则进行判断.
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