SpringBoot參數(shù)驗證的幾種方式小結
1、為什么要進行參數(shù)驗證
在日常的接口開發(fā)中,為了防止非法參數(shù)對業(yè)務造成影響,經(jīng)常需要對接口的參數(shù)進行校驗,例如登錄的時候需要校驗用戶名和密碼是否為空,添加用戶的時候校驗用戶郵箱地址、手機號碼格式是否正確。 靠代碼對接口參數(shù)一個個校驗的話就太繁瑣了,代碼可讀性極差。
進行參數(shù)驗證是軟件開發(fā)中的一個重要環(huán)節(jié),其主要原因包括但不限于以下幾點:
- 數(shù)據(jù)完整性與準確性:確保接收到的數(shù)據(jù)是完整且準確的,避免因錯誤或惡意的數(shù)據(jù)輸入導致系統(tǒng)異?;驍?shù)據(jù)損壞。
- 安全防護:防止注入攻擊(如SQL注入)、跨站腳本攻擊(XSS)等安全威脅,通過驗證可以過濾掉非法輸入,增強系統(tǒng)安全性。
- 性能優(yōu)化:提前驗證參數(shù)可以減少不必要的數(shù)據(jù)庫查詢或業(yè)務邏輯執(zhí)行,從而提升系統(tǒng)整體性能。
- 用戶體驗:及時向用戶提供清晰的錯誤提示,指導他們正確輸入信息,避免提交表單后才告知錯誤,提高了用戶體驗。
- 代碼可維護性:集中處理參數(shù)驗證邏輯,使得業(yè)務邏輯代碼更加清晰,易于理解和維護。
- 遵循最佳實踐:參數(shù)驗證是編程和Web開發(fā)中的一個基本最佳實踐,遵循這些原則可以減少錯誤和漏洞,提升軟件質(zhì)量。
- 減少異常處理:通過前端和后端的雙重驗證,可以減少運行時異常的發(fā)生,使得程序更加穩(wěn)定可靠。
- 合規(guī)性:對于涉及用戶隱私或敏感信息的應用,參數(shù)驗證也是遵守數(shù)據(jù)保護法規(guī)(如GDPR)的一個重要方面。
因此,參數(shù)驗證是構建高質(zhì)量、安全、易用的應用程序不可或缺的一環(huán)。
2、驗證方式
2.1 if 語句判斷
@PostMapping("/parameterCheck") @Operation(summary = "參數(shù)校驗", description = "嵌套參數(shù)校驗-測試") public CommonResult<TestDto> parameterCheck(@RequestBody TestDto dto) { if (dto == null){ throw new RuntimeException("參數(shù)不能為空"); } return CommonResult.SUCCESS(dto); }
缺點:
- 代碼可讀性和維護性降低:當if條件復雜或嵌套層次過多時,代碼可讀性大大降低,使得維護和理解代碼變得更加困難。開發(fā)者可能需要花費更多時間去梳理邏輯關系。
- 容易出錯:復雜的if條件判斷容易出現(xiàn)邏輯錯誤,比如漏寫某個條件分支,或條件判斷邏輯失誤,導致程序行為不符合預期。
- 測試難度增加:if語句尤其是嵌套和多重if的情況下,會生成多個代碼路徑,這意味著需要編寫更多的測試用例來覆蓋所有可能的執(zhí)行路徑,增加了測試的復雜度和成本。
- 性能影響:雖然現(xiàn)代編譯器會對代碼進行優(yōu)化,但在某些情況下,特別是深度嵌套或大量if判斷時,可能會對程序的執(zhí)行效率產(chǎn)生負面影響,尤其是在循環(huán)內(nèi)部或者高頻調(diào)用的代碼塊中。
- 擴展性差:隨著需求變化,頻繁修改或增減if條件會使代碼結構變得混亂,不利于后期的擴展和修改。
- 難以調(diào)試:當if邏輯出錯時,定位問題可能比較困難,特別是在沒有明確錯誤信息或日志記錄的情況下。
因此,在設計代碼時,推薦采用諸如策略模式、狀態(tài)模式等設計模式來替代復雜的if判斷,或者使用Switch語句(在適用的情況下)來提高代碼的清晰度和可維護性。同時,也可以考慮利用函數(shù)式編程的思想,將邏輯分解為更小的、可重用的函數(shù),以提高代碼的模塊化程度。
2.2 Assert
@PostMapping("/parameterCheck") @Operation(summary = "參數(shù)校驗", description = "嵌套參數(shù)校驗-測試") public CommonResult<TestDto> parameterCheck(@RequestBody TestDto dto) { Assert.isNull(dto.getName(), "姓名不能為空"); Assert.isNull(dto.getSex(), "性別不能為空"); return CommonResult.SUCCESS(dto); }
使用Assert
語句進行參數(shù)校驗在Java等編程語言中較為常見,尤其是在單元測試中用于驗證預期結果。然而,在生產(chǎn)代碼中過度依賴Assert
進行參數(shù)校驗也存在一些缺點:
- 非異常處理機制:
Assert
主要用于開發(fā)階段的自我檢查,它拋出的是AssertionError
,這是一種錯誤而非異常。在默認的Java虛擬機設置下,生產(chǎn)環(huán)境通常不啟用斷言(即-ea
標志未設置),這意味著斷言不會執(zhí)行,從而無法起到參數(shù)校驗的作用。 - 用戶體驗不佳:即便在啟用了斷言的環(huán)境中,
AssertionError
通常是直接終止程序的,沒有被捕獲和處理的機制,這會導致程序突然崩潰,給用戶帶來不友好的體驗。 - 缺乏靈活性:
Assert
主要用于驗證程序內(nèi)部不變性條件,其信息更多服務于開發(fā)者調(diào)試,而不能提供豐富的錯誤信息反饋或自定義錯誤處理邏輯。 - 不利于維護和調(diào)試:由于
Assert
在生產(chǎn)環(huán)境中默認不啟用,可能導致某些錯誤在開發(fā)階段未被發(fā)現(xiàn),而在生產(chǎn)環(huán)境中因為不同的配置導致問題浮現(xiàn),增加了問題排查的難度。 - 不適用于所有類型的應用程序:對于要求高穩(wěn)定性和錯誤處理邏輯復雜的應用,直接使用
Assert
進行參數(shù)校驗并不合適,因為它缺乏控制異常流和提供恢復機制的能力。
2.3 Validator
Validator
框架就是為了解決開發(fā)人員在開發(fā)的時候少寫代碼,提升開發(fā)效率;Validator專門用來進行接口參數(shù)校驗,例如常見的必填校驗,email格式校驗,用戶名必須位于6到12之間等等。
2.3.1 引入依賴
注意:如果spring-boot版本小于2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴。如果spring-boot版本大于2.3.x,則需要手動引入依賴。我這里使用的SpringBoot版本是3.0.0,因此手動引入了。
<!-- 如果spring-boot版本小于2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴。如果spring-boot版本大于2.3.x,則需要手動引入依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2.3.2 定義參數(shù)實體類
常見的約束注解如下:
注解 | 功能 |
---|---|
@AssertFalse | 可以為null,如果不為null的話必須為false |
@AssertTrue | 可以為null,如果不為null的話必須為true |
@DecimalMax | 設置不能超過最大值 |
@DecimalMin | 設置不能超過最小值 |
@Digits | 設置必須是數(shù)字且數(shù)字整數(shù)的位數(shù)和小數(shù)的位數(shù)必須在指定范圍內(nèi) |
@Future | 日期必須在當前日期的未來 |
@Past | 日期必須在當前日期的過去 |
@Max | 最大不得超過此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能為null,可以是空 |
@Null | 必須為null |
@Pattern | 必須滿足指定的正則表達式 |
@Size | 集合、數(shù)組、map等的size()值必須在指定范圍內(nèi) |
必須是email格式 | |
@Length | 長度必須在指定范圍內(nèi) |
@NotBlank | 字符串不能為null,字符串trim()后也不能等于"" |
@NotEmpty | 不能為null,集合、數(shù)組、map等size()不能為0;字符串trim()后可以等于"" |
@Range | 值必須在指定范圍內(nèi) |
@URL | 必須是一個URL |
import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class TestDto { @NotBlank(message = "姓名不能為空") @Schema(description = "姓名") private String name; @NotNull(message = "年齡不能為空") @Schema(description = "年齡") @Min(value = 0, message = "年齡不能小于0") @Max(value = 200, message = "年齡不能大于200") private Integer age; //性別只允許為男或女 @NotBlank(message = "性別不能為空") @Pattern(regexp = "^(男|女)$", message = "性別必須為'男'或'女'") @Schema(description = "性別") private String sex; @Valid @Schema(description = "嵌套對象") private TestDtoObj testDtoObj; }
import com.example.demo.annotation.PhoneNumber; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.URL; import java.time.LocalDate; import java.time.LocalDateTime; @Data @AllArgsConstructor @NoArgsConstructor public class TestDtoObj { @PhoneNumber @NotBlank(message = "手機號1不能為空") @Schema(description = "手機號1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "無效的手機號碼格式") @NotBlank(message = "手機號不能為空") @Schema(description = "手機號2") private String phone2; @NotBlank(message = "密碼不能為空") @Size(min = 6, max = 16, message = "密碼長度必須在6到16個字符之間") @Schema(description = "密碼") private String password; @NotBlank(message = "郵箱不能為空") @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "郵箱格式不正確") @Schema(description = "郵箱") private String email; @Digits(integer = 4, fraction = 2, message = "整數(shù)位數(shù)必須在4位以內(nèi)小數(shù)位數(shù)必須在2位以內(nèi)") @Schema(description = "小數(shù)") private Double num; @URL(message = "url格式錯誤") @Schema(description = "地址") private String url; @Past(message = "日期必須為過去日期") @Schema(description = "過去日期") private LocalDate pastDate; @Future(message = "日期必須為將來日期") @Schema(description = "將來日期") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private LocalDateTime futureDate; }
2.3.3 定義特定異常全局攔截方法
Validator
框架 拋出的特定異常為MethodArgumentNotValidException,該異常會將我們在參數(shù)校驗注解自定義的message返回到e.getBindingResult().getFieldError().getDefaultMessage()中。
/** * 全局異常攔截 * * @author zyw */ @Slf4j @RestControllerAdvice public class BaseExceptionHandler { /** * 攔截參數(shù)校驗異常 * @param e * @param request * @return */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult<?> handleGlobalException(MethodArgumentNotValidException e, HttpServletRequest request) { log.error("請求地址'{}',發(fā)生系統(tǒng)異常'{}'", request.getRequestURI(), e.getBindingResult().getFieldError().getDefaultMessage()); return CommonResult.ECEPTION(ResultCode.PARAMETER_EXCEPTION, e.getBindingResult().getFieldError().getDefaultMessage()); } }
2.3.4 定義校驗類進行測試
import com.example.demo.config.CommonResult; import com.example.demo.model.dto.TestDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @Slf4j @RequestMapping("knife4j") @Tag(name = "knife4j測試控制器") public class Knife4jController { @PostMapping("/parameterCheck") @Operation(summary = "參數(shù)校驗", description = "嵌套參數(shù)校驗-測試") public CommonResult<TestDto> parameterCheck(@Validated @RequestBody TestDto dto) { return CommonResult.SUCCESS(dto); } }
2.3.5 測試
2.4 自定義驗證注解
在Java項目中,自定義注解是一種強大的功能,允許開發(fā)者創(chuàng)建自己的注解類型來滿足特定需求,比如驗證、日志記錄、性能監(jiān)控等。我們通過自定義注解修飾特定的接口、方法、屬性、類,可以實現(xiàn)更加靈活的功能。
2.4.1 定義自定義注解
import com.example.demo.uitls.validator.PhoneNumberValidator; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; /** * PhoneNumber : 手機號格式驗證注解 * 用于驗證電話號碼格式的注解。 * 該注解可以應用于字段或參數(shù)上,以驗證其是否為有效的電話號碼格式。 * 默認的錯誤消息是“無效的手機號碼格式”,但可以通過message屬性自定義。 * 可以通過groups和payload屬性來支持分組驗證和負載信息。 * * @Documented 標記此注解將被包含在文檔中。 * @Constraint 標記此注解為約束注解,并指定PhoneNumberValidator類作為驗證器。 * @Target 指定此注解可以應用于字段和參數(shù)上。 * @Retention 指定此注解在運行時保留。 * @author zyw * @create 2024-05-31 15:38 */ @Documented @Constraint(validatedBy = PhoneNumberValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { /** * 驗證失敗時的錯誤消息,默認為“無效的手機號碼格式”。 * 可以通過將此屬性設置為自定義錯誤消息來更改默認消息。 * * @return 驗證失敗時的錯誤消息。 */ String message() default "無效的手機號碼格式"; /** * 定義驗證的分組,默認為空組。 * 可以通過將此屬性設置為一個或多個分組類來指定字段應在哪些分組中進行驗證。 * * @return 驗證的分組類數(shù)組。 */ Class<?>[] groups() default {}; /** * 定義驗證的負載信息,默認為空負載。 * 可以通過將此屬性設置為一個或多個負載類來攜帶額外的驗證信息。 * * @return 驗證的負載信息類數(shù)組。 */ Class<? extends Payload>[] payload() default {}; }
2.4.2 定義自定義驗證器類
import com.example.demo.annotation.PhoneNumber; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; /** * PhoneNumberValidator : 手機號驗證器類 * * @author zyw * @create 2024-05-31 15:39 */ public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> { private static final String PHONE_PATTERN = "^1[3-9]\\d{9}$"; // 中國手機號碼的簡單正則表達式 @Override public boolean isValid(String phoneNumber, ConstraintValidatorContext context) { return phoneNumber != null && phoneNumber.matches(PHONE_PATTERN); } }
2.4.3 應用自定義注解
import com.example.demo.annotation.PhoneNumber; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.URL; import java.time.LocalDate; import java.time.LocalDateTime; /** * TestDtoObj : * * @author zyw * @create 2024-05-31 15:47 */ @Data @AllArgsConstructor @NoArgsConstructor public class TestDtoObj { @PhoneNumber @PhoneNumber(message = "手機號格式錯誤") @Schema(description = "手機號1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "無效的手機號碼格式") @NotBlank(message = "手機號不能為空") @Schema(description = "手機號2") private String phone2; }
@PhoneNumber @PhoneNumber(message = "手機號格式錯誤") @Schema(description = "手機號1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "無效的手機號碼格式") @NotBlank(message = "手機號不能為空") @Schema(description = "手機號2") private String phone2;
以上就是SpringBoot參數(shù)驗證的幾種方式小結的詳細內(nèi)容,更多關于SpringBoot參數(shù)驗證的資料請關注腳本之家其它相關文章!
相關文章
SpringBoot3中token攔截器鏈的設計與實現(xiàn)步驟
本文介紹了spring boot后端服務開發(fā)中有關如何設計攔截器的思路,文中通過代碼示例和圖文講解的非常詳細,具有一定的參考價值,需要的朋友可以參考下2024-03-03Java Comparable及Comparator接口區(qū)別詳解
這篇文章主要介紹了Java Comparable及Comparator接口區(qū)別詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07如何利用Java使用AOP實現(xiàn)數(shù)據(jù)字典轉(zhuǎn)換
這篇文章主要介紹了如何利用Java使用AOP實現(xiàn)數(shù)據(jù)字典轉(zhuǎn)換,AOP也是我們常說的面向切面編程,AOP在我們開發(fā)過程中應用也比較多,在這里我們就基于AOP來實現(xiàn)一個數(shù)據(jù)字典轉(zhuǎn)換的案例2022-06-06Java?Mybatis?foreach嵌套foreach?List<list<Object>&
在MyBatis的mapper.xml文件中,foreach元素常用于動態(tài)生成SQL查詢條件,此元素包括item(必選,元素別名)、index(可選,元素序號或鍵)、collection(必選,指定迭代對象)、open、separator、close(均為可選,用于定義SQL結構)2024-09-09