SpringBoot中實現業務校驗,這種方式才叫優雅!

語言: CN / TW / HK

大家好,我是飄渺。

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

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

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

引數校驗

引數校驗很好理解,比如登入的時候需要校驗使用者名稱密碼是否為空,建立使用者的時候需要校驗郵件、手機號碼格式是否準確。

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

常見的校驗註解有:

java @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邏輯中,常見的程式碼邏輯如下:

java 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:表示一個使用者是唯一的,唯一性包含:使用者名稱,手機號碼、郵箱

```java @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:表示一個使用者的資訊是無衝突的,無衝突是指該使用者的敏感資訊與其他使用者不重合

```java @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 它在啟動的時候就已經被載入到容器中了。

```java @Slf4j public class UserValidation implements ConstraintValidator {

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函式式介面對業務規則進行判斷。

使用

```java @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;
}

} ```

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

測試

呼叫介面後出現如下錯誤,說明業務規則校驗生效。

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

小結

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

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

tips : 老鳥系列原始碼已經上傳至GitHub,需要的關注本公眾號並回復關鍵字 0923 獲取原始碼地址。