前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot - 优雅的实现【业务校验】高级进阶

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

作者头像
小小工匠
发布2022-02-28 10:23:33
8320
发布2022-02-28 10:23:33
举报
文章被收录于专栏:小工匠聊架构小工匠聊架构

文章目录

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 搞个判断 。 今天晚点看起来有点不一样的


实体类

代码语言:javascript
复制
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 搞两个自定义注解

创建两个自定义注解,用于业务规则校验

代码语言:javascript
复制
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 {};
}
代码语言:javascript
复制
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 搞自定义校验器

代码语言:javascript
复制
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 搞验证

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • Pre
  • 需求
  • 实现三部曲
    • 实体类
      • Step1 搞两个自定义注解
        • Step2 搞自定义校验器
          • Step3 搞验证
          • 小结
          • 源码
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档