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