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

JAVA日知錄發表於2023-04-04

大家好,我是飄渺。

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

  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 的類定義的外面。這兩者的區別是放在類定義中的註解能夠自動執行,而放到類外面則需要像前面程式碼那樣,明確標出註解時才會執行。

老鳥系列原始碼已經上傳至GitHub,需要的在公號【JAVA日知錄】回覆關鍵字 0923 獲取原始碼地址。

相關文章