【超實用攻略】SpringBoot + validator 輕鬆實現全註解式的引數校驗

志哥聊技术發表於2024-07-24

一、故事背景

關於引數合法性驗證的重要性就不多說了,即使前端對引數做了基本驗證,後端依然也需要進行驗證,以防不合規的資料直接進入伺服器,如果不對其進行攔截,嚴重的甚至會造成系統直接崩潰

本文結合自己在專案中的實際使用經驗,主要以實用為主,對資料合法性驗證做一次總結,不瞭解的朋友可以學習一下,同時可以立馬實踐到專案上去。

下面我們透過幾個示例來演示如何判斷引數是否合法,廢話不多說,直接擼起來!

二、斷言驗證

對於引數的合法性驗證,最初的做法比較簡單,自定義一個異常類。

public class CommonException extends RuntimeException {

    private Integer code;

    public Integer getCode() {
        return code;
    }

    public CommonException(String message) {
        super(message);
        this.code = 500;
    }

    public CommonException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

當檢查到某個引數不合法的時候,直接拋異常!

@RestController
public class HelloController {

    @RequestMapping("/upload")
    public void upload(MultipartFile file) {
        if (file == null) {
            throw new CommonException("請選擇上傳檔案!");
        }
        //.....
    }
}

最後寫一個統一異常攔截器,對拋異常的邏輯進行兜底處理。

這種做法比較簡單直觀,如果當前引數既要判斷是否為空,又要判斷長度是否超過最大限制的時候,程式碼就會顯得很臃腫,而且複用性很差

於是,程式界的大佬想到了一個更加優雅又能節省程式碼的方式,建立一個斷言類工具類,專門用來判斷引數的是否合法,如果不合法就拋異常,示例如下:

/**
 * 斷言工具類
 */
public abstract class LocalAssert {
    
    public static void isTrue(boolean expression, String message) throws CommonException {
        if (!expression) {
            throw new CommonException(message);
        }
    }
    public static void isStringEmpty(String param, String message) throws CommonException{
        if(StringUtils.isEmpty(param)) {
            throw new CommonException(message);
        }
    }

    public static void isObjectEmpty(Object object, String message) throws CommonException {
        if (object == null) {
            throw new CommonException(message);
        }
    }

    public static void isCollectionEmpty(Collection coll, String message) throws CommonException {
        if (coll == null || (coll.size() == 0)) {
            throw new CommonException(message);
        }
    }
}

當我們需要對引數進行驗證的時候,直接透過這個類就可以完成,示例如下:

@RestController
public class HelloController {

    @RequestMapping("/save")
    public void save(String name, String email) {
        LocalAssert.isStringEmpty(name, "使用者名稱不能為空!");
        LocalAssert.isStringEmpty(email, "郵箱不能為空!");
        
        //.....
    }
}

相比上面的實現方式,這種處理邏輯,程式碼明顯要簡潔的多!

類似這樣的工具類還很多,比如spring也提供了一個名為Assert的斷言工具類,在開發的時候,可以直接使用!

三、註解驗證

下面我們要介紹的是另一種更簡潔的引數驗證邏輯,使用註解來對資料進行合法性驗證,不僅程式碼會變得很簡潔,閱讀起來也十分令人賞心悅目!

以 Spring Boot 工程為例,下面我們一起來看看具體的實踐方式。

3.1、新增依賴包

首先在pom.xml中引入spring-boot-starter-web依賴包即可,它會自動將註解驗證相關的依賴包打入工程!

<!-- spring boot web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2、編寫註解校驗請求物件

接著建立一個實體User,用於封裝使用者註冊時的請求引數,並在引數屬性上新增對應的註解驗證規則

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class User {

    @NotBlank(message = "使用者名稱不能為空!")
    private String userName;

    @Email(message = "郵箱格式不正確")
    @NotBlank(message = "郵箱不能為空!")
    private String email;

    @NotBlank(message = "密碼不能為空!")
    @Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
    private String userPwd;

    @NotBlank(message = "確認密碼不能為空!")
    private String confirmPwd;

    // set、get方法等...
}

3.3、編寫請求介面

web層建立一個register()註冊介面方法,同時在請求引數上新增@Valid註解,示例如下:

import javax.validation.Valid;

@RestController
public class UserController {

