Spring Boot
提供了內建的驗證註解,可以幫助簡單、快速地對輸入欄位進行驗證,例如檢查null 或空白欄位、強制執行長度限制、使用正規表示式驗證模式以及驗證電子郵件地址。
一些最常用的驗證註解包括:
@NotNull
:指定欄位不能為空。
@NotEmpty
:指定清單欄位不能為空。
@NotBlank
:指定字串欄位不得為空或僅包含空格。
@Min
和 @Max
:指定數字欄位的最小值和最大值。
@Pattern
:指定字串欄位必須符合的正規表示式模式。
@Email
:指定字串欄位必須是有效的電子郵件地址。
具體用法參考下面範例:
public class User { @NotNull private Long id; @NotBlank @Size(min = 2, max = 50) private String firstName; @NotBlank @Size(min = 2, max = 50) private String lastName; @Email private String email; @NotNull @Min(18) @Max(99) private Integer age; @NotEmpty private List<String> hobbies; @Pattern(regexp = "[A-Z]{2}\d{4}") private String employeeId;
雖然Spring Boot 的內建驗證註解很有用,但它們可能無法涵蓋所有情況。如果有特殊參數驗證的場景,可以使用 Spring 的 JSR 303 驗證框架建立自訂驗證註解。自訂註解可以讓你的驗證邏輯更具可重複使用和可維護性。
假設我們有一個應用程序,用戶可以在其中建立帖子。每個帖子都應該有一個標題和一個正文,並且標題在所有帖子中應該是唯一的。雖然 Spring Boot 提供了用於檢查欄位是否為空的內建驗證註釋,但它沒有提供用於檢查唯一性的內建驗證註釋。在這種情況下,我們可以建立一個自訂驗證註解來處理這種情況。
首先,我們建立自訂約束註解UniqueTitle
:
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UniqueTitleValidator.class) public @interface UniqueTitle { String message() default "Title must be unique"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
接下來,我們建立一個PostRepository
接口,目的是從資料庫中檢索帖子:
public interface PostRepository extends JpaRepository<Post, Long> { Post findByTitle(String title); }
然後我們需要定義驗證器類別UniqueTitleValidator
,如下所示:
@Component public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> { @Autowired private PostRepository postRepository; @Override public boolean isValid(String title, ConstraintValidatorContext context) { if (title == null) { return true; } return Objects.isNull(postRepository.findByTitle(title)); } }
UniqueTitleValidator
#實現了ConstraintValidator
接口,它有兩個泛型類型:第一個是自訂註解UniqueTitle
,第二個是正在驗證的欄位類型(在本例中為String
#).我們也自動組裝了PostRepository
類別以從資料庫中檢索貼文。
isValid()
方法透過查詢 PostRepository
來檢查 title
是否為 null 或它是否是唯一的。如果 title
為 null 或唯一,則驗證成功,並傳回 true。
定義了自訂驗證註解和驗證器類別後,我們現在可以使用它來驗證Spring Boot 應用程式中的貼文標題:
public class Post { @UniqueTitle private String title; @NotNull private String body; }
我們已將@UniqueTitle
註解應用於Post
類別中的title
變數。驗證此欄位時,這將觸發 UniqueTitleValidator
類別中定義的驗證邏輯。
除了前端或客戶端做了驗證意外,伺服器端驗證輸入是至關重要的。它可以確保在處理或儲存任何惡意或格式錯誤的資料之前將其捕獲,這對於應用程式的安全性和穩定性至關重要。
假設我們有一個允許使用者建立新帳戶的 REST
端點。端點需要一個包含使用者使用者名稱和密碼的 JSON 請求體。為確保輸入有效,我們可以建立一個DTO(資料傳輸物件)類別並將驗證註解應用於其欄位:
public class UserDTO { @NotBlank private String username; @NotBlank private String password; }
我們使用@NotBlank
註解來確保username
和password
欄位不為空或null。
接下來,我們可以建立一個控制器方法來處理HTTP POST 請求並在建立新使用者之前驗證輸入:
@RestController @RequestMapping("/users") @Validated public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) { userService.createUser(userDto); return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully"); } }
我們使用Spring 的@Validated
註解來啟用方法級驗證,我們也將@Valid
註解套用到userDto
參數以觸發驗證過程。
當驗證失敗時,必須提供清晰簡潔的錯誤訊息來描述出了什麼問題以及如何修復它。
這是一個範例,如果我們有一個允許使用者建立新使用者的 RESTful API
。我們要確保姓名和電子郵件地址字段不為空,年齡在18 到99 歲之間,除了這些字段,如果用戶嘗試使用重複的「用戶名」建立帳戶,我們還會提供明確的錯誤訊息或「電子郵件」。
為此,我們可以定義一個帶有必要驗證註解的模型類別 User,如下所示:
public class User { @NotBlank(message = "用户名不能为空") private String name; @NotBlank(message = "Email不能为空") @Email(message = "无效的Emaild地址") private String email; @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于18") @Max(value = 99, message = "年龄必须小于99") private Integer age; }
我們使用 message屬性為每個驗證註解提供了自訂錯誤訊息。
接下來,在我們的Spring 控制器中,我們可以處理表單提交並使用@Valid
註解驗證使用者輸入:
@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<String> createUser(@Valid @RequestBody User user, BindingResult result) { if (result.hasErrors()) { List<String> errorMessages = result.getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.toList()); return ResponseEntity.badRequest().body(errorMessages.toString()); } // save the user to the database using UserService userService.saveUser(user); return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully"); } }
我們使用@ Valid
註解來觸發User
物件的驗證,並使用BindingResult
物件來捕獲任何驗證錯誤。
如果你的应用程序支持多种语言,则必须使用国际化 (i18n) 以用户首选语言显示错误消息。
以下是在 Spring Boot 应用程序中使用 i18n 处理错误消息的示例
首先,在资源目录下创建一个包含默认错误消息的 messages.properties
文件
# messages.properties user.name.required=Name is required. user.email.invalid=Invalid email format. user.age.invalid=Age must be a number between 18 and 99.
接下来,为每种支持的语言创建一个 messages_xx.properties
文件,例如,中文的 messages_zh_CN.properties
。
user.name.required=名称不能为空. user.email.invalid=无效的email格式. user.age.invalid=年龄必须在18到99岁之间.
然后,更新您的验证注释以使用本地化的错误消息
public class User { @NotNull(message = "{user.id.required}") private Long id; @NotBlank(message = "{user.name.required}") private String name; @Email(message = "{user.email.invalid}") private String email; @NotNull(message = "{user.age.required}") @Min(value = 18, message = "{user.age.invalid}") @Max(value = 99, message = "{user.age.invalid}") private Integer age; }
最后,在 Spring 配置文件中配置 MessageSource bean
以加载 i18n
消息文件
@Configuration public class AppConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } @Bean public LocalValidatorFactoryBean validator() { LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean(); validatorFactoryBean.setValidationMessageSource(messageSource()); return validatorFactoryBean; } }
现在,当发生验证错误时,错误消息将根据随请求发送的“Accept-Language”标头以用户的首选语言显示。
验证组是 Spring Boot 验证框架的一个强大功能,允许您根据其他输入值或应用程序状态应用条件验证规则。
现在有一个包含三个字段的User
类的情况下:firstName
、lastName
和email
。我们要确保如果 email
字段为空,则 firstName
或 lastName
字段必须非空。否则,所有三个字段都应该正常验证。
为此,我们将定义两个验证组:EmailNotEmpty
和 Default
。EmailNotEmpty
组将包含当 email
字段不为空时的验证规则,而 Default
组将包含所有三个字段的正常验证规则。
创建带有验证组的 User
类
public class User { @NotBlank(groups = Default.class) private String firstName; @NotBlank(groups = Default.class) private String lastName; @Email(groups = EmailNotEmpty.class) private String email; // getters and setters omitted for brevity public interface EmailNotEmpty {} public interface Default {} }
请注意,我们在User
类中定义了两个接口,EmailNotEmpty
和 Default
。这些将作为我们的验证组。
接下来,我们更新Controller
使用这些验证组
@RestController @RequestMapping("/users") @Validated public class UserController { public ResponseEntity<String> createUser( @Validated({org.example.model.ex6.User.EmailNotEmpty.class}) @RequestBody User userWithEmail, @Validated({User.Default.class}) @RequestBody User userWithoutEmail) { // Create the user and return a success response } }
我们已将@Validated
注释添加到我们的控制器,表明我们想要使用验证组。我们还更新了 createUser
方法,将两个 User
对象作为输入,一个在 email
字段不为空时使用,另一个在它为空时使用。
@Validated
注释用于指定将哪个验证组应用于每个 User
对象。对于 userWithEmail
参数,我们指定了 EmailNotEmpty
组,而对于 userWithoutEmail
参数,我们指定了 Default
组。
进行这些更改后,现在将根据“电子邮件”字段是否为空对“用户”类进行不同的验证。如果为空,则 firstName
或 lastName
字段必须非空。否则,所有三个字段都将正常验证。
如果需要验证跨多个字段的复杂输入规则,可以使用跨字段验证来保持验证逻辑的组织性和可维护性。跨字段验证可确保所有输入值均有效且彼此一致,从而防止出现意外行为。
假设我们有一个表单,用户可以在其中输入任务的开始日期和结束日期,并且我们希望确保结束日期不早于开始日期。我们可以使用跨域验证来实现这一点。
首先,我们定义一个自定义验证注解EndDateAfterStartDate
:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EndDateAfterStartDateValidator.class) public @interface EndDateAfterStartDate { String message() default "End date must be after start date"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
然后,我们创建验证器EndDateAfterStartDateValidator
:
public class EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> { @Override public boolean isValid(TaskForm taskForm, ConstraintValidatorContext context) { if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) { return true; } return taskForm.getEndDate().isAfter(taskForm.getStartDate()); } }
最后,我们将EndDateAfterStartDate
注释应用于我们的表单对象TaskForm
:
@EndDateAfterStartDate public class TaskForm { @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; }
现在,当用户提交表单时,验证框架将自动检查结束日期是否晚于开始日期,如果不是,则提供有意义的错误消息。
可以使用异常处理ExceptionHandler
来统一捕获和处理验证错误。
以下是如何在 Spring Boot 中使用异常处理来处理验证错误的示例:
@RestControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("status", status.value()); // Get all errors List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(x -> x.getDefaultMessage()) .collect(Collectors.toList()); body.put("errors", errors); return new ResponseEntity<>(body, headers, status); } }
在这里,我们创建了一个用 @RestControllerAdvice
注解的 RestExceptionHandler
类来处理我们的 REST API 抛出的异常。然后我们创建一个用 @ExceptionHandler
注解的方法来处理在验证失败时抛出的 MethodArgumentNotValidException
。
在处理程序方法中,我们创建了一个 Map
对象来保存错误响应的详细信息,包括时间戳、HTTP 状态代码和错误消息列表。我们使用 MethodArgumentNotValidException
对象的 getBindingResult()
方法获取所有验证错误并将它们添加到错误消息列表中。
最后,我们返回一个包含错误响应详细信息的ResponseEntity
对象,包括作为响应主体的错误消息列表、HTTP 标头和 HTTP 状态代码。
有了这个异常处理代码,我们的 REST API 抛出的任何验证错误都将被捕获并以结构化和有意义的格式返回给用户,从而更容易理解和解决问题。
需要为你的验证逻辑编写单元测试,以帮助确保它正常工作。
@DataJpaTest public class UserValidationTest { @Autowired private TestEntityManager entityManager; @Autowired private Validator validator; @Test public void testValidation() { User user = new User(); user.setFirstName("John"); user.setLastName("Doe"); user.setEmail("invalid email"); Set<ConstraintViolation<User>> violations = validator.validate(user); assertEquals(1, violations.size()); assertEquals("must be a well-formed email address", violations.iterator().next().getMessage()); } }
我们使用 JUnit 5 编写一个测试来验证具有无效电子邮件地址的“用户”对象。然后我们使用 Validator
接口来验证 User
对象并检查是否返回了预期的验证错误。
客户端验证可以通过向用户提供即时反馈并减少对服务器的请求数量来改善用户体验。但是,不应依赖它作为验证输入的唯一方法。客户端验证很容易被绕过或操纵,因此必须在服务器端验证输入,以确保安全性和数据完整性。
以上是SpringBoot參數驗證的技巧有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!