SpringBoot 如何進行業務校驗,老鳥們都這麼玩的!

大家好,我是飄渺。

在日常的接口開發中,爲了保證接口的穩定安全,我們一般需要在接口邏輯中處理兩種校驗:

  1. 參數校驗
  2. 業務規則校驗

首先我們先看看參數校驗。

參數校驗

參數校驗很好理解,比如登錄的時候需要校驗用戶名密碼是否爲空,創建用戶的時候需要校驗郵件、手機號碼格式是否準確。

而實現參數校驗也非常簡單,我們只需要使用Bean Validation校驗框架即可,藉助它提供的校驗註解我們可以非常方便的完成參數校驗。

常見的校驗註解有:

@Null、@NotNull、@AssertTrue、@AssertFalse、@Min、@Max、@DecimalMin、@DecimalMax、@Negative、@NegativeOrZero、@Positive、@PositiveOrZero、@Size、@Digits、@Past、@PastOrPresent、@Future、@FutureOrPresent、@Pattern、@NotEmpty、@NotBlank、@Email

在SpringBoot中集成參數校驗我特意寫了一篇文章,感興趣的可以點擊閱讀。SpringBoot 如何進行參數校驗,老鳥們都這麼玩的!

接下來我們再看看業務規則校驗。

業務規則校驗

業務規則校驗指接口需要滿足某些特定的業務規則,舉個例子:業務系統的用戶需要保證其唯一性,用戶屬性不能與其他用戶產生衝突,不允許與數據庫中任何已有用戶的用戶名稱、手機號碼、郵箱產生重複。

這就要求在創建用戶時需要校驗用戶名稱、手機號碼、郵箱是否被註冊編輯用戶時不能將信息修改成已有用戶的屬性

95%的程序員當面對這種業務規則校驗時往往選擇寫在service邏輯中,常見的代碼邏輯如下:

public void create(User user) {
    Account account = accountDao.queryByUserNameOrPhoneOrEmail(user.getName(),user.getPhone(),user.getEmail());
    if (account != null) {
        throw new IllegalArgumentException("用戶已存在,請重新輸入");
    }
}

雖然我在上一篇文章中介紹了使用Assert來優化代碼可以使其看上去更簡潔,但是將簡單的校驗交給 Bean Validation,而把複雜的校驗留給自己,這簡直是買櫝還珠故事的程序員版本。

image-20210716084136689

最優雅的實現方法應該是參考 Bean Validation 的標準方式,藉助自定義校驗註解完成業務規則校驗。

接下來我們通過上面提到的用戶接口案例,通過自定義註解完成業務規則校驗。

代碼實戰

需求很容易理解,註冊新用戶時,應約束不與任何已有用戶的關鍵信息重複;而修改自己的信息時,只能與自己的信息重複,不允許修改成已有用戶的信息。

這些約束規則不僅僅爲這兩個方法服務,它們可能會在用戶資源中的其他入口被使用到,乃至在其他分層的代碼中被使用到,在 Bean 上做校驗就能全部覆蓋上述這些使用場景。

自定義註解

首先我們需要創建兩個自定義註解,用於業務規則校驗:

  • UniqueUser:表示一個用戶是唯一的,唯一性包含:用戶名,手機號碼、郵箱
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidation.UniqueUserValidator.class)
public @interface UniqueUser {

    String message() default "用戶名、手機號碼、郵箱不允許與現存用戶重複";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

  • NotConflictUser:表示一個用戶的信息是無衝突的,無衝突是指該用戶的敏感信息與其他用戶不重合
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidation.NotConflictUserValidator.class)
public @interface NotConflictUser {
    String message() default "用戶名稱、郵箱、手機號碼與現存用戶產生重複";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

實現業務校驗規則

想讓自定義驗證註解生效,需要實現 ConstraintValidator 接口。接口的第一個參數是 自定義註解類型,第二個參數是 被註解字段的類,因爲需要校驗多個參數,我們直接傳入用戶對象。需要提到的一點是 ConstraintValidator 接口的實現類無需添加 @Component 它在啓動的時候就已經被加載到容器中了。

@Slf4j
public class UserValidation<T extends Annotation> implements ConstraintValidator<T, User> {

    protected Predicate<User> predicate = c -> true;

    @Resource
    protected UserRepository userRepository;

    @Override
    public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) {
        return userRepository == null || predicate.test(user);
    }

    /**
     * 校驗用戶是否唯一
     * 即判斷數據庫是否存在當前新用戶的信息,如用戶名,手機,郵箱
     */
    public static class UniqueUserValidator extends UserValidation<UniqueUser>{
        @Override
        public void initialize(UniqueUser uniqueUser) {
            predicate = c -> !userRepository.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    /**
     * 校驗是否與其他用戶衝突
     * 將用戶名、郵件、電話改成與現有完全不重複的,或者只與自己重複的,就不算衝突
     */
    public static class NotConflictUserValidator extends UserValidation<NotConflictUser>{
        @Override
        public void initialize(NotConflictUser notConflictUser) {
            predicate = c -> {
                log.info("user detail is {}",c);
                Collection<User> collection = userRepository.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 將用戶名、郵件、電話改成與現有完全不重複的,或者只與自己重複的,就不算衝突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}

這裏使用Predicate函數式接口對業務規則進行判斷。

使用

@RestController
@RequestMapping("/senior/user")
@Slf4j
@Validated
public class UserController {
    @Autowired
    private UserRepository userRepository;
    

    @PostMapping
    public User createUser(@UniqueUser @Valid User user){
        User savedUser = userRepository.save(user);
        log.info("save user id is {}",savedUser.getId());
        return savedUser;
    }

    @SneakyThrows
    @PutMapping
    public User updateUser(@NotConflictUser @Valid @RequestBody User user){
        User editUser = userRepository.save(user);
        log.info("update user is {}",editUser);
        return editUser;
    }
}

使用很簡單,只需要在方法上加入自定義註解即可,業務邏輯中不需要添加任何業務規則的代碼。

測試

調用接口後出現如下錯誤,說明業務規則校驗生效。

{
  "status": 400,
  "message": "用戶名、手機號碼、郵箱不允許與現存用戶重複",
  "data": null,
  "timestamp": 1644309081037
}

小結

通過上面幾步操作,業務校驗便和業務邏輯就完全分離開來,在需要校驗時用@Validated註解自動觸發,或者通過代碼手動觸發執行,可根據你們項目的要求,將這些註解應用於控制器、服務層、持久層等任何層次的代碼之中。

這種方式比任何業務規則校驗的方法都優雅,推薦大家在項目中使用。在開發時可以將不帶業務含義的格式校驗註解放到 Bean 的類定義之上,將帶業務邏輯的校驗放到 Bean 的類定義的外面。這兩者的區別是放在類定義中的註解能夠自動運行,而放到類外面則需要像前面代碼那樣,明確標出註解時纔會運行。

tips : 老鳥系列源碼已經上傳至GitHub,需要的關注公衆號Java日知錄並回復關鍵字 0923 獲取源碼地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章