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

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于SpringBoot - 优雅的实现业务校验高级进阶的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot - 使用Assert校验让业务代码更简洁

SpringBoot - 使用Assert校验让业务代码更简洁

SpringBoot - 优雅的实现参数分组校验高级进阶

SpringBoot - 优雅的实现参数校验高级进阶

SpringBoot中如何实现业务校验,这种方式才叫优雅!

SpringBoot 实现业务校验,这种方式才叫优雅!