boot-admin開源項目中有關後端參數校驗的最佳實踐

来源:https://www.cnblogs.com/soft1314/archive/2023/05/07/17380059.html
-Advertisement-
Play Games

我們在項目開發中,經常會對一些參數進行校驗,比如非空校驗、長度校驗,以及定製的業務校驗規則等,如果使用if/else語句來對請求的每一個參數一一校驗,就會出現大量與業務邏輯無關的代碼,繁重不堪且繁瑣的校驗,會大大降低我們的工作效率,而且準確性也無法保證。為保證數據的正確性、完整性,前後端都需要進行數 ...


我們在項目開發中,經常會對一些參數進行校驗,比如非空校驗、長度校驗,以及定製的業務校驗規則等,如果使用if/else語句來對請求的每一個參數一一校驗,就會出現大量與業務邏輯無關的代碼,繁重不堪且繁瑣的校驗,會大大降低我們的工作效率,而且準確性也無法保證。為保證數據的正確性、完整性,前後端都需要進行數據檢驗。本文對開源 boot-admin 項目的後端校驗實踐進行總結,以饗碼友。
boot-admin 是一款採用前後端分離模式、基於 SpringCloud 微服務架構的SaaS後臺管理框架。系統內置基礎管理、許可權管理、運行管理、定義管理、代碼生成器和辦公管理6個功能模塊,集成分散式事務 Seata、工作流引擎 Flowable、業務規則引擎 Drools、後臺作業調度框架 Quartz 等,技術棧包括 Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。

項目源碼倉庫github
項目源碼倉庫gitee

引入Maven依賴

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

參數校驗實踐

定義校驗對象

@Data
/** 組合校驗註解(方式1) **/
@OverallValid(value = "check1" ,message="女士不得小於16歲。")
@OverallValid(value = "check2" ,message="男士不得小於18歲。")
public class User {
    //字元個數檢測(內置註解)
    @Size(min = 1,max = 10,message = "姓名長度必須為1到10")
    //占用空間長度檢測(自定義註解)
    @StringLength(min = 1,max = 12,message = "姓名的保存長度不允許超過12個位元組。")
    private String name;
    //利用枚舉類檢測(自定義註解)
    @EnumValid(target = SexEnum.class, message = "性別的取值範圍是【1】和【2】")
    private String sex;
    //註意 @NotNull @NotEmpty @NotBlank 的區別
    @NotBlank(message = "姓氏是必填項。")
    private String firstName;

    @Min(value = 10,message = "年齡最小為10")
    @Max(value = 100,message = "年齡最大為100")
    private Integer age;

    @Past(message = "出生時間必須為過去時間")
    private Date birth;

    @NotEmpty(message = "興趣不能為空")
    private List<String> interest;

    //嵌套檢測
    @Valid
    private List<User> children;
    @Valid
    private User father;
    @Valid
    private User mother;

    /** 組合校驗(方式2) **/
    @BooleanValid(message = "男性年齡需在60歲以下")
    public boolean getValid1(){
        if(sex.equalsIgnoreCase("1") && age >= 60 ){
            return false;
        }
        return true;
    }
    /** 組合校驗(方式2) **/
    @BooleanValid(message = "女性年齡需在55歲以下")
    public boolean getValid2(){
        if(sex.equalsIgnoreCase("2") && age >= 55 ){
            return false;
        }
        return true;
    }
    /** 組合校驗(方式1)方法 **/
    public boolean check1(){
        if(sex.equalsIgnoreCase("2") && age < 16 ){
            return false;
        }
        return true;
    }
    /** 組合校驗(方式1)方法 **/
    public boolean check2(){
        if(sex.equalsIgnoreCase("1") && age < 18 ){
            return false;
        }
        return true;
    }
}

相關枚舉類:

public enum SexEnum {
    男("1"),女("2");
    private final String value;
    SexEnum(String value) {
        this.value = value;
    }
    public String getValue() {
        return value;
    }
}

參數校驗(在 Controller 中使用)

@RestController
@RequestMapping("/api/system")
@Slf4j
public class DemoController {
    //註入校驗信息採集器
    @Resource
    private FormValidator formValidator;

    @PostMapping("/free/user/check")
    public ResultDTO check(@Valid @RequestBody User user, BindingResult bindingResult, HttpServletRequest request) throws Exception{
        /** 參數校驗 **/
        if (bindingResult.hasErrors()) {
            return formValidator.generateMessage(bindingResult);
        }
        /** 繼續執行業務邏輯 **/
        return ResultDTO.success();
    }
}

