SpringBoot 實現(xiàn)業(yè)務(wù)校驗,這種方式才叫優(yōu)雅!
Hollis的新書限時折扣中,一本深入講解Java基礎(chǔ)的干貨筆記!
在日常的接口開發(fā)中,為了保證接口的穩(wěn)定安全,我們一般需要在接口邏輯中處理兩種校驗:
參數(shù)校驗 業(yè)務(wù)規(guī)則校驗
首先我們先看看參數(shù)校驗。
參數(shù)校驗
參數(shù)校驗很好理解,比如登錄的時候需要校驗用戶名密碼是否為空,創(chuàng)建用戶的時候需要校驗郵件、手機號碼格式是否準確。
而實現(xiàn)參數(shù)校驗也非常簡單,我們只需要使用Bean Validation校驗框架即可,借助它提供的校驗注解我們可以非常方便的完成參數(shù)校驗。
常見的校驗注解有:
@Null、@NotNull、@AssertTrue、@AssertFalse、@Min、@Max、@DecimalMin、@DecimalMax、@Negative、@NegativeOrZero、@Positive、@PositiveOrZero、@Size、@Digits、@Past、@PastOrPresent、@Future、@FutureOrPresent、@Pattern、@NotEmpty、@NotBlank、@Email接下來我們再看看業(yè)務(wù)規(guī)則校驗。
業(yè)務(wù)規(guī)則校驗
業(yè)務(wù)規(guī)則校驗指接口需要滿足某些特定的業(yè)務(wù)規(guī)則,舉個例子:業(yè)務(wù)系統(tǒng)的用戶需要保證其唯一性,用戶屬性不能與其他用戶產(chǎn)生沖突,不允許與數(shù)據(jù)庫中任何已有用戶的用戶名稱、手機號碼、郵箱產(chǎn)生重復(fù)。
這就要求在創(chuàng)建用戶時需要校驗用戶名稱、手機號碼、郵箱是否被注冊;編輯用戶時不能將信息修改成已有用戶的屬性。
95%的程序員當面對這種業(yè)務(wù)規(guī)則校驗時往往選擇寫在service邏輯中,常見的代碼邏輯如下:
public?void?create(User?user)?{
????Account?account?=?accountDao.queryByUserNameOrPhoneOrEmail(user.getName(),user.getPhone(),user.getEmail());
????if?(account?!=?null)?{
????????throw?new?IllegalArgumentException("用戶已存在,請重新輸入");
????}
}
雖然我在上一篇文章中介紹了使用Assert來優(yōu)化代碼可以使其看上去更簡潔,但是將簡單的校驗交給 Bean Validation,而把復(fù)雜的校驗留給自己,這簡直是買櫝還珠故事的程序員版本。