    @RequestMapping("/register")
    public ResultMsg register(@RequestBody @Valid User user){
        if(!user.getUserPwd().equals(user.getConfirmPwd())){
            throw new CommonException(4001, "確認密碼與密碼不相同,請確認!");
        }
        //業務處理...
        return ResultMsg.success();
    }
}

3.4、編寫全域性異常處理器

最後自定義一個異常全域性處理器,用於處理異常邏輯,如下:

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 攔截Controller層的異常
     * @param e
     * @return
     */
    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    public Object exceptionHandler(HttpServletRequest request, Exception e){
        LOGGER.error("【統一異常攔截】請求地址:{}, 錯誤資訊:{}", request.getRequestURI(), e.getMessage());
        // 註解驗證丟擲的異常
        if(e instanceof MethodArgumentNotValidException){
            // 獲取錯誤資訊
            String error = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
            return ResultMsg.fail(500, error);
        }
        // 自定義丟擲的異常
        if(e instanceof CommonException){
            return ResultMsg.fail(((CommonException) e).getCode(), e.getMessage());
        }
        return ResultMsg.fail(999, e.getMessage());
    }
}

統一響應物件ResultMsg,如下:

public class ResultMsg<T> {

    /**狀態碼**/
    private int code;

    /**結果描述**/
    private String message;

    /**結果集**/
    private T data;

    /**時間戳**/
    private long timestamp;

    // set、get方法等...
}

3.5、服務測試

啟動專案,使用postman來驗證一下程式碼的正確性,看看效果如何?

  • 測試欄位是否為空

  • 測試郵箱是否合法

  • 測試密碼長度是否符合要求

  • 測試密碼與確認密碼是否相同

可以看到,驗證結果與預期一致!

四、自定義註解驗證

事實上,熟悉 SpringMVC 原始碼的同學可能知道,Spring Boot 內建了一個hibernate-validator校驗元件,上文就是利用它來完成對請求時入參上的註解驗證。

預設的情況下,依賴包已經給我們提供了非常多的校驗註解,如下!

  • JSR 提供的校驗註解!

  • Hibernate Validator 提供的校驗註解

但是某些情況,例如性別這個引數,可能需要我們自己去手動驗證。

針對這種情況,我們也可以自定義一個註解來完成引數的校驗,也便於進一步瞭解註解驗證的原理。

自定義註解驗證,實現方式如下!

首先,建立一個Sex註解。

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = SexValidator.class)
@Documented
public @interface Sex {

    String message() default "性別值不在可選範圍內";

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

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

然後,建立一個SexValidator類,實現自ConstraintValidator介面

public class SexValidator implements ConstraintValidator<Sex, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Set<String> sexSet = new HashSet<String>();
        sexSet.add("男");
        sexSet.add("女");
        return sexSet.contains(value);
    }
}

最後,在User實體類上加入一個性別引數,使用自定義註解進行校驗!

public class User {

    @NotBlank(message = "使用者名稱不能為空!")
    private String userName;

    @Email(message = "郵箱格式不正確")
    @NotBlank(message = "郵箱不能為空!")
    private String email;

    @NotBlank(message = "密碼不能為空!")
    @Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
    private String userPwd;

    /**
     * 自定義註解校驗
     */
    @Sex(message = "性別輸入有誤!")
    private String sex;

    // set、get方法等...
}

啟動服務,重新請求,執行結果如下:

結果與預期一致!

五、總結

引數驗證,在開發中使用非常頻繁,如何優雅的進行驗證,讓程式碼變得更加可讀,是業界大佬一直在追求的目標!

本文主要圍繞在 Spring Boot 中實現引數統一驗證進行相關的知識總結和介紹,如果有描述不對的地方,歡迎留言支援。

示例程式碼:spring-boot-example-valid

相關文章