在Controller中使用的校驗結果信息採集器實現

介面定義:

public interface FormValidator {
    ResultDTO generateMessage(BindingResult bindingResult) throws Exception;
}

類實現:

@Service
@Slf4j
public class FormValidatorImpl implements FormValidator {
    @Override
    public ResultDTO generateMessage(BindingResult bindingResult) throws Exception {
        String msg = this.getMessage(bindingResult);
        return ResultDTO.failureCustom(msg);
    }
    /**
     * 生成校驗結果
     * @param bindingResult
     * @return
     */
    private String getMessage(BindingResult bindingResult){
        log.info(bindingResult.toString());
        List<ObjectError> objectErrorList=bindingResult.getAllErrors();
        String msg= this.getFormValidErrsMsgNoBr(objectErrorList);
        log.info(msg);
        return msg;
    }
    private String getFormValidErrsMsgNoBr(List<ObjectError> objectErrorList) {
        if (objectErrorList==null) {
            return "";
        }
        StringBuffer csv = new StringBuffer();
        csv.append("數據驗證未通過:[");
        for (int i = 0; i < objectErrorList.size(); i++){
            if (i > 0){
                csv.append("],[");
            }
            csv.append(objectErrorList.get(i).getDefaultMessage());
        }
        csv.append("]");
        return csv.toString();
    }
}

相關註解介紹

JSR-303 規範常用註解

以下列舉常用內置註解,可直接使用。

註解 描述
@Valid 對po實體盡心校驗
@AssertFalse 所註解的元素必須是Boolean類型,且值為false
@AssertTrue 所註解的元素必須是Boolean類型,且值為true
@DecimalMax 所註解的元素必須是數字,且值小於等於給定的值
@DecimalMin 所註解的元素必須是數字,且值大於等於給定的值
@Digits 所註解的元素必須是數字,且值必須是指定的位數
@Future 所註解的元素必須是將來某個日期
@Max 所註解的元素必須是數字,且值小於等於給定的值
@Min 所註解的元素必須是數字,且值大於等於給定的值
@Range 所註解的元素需在指定範圍區間內
@NotNull 所註解的元素值不能為null
@NotBlank 所註解的元素值有內容
@Null 所註解的元素值為null
@Past 所註解的元素必須是某個過去的日期
@PastOrPresent 所註解的元素必須是過去某個或現在日期
@Pattern 所註解的元素必須滿足給定的正則表達式
@Size 所註解的元素必須是String、集合或數組,且長度大小需保證在給定範圍之內
@Email 所註解的元素需滿足Email格式

自定義註解

僅僅使用內置的註解,無法滿足複雜的業務需求,故擴展下麵幾個自定義註解。

UTF-8 字元串長度校驗

對字元串長度的校驗目的,一般是用於保證數據表欄位可以容納,當字元串內容是中文時,內置的 @Size 是不適用的,此時就需要自行擴展 UTF-8 字元串長度校驗。
註解類:

@Target( {
        METHOD,
        FIELD,
        ANNOTATION_TYPE,
        CONSTRUCTOR,
        PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {StringLengthValidator.class})
public @interface StringLength {
    int max() default 4000;
    int min() default 0;
    String message() default "字元串長度不符合要求。";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

註解類實現:

@Slf4j
public class StringLengthValidator implements ConstraintValidator<StringLength, String> {
    private int max;
    private int min;
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        try {
            if(StringUtils.isBlank(value)){
                if(min > 0){
                    return false;
                }else {
                    return true;
                }
            }
            byte[] tmpbyte = value.getBytes("UTF-8");
            int length = tmpbyte.length;
            if(length < min || length > max){
                return false;
            }
            return true;
        }catch (Exception ex){
            log.error("註解校驗StringLength發生異常。");
            log.error(ex.getMessage(),ex);
            return false;
        }
    }
    @Override
    public void initialize(StringLength constraintAnnotation) {
        max = constraintAnnotation.max();
        min = constraintAnnotation.min();
    }
}

手機號碼校驗

註解類:

@Target( {
        METHOD,
        FIELD,
        ANNOTATION_TYPE,
        CONSTRUCTOR,
        PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {
    String regexp() default "";
    String message() default "手機號碼格式不正確";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

註解類實現:

public class MobileValidator implements ConstraintValidator<Mobile, String> {
    /**
     * 手機號的正則表達式.
     */
    private static Pattern pattern = Pattern.compile(
            "^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$");
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        Matcher m = pattern.matcher(value);
        return m.matches();
    }
    @Override
    public void initialize(Mobile constraintAnnotation) {}
}

這裡對手機號碼的校驗使用了正則表達式,也可以直接使用內置註解 @Pattern 定義校驗規則。

枚舉類整數值校驗

有時需要校驗參數值必須是系統定義的枚舉值(整數值),此時需要擴展以下註解。
註解類:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumIntegerValidator.class})
public @interface EnumIntegerValid {
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**     * 目標枚舉類     */
    Class<?> target() default Class.class;
    /**     * 是否忽略空值     */
    boolean ignoreEmpty() default true;
}

註解類實現:

@Slf4j
public class EnumIntegerValidator implements ConstraintValidator<EnumIntegerValid, Integer> {
    /** 枚舉校驗註解 */
    private EnumIntegerValid annotation;
    @Override
    public void initialize(EnumIntegerValid constraintAnnotation) {
        annotation = constraintAnnotation;
    }
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        boolean result = false;
        Class<?> cls = annotation.target();
        boolean ignoreEmpty = annotation.ignoreEmpty();
        // target為枚舉,並且value有值,或者不忽視空值,才進行校驗
        if (cls.isEnum() && value != null) {
            Object[] objects = cls.getEnumConstants();
            try {
                Method method = cls.getMethod("getValue");
                for (Object obj : objects) {
                    Object code = method.invoke(obj);
                    if (value.compareTo((Integer) code) == 0) {
                        result = true;
                        break;
                    }
                }
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                log.warn("EnumValidator call isValid() method exception.");
                result = false;
            }
        } else {
            result = true;
        }
        return result;
    }
}

枚舉類字元串校驗

有時需要校驗參數值必須是系統定義的枚舉值(字元串),此時需要擴展以下註解。
註解類:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValidator.class})
public @interface EnumValid {
    String message() default "";
       Class<?>[] groups() default {};
       Class<? extends Payload>[] payload() default {};
       /**     * 目標枚舉類     */
       Class<?> target() default Class.class;
       /**     * 是否忽略空值     */
       boolean ignoreEmpty() default true;
}

註解類實現:

@Slf4j
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
    /** 枚舉校驗註解 */
    private EnumValid annotation;
    @Override
    public void initialize(EnumValid constraintAnnotation) {
        annotation = constraintAnnotation;
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        boolean result = false;
        Class<?> cls = annotation.target();
        boolean ignoreEmpty = annotation.ignoreEmpty();
        // target為枚舉,並且value有值,或者不忽視空值,才進行校驗
        boolean fitCheck = cls.isEnum() && (isNotEmpty(value) || !ignoreEmpty);
        if (fitCheck) {
            Object[] objects = cls.getEnumConstants();
            try {
                Method method = cls.getMethod("getValue");
                for (Object obj : objects) {
                    Object code = method.invoke(obj);
                    if (value.equals(code.toString())) {
                        result = true;
                        break;
                    }
                }
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                log.warn("EnumValidator call isValid() method exception.");
                result = false;
            }
        } else {
            result = true;
        }
        return result;
    }
}

Bean 內多屬性組合校驗(組合校驗)

此類校驗一般屬於業務邏輯校驗,常常要求多個屬性符合一定的邏輯設定。此時需要在Bean中編寫校驗方法,併在類定義前面添加自定義註解 @OverallValid 或者在方法前面加上自定義註解 @BooleanValid

方式1:

註解在類定義前面,類方法要求:

  1. 方法的可訪問屬性:public
  2. 方法的返回類型: boolean
    @OverallValid註解類:
@Target({METHOD, FIELD,TYPE})
@Retention(RUNTIME)
@Repeatable(OverallValids.class)
@Documented
@Constraint(validatedBy = {OverallValidImpl.class})
public @interface OverallValid {
    String value() default "overallValid";
    String message() default "組合校驗未通過。";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

上面註解要求可重覆使用,使用了 @Repeatable(OverallValids.class),OverallValids 代碼如下:

@Target({METHOD, FIELD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OverallValids {
    OverallValid[] value();
}

使用註入的方法名,通過反射執行該方法,得到校驗結果。註解實現如下:

@Slf4j
public class OverallValidImpl implements ConstraintValidator<OverallValid, Object> {
    private String functionName;
    @Override
    public void initialize(OverallValid overallValid) {
        functionName = overallValid.value();
    }
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        try {
            //得到方法對象
            Method checkMethod = o.getClass().getMethod(functionName);
            //調用方法,得到返回值
            Object checkRet = checkMethod.invoke(o);
            return Boolean.valueOf(checkRet.toString());
        }catch (Exception ex){
            log.error("綜合校驗異常。");
            log.error(ex.getMessage(),ex);
        }
        return false;
    }
}

方式2:

註解在方法前面,類方法要求:

  1. 方法的可訪問屬性:public
  2. 方法的返回類型: boolean
  3. 方法名稱格式:get+首字母大寫駝峰,如 getValid1

@BooleanValid註解類:

@Target( {
        METHOD,
        FIELD,
        ANNOTATION_TYPE,
        CONSTRUCTOR,
        PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {BooleanValidImpl.class})
public @interface BooleanValid {
    boolean value() default true;
    String message() default "綜合校驗未通過。";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

類實現:

@Slf4j
public class BooleanValidImpl implements ConstraintValidator<BooleanValid, Boolean> {
    @Override
    public boolean isValid(Boolean value, ConstraintValidatorContext constraintValidatorContext) {
        return value;
    }
    @Override
    public void initialize(BooleanValid constraintAnnotation) {
    }
}

嵌套校驗

在成員屬性上加註解 @Valid ,意味著對該成員屬性進行嵌套校驗,校驗規則按該成員的內部校驗註解執行。

本文來自博客園,作者:超然樓,轉載請註明原文鏈接:https://www.cnblogs.com/soft1314/p/17380059.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 家人們,這圖片到底怎樣才能完全填滿div啊,想問度娘結果搜索的問題都亂七八糟的 (怎麼那麼多問題QAQ),描述問題都描述不來 具體問題如下:圖片有自己的解析度大小,例如寬100px,高100px,將圖片添加到div中: <div class="xx"> <img src="xxx"> </div> ...
  • 大佬們呀,花了好幾天的時間總算是看著頁面展示可以了,求賜教! 小米商城主頁,對大佬來說肯定簡單爆了,我抄寫了好久呀,總是有一點點的小問題,還搞不明白 主要是一個靜態的小米商城頁面,HTML前端代碼不複雜,一堆的div和ul、li就沒問題了,主要是css代碼搞不懂導致顯示不正常 話不多說,直接上代碼: ...
  • 前端開發中涉及表單的頁面非常多,看似功能簡單,開發快速,實則占去了很大一部分時間。當某個表單包含元素過多時還會導致html代碼過多,vue文件過大。從而不容易查找、修改和維護。為了提高開發效率及降低維護成本,下麵介紹表單配置化組件的封裝原理與封裝方法。 ...
  • 環境:CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 Python版本:3.9.12 一、背景描述 PCM(Pulse Code Modulation,脈衝編碼調製)音頻數據是未經壓縮的音頻採樣數據,它是由模擬信號經過採樣、量化、編碼轉換成的標準數字音頻數據。 在FreeSW ...
  • 架構介紹 系統組件 CAS伺服器和客戶端構成了CAS系統體繫結構的兩個物理組件,它們通過各種協議進行通信。 CAS伺服器 CAS伺服器是基於Spring Framework構建的Java servlet,其主要職責是通過簽發和驗證ticket來驗證用戶並授予對啟用CAS認證了的服務(通常稱為CAS客 ...
  • pandas中用來承載數據的兩個最重要的結構分別是: Series:相當於增強版的一維數組 DataFrame:相當於增強版的二維數組 pandas最大的優勢在於處理表格類數據,如果數據維度超過二維,一般我們會使用另一個 python的庫 numpy。 本篇主要介紹這兩種核心數據結構的創建方式。 1 ...
  • 概述 由於gpt比較火爆,可以幫我們寫文章、寫代碼等,本文將製作屬於我們自己的ai助手,無需翻牆, 準備工作 在萬能的某寶購買open ai的apikey,直接搜索apikey即可找到 下載Andlua+軟體,在後臺回覆【andlua】即可獲得下載鏈接 最終效果 代碼實現 main.lua主要代碼 ...
  • 過去若幹年,一邊工作編程,一邊思考提煉,寫了一些關於“寫整潔業務代碼”的文章,在隨筆分類“代碼修行”下。有一天在公司文檔空間分享時,突然想到:可以製作一本電子書,將過往的重要經驗總結起來,也是對自己十年編程生涯的一個階段性回顧,作為繼續前進的階梯。 我的第一本電子書 書名:《代碼修行:一步一步寫出整 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...