大家好,我是飄渺。
在日常的介面開發中,為了保證介面的穩定安全,我們一般需要在介面邏輯中處理兩種校驗:
- 引數校驗
- 業務規則校驗
首先我們先看看引數校驗。
引數校驗
引數校驗很好理解,比如登入的時候需要校驗使用者名稱密碼是否為空,建立使用者的時候需要校驗郵件、手機號碼格式是否準確。
而實現引數校驗也非常簡單,我們只需要使用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,而把複雜的校驗留給自己,這簡直是買櫝還珠故事的程式設計師版本。
最優雅的實現方法應該是參考 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 獲取原始碼地址。