SpringBoot - 优雅的实现【业务校验】高级进阶

简介: SpringBoot - 优雅的实现【业务校验】高级进阶


Pre

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 验证注解的元素值是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


相关文章
|
6月前
|
安全 前端开发 Java
Springboot中如何优雅的进行字段以及业务校验
Springboot中如何优雅的进行字段以及业务校验
103 0
|
4月前
|
Java Spring
springBoot 使用 @NotEmpty,@NotBlank,@NotNull 及@Valid注解校验请求参数
springBoot 使用 @NotEmpty,@NotBlank,@NotNull 及@Valid注解校验请求参数
196 7
|
5月前
|
Java
springboot自定义拦截器,校验token
springboot自定义拦截器,校验token
389 6
|
5月前
|
缓存 NoSQL Java
案例 采用Springboot默认的缓存方案Simple在三层架构中完成一个手机验证码生成校验的程序
案例 采用Springboot默认的缓存方案Simple在三层架构中完成一个手机验证码生成校验的程序
102 5
|
5月前
|
XML 前端开发 Java
SpringBoot参数校验@Validated、@Valid(javax.validation)详解
SpringBoot参数校验@Validated、@Valid(javax.validation)
557 4
|
4月前
|
存储 Java Spring
在Spring Boot中实现数据验证与校验
在Spring Boot中实现数据验证与校验
|
6月前
|
Java
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
|
6月前
|
前端开发 Java
SpringBoot之自定义注解参数校验
SpringBoot之自定义注解参数校验
55 2
|
6月前
|
存储 JSON 算法
SpringBoot之JWT令牌校验
SpringBoot之JWT令牌校验
184 2
|
6月前
|
JSON Java 数据格式
Spring Boot实现各种参数校验
这些是Spring Boot中实现参数校验的一些常见方法,你可以根据项目需求选择适合的方式来进行参数校验。
56 0