最優(yōu)雅的實現(xiàn)方法應(yīng)該是參考 Bean Validation 的標準方式,借助自定義校驗注解完成業(yè)務(wù)規(guī)則校驗。
接下來我們通過上面提到的用戶接口案例,通過自定義注解完成業(yè)務(wù)規(guī)則校驗。
代碼實戰(zhàn)
需求很容易理解,注冊新用戶時,應(yīng)約束不與任何已有用戶的關(guān)鍵信息重復(fù);而修改自己的信息時,只能與自己的信息重復(fù),不允許修改成已有用戶的信息。
這些約束規(guī)則不僅僅為這兩個方法服務(wù),它們可能會在用戶資源中的其他入口被使用到,乃至在其他分層的代碼中被使用到,在 Bean 上做校驗就能全部覆蓋上述這些使用場景。
自定義注解
首先我們需要創(chuàng)建兩個自定義注解,用于業(yè)務(wù)規(guī)則校驗:
UniqueUser:表示一個用戶是唯一的,唯一性包含:用戶名,手機號碼、郵箱
@Documented
@Retention(RUNTIME)
@Target({FIELD,?METHOD,?PARAMETER,?TYPE})
@Constraint(validatedBy?=?UserValidation.UniqueUserValidator.class)
public?@interface?UniqueUser?{
????String?message()?default?"用戶名、手機號碼、郵箱不允許與現(xiàn)存用戶重復(fù)";
????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?"用戶名稱、郵箱、手機號碼與現(xiàn)存用戶產(chǎn)生重復(fù)";
????Class>[]?groups()?default?{};
????Class?extends?Payload>[]?payload()?default?{};
}
實現(xiàn)業(yè)務(wù)校驗規(guī)則
想讓自定義驗證注解生效,需要實現(xiàn)?ConstraintValidator?接口。接口的第一個參數(shù)是?自定義注解類型,第二個參數(shù)是?被注解字段的類,因為需要校驗多個參數(shù),我們直接傳入用戶對象。需要提到的一點是?ConstraintValidator?接口的實現(xiàn)類無需添加?@Component?它在啟動的時候就已經(jīng)被加載到容器中了。
@Slf4j
public?class?UserValidation<T?extends?Annotation>?implements?ConstraintValidator<T,?User>?{
????protected?Predicate?predicate?=?c?->?true;
????@Resource
????protected?UserRepository?userRepository;
????@Override
????public?boolean?isValid(User?user,?ConstraintValidatorContext?constraintValidatorContext)?{
????????return?userRepository?==?null?||?predicate.test(user);
????}
????/**
?????*?校驗用戶是否唯一
?????*?即判斷數(shù)據(jù)庫是否存在當前新用戶的信息,如用戶名,手機,郵箱
?????*/
????public?static?class?UniqueUserValidator?extends?UserValidation<UniqueUser>{
????????@Override
????????public?void?initialize(UniqueUser?uniqueUser)?{
????????????predicate?=?c?->?!userRepository.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
????????}
????}
????/**
?????*?校驗是否與其他用戶沖突
?????*?將用戶名、郵件、電話改成與現(xiàn)有完全不重復(fù)的,或者只與自己重復(fù)的,就不算沖突
?????*/
????public?static?class?NotConflictUserValidator?extends?UserValidation<NotConflictUser>{
????????@Override
????????public?void?initialize(NotConflictUser?notConflictUser)?{
????????????predicate?=?c?->?{
????????????????log.info("user?detail?is?{}",c);
????????????????Collection?collection?=?userRepository.findByUserNameOrEmailOrTelphone(c.getUserName(),?c.getEmail(),?c.getTelphone());
????????????????//?將用戶名、郵件、電話改成與現(xiàn)有完全不重復(fù)的,或者只與自己重復(fù)的,就不算沖突
????????????????return?collection.isEmpty()?||?(collection.size()?==?1?&&?collection.iterator().next().getId().equals(c.getId()));
????????????};
????????}
????}
}
這里使用Predicate函數(shù)式接口對業(yè)務(wù)規(guī)則進行判斷。
使用
@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;
????}
}
使用很簡單,只需要在方法上加入自定義注解即可,業(yè)務(wù)邏輯中不需要添加任何業(yè)務(wù)規(guī)則的代碼。
測試
調(diào)用接口后出現(xiàn)如下錯誤,說明業(yè)務(wù)規(guī)則校驗生效。
{
??"status":?400,
??"message":?"用戶名、手機號碼、郵箱不允許與現(xiàn)存用戶重復(fù)",
??"data":?null,
??"timestamp":?1644309081037
}
小結(jié)
通過上面幾步操作,業(yè)務(wù)校驗便和業(yè)務(wù)邏輯就完全分離開來,在需要校驗時用@Validated注解自動觸發(fā),或者通過代碼手動觸發(fā)執(zhí)行,可根據(jù)你們項目的要求,將這些注解應(yīng)用于控制器、服務(wù)層、持久層等任何層次的代碼之中。
這種方式比任何業(yè)務(wù)規(guī)則校驗的方法都優(yōu)雅,推薦大家在項目中使用。在開發(fā)時可以將不帶業(yè)務(wù)含義的格式校驗注解放到 Bean 的類定義之上,將帶業(yè)務(wù)邏輯的校驗放到 Bean 的類定義的外面。這兩者的區(qū)別是放在類定義中的注解能夠自動運行,而放到類外面則需要像前面代碼那樣,明確標出注解時才會運行。
完
往期推薦

Gitee 宣布開源須人工審核,大量開源項目被轉(zhuǎn)為私有!

報告出爐:清北畢業(yè)生都去哪了?

面試官:你天天用 Lombok,說說它什么原理?我竟然答不上來…
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
