REST 中的本地化驗證訊息
1. 概述
我們經常發現自己的任務是設計必須在多語言環境中傳遞本地化訊息的應用程式。在這種情況下,以使用者選擇的語言傳遞訊息是一種常見的做法。
當我們收到對 REST Web 服務的用戶端請求時,我們必須確保傳入的用戶端請求在處理之前符合預先定義的驗證規則。驗證旨在維護資料完整性並增強系統安全性。該服務負責在驗證失敗時提供資訊性訊息來指示請求出現的問題。
在本教程中,我們將探索在 REST Web 服務中傳遞本地化驗證訊息的實作。
2. 基本步驟
我們的旅程從利用資源包作為儲存本地化訊息的儲存庫開始。然後,我們將資源包與 Spring Boot 集成,這使我們能夠在應用程式中檢索本地化訊息。
之後,我們將繼續建立包含請求驗證的 Web 服務。這展示了在請求期間出現驗證錯誤時如何使用本地化訊息。
最後,我們將探索不同類型的本地化訊息自訂。其中包括覆蓋預設驗證訊息、定義我們自己的資源包以提供自訂驗證訊息以及為動態訊息產生建立自訂驗證註釋。
透過這些步驟,我們將加深對在多語言應用程式中提供精確且特定於語言的回饋的理解。
3.Maven依賴
在開始之前,讓我們將用於 Web 開發和 Java Bean 驗證的Spring Boot Starter Web和Spring Boot Starter Validation依賴項新增至pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
這些的最新版本可以在 Maven Central 上找到。
4. 本地化訊息存儲
在 Java 應用程式開發中,屬性檔案通常充當國際化應用程式中本地化訊息的儲存庫。它被認為是一種傳統的本地化方法。它通常被稱為屬性資源包。
這些文件是包含鍵值對的純文字文件。鍵充當訊息檢索的標識符,而關聯值則保存對應語言的本地化訊息。
在本教程中,我們將建立兩個屬性檔案。
CustomValidationMessages.properties
是我們的預設屬性文件,其中文件名稱不包含任何區域設定名稱。每當客戶端指定不支援的區域設定時,應用程式總是回退到其預設語言:
field.personalEmail=Personal Email
validation.notEmpty={field} cannot be empty
validation.email.notEmpty=Email cannot be empty
我們也想建立一個額外的中文屬性檔 – CustomValidationMessages_zh.properties
。每當客戶端指定zh
或zh-tw
等變體作為語言環境時,應用程式語言就會切換為中文:
field.personalEmail=個人電郵
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空
我們必須確保所有屬性檔案都以 UTF-8 編碼。當處理包含非拉丁字元(如中文、日文和韓文)的訊息時,這一點變得尤為重要。此保證保證我們將準確顯示所有訊息,而不會出現損壞的風險。
5. 本地化訊息檢索
Spring Boot 透過MessageSource
介面簡化了本地化訊息檢索。它解析來自應用程式中的資源包的訊息,並使我們能夠無需額外的工作即可獲取不同區域設定的訊息。
我們必須在 Spring Boot 中設定MessageSource
的提供者才能使用它。在本教學中,我們將使用ReloadableResourceBundleMessageSource
作為實作。
它能夠重新載入訊息屬性檔案而無需重新啟動伺服器。當我們處於應用程式開發的初始階段時,當我們希望看到訊息更改而不重新部署整個應用程式時,這非常有用。
我們必須將預設編碼與我們用於屬性檔案的 UTF-8 編碼對齊:
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:CustomValidationMessages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
6.Bean驗證
在驗證過程中,使用名為User
資料傳輸物件 (DTO),其中包含email
欄位。我們將應用 Java Bean Validation 來驗證此 DTO 類別。 email
欄位使用@NotEmpty
進行註釋,以確保它不是空字串。該註解是一個標準的 Java Bean Validation 註解:
public class User {
@NotEmpty
private String email;
// getters and setters
}
7.休息服務
在本節中,我們將建立一個 REST 服務UserService,
它負責根據請求正文透過 PUT 方法更新特定的使用者資訊:
@RestController
public class UserService {
@PutMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UpdateUserResponse> updateUser(
@RequestBody @Valid User user,
BindingResult bindingResult) {
if (bindingResult.hasFieldErrors()) {
List<InputFieldError> fieldErrorList = bindingResult.getFieldErrors().stream()
.map(error -> new InputFieldError(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
UpdateUserResponse updateResponse = new UpdateUserResponse(fieldErrorList);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(updateResponse);
}
else {
// Update logic...
return ResponseEntity.status(HttpStatus.OK).build();
}
}
}
7.1.區域設定選擇
使用**Accept-Language**
HTTP 標頭來定義客戶端的語言首選項**是常見的做法**。
我們可以使用 Spring Boot 中的LocaleResolver
介面從 HTTP 請求中的Accept-Language
標頭中取得語言環境。在我們的例子中,我們不必明確定義LocaleResolver
。 Spring Boot 為我們提供了一個預設的。
然後,我們的服務會根據此標頭傳回適當的本地化訊息。如果客戶指定我們的服務不支援的語言,我們的服務只會採用英語作為預設語言。
7.2.驗證
我們在updateUser(…)
方法中使用@Valid
註釋User
DTO。這表示 Java Bean Validation 在呼叫 REST Web 服務時驗證物件。驗證發生在幕後。我們將透過BindingResult
物件檢查驗證結果。
每當出現任何欄位錯誤時bindingResult.hasFieldErrors(),
Spring Boot 都會根據目前的語言環境為我們取得本地化的錯誤訊息,並將該訊息封裝到欄位錯誤實例中。
我們將迭代BindingResult
中的每個欄位錯誤並將它們收集到回應物件中,並將回應傳送回客戶端。
7.3.回應對象
如果驗證失敗,服務將傳回一個UpdateResponse
對象,其中包含指定語言的驗證錯誤訊息:
public class UpdateResponse {
private List<InputFieldError> fieldErrors;
// getter and setter
}
InputFieldError
是一個佔位符類,用於儲存哪個欄位包含錯誤以及錯誤訊息是什麼:
public class InputFieldError {
private String field;
private String message;
// getter and setter
}
8. 驗證訊息類型
讓我們使用以下請求正文向 REST 服務/user
發起更新請求:
{
"email": ""
}
提醒一下, User
物件必須包含非空電子郵件。因此,我們預計該請求會觸發驗證錯誤。
8.1.標準訊息
如果我們不在請求中提供任何語言訊息,我們將看到以下帶有英文訊息的典型回應:
{
"fieldErrors": [
{
"field": "email",
"message": "must not be empty"
}
]
}
現在,讓我們使用以下accept-language
HTTP 標頭髮起另一個請求:
accept-lanaguage: zh-tw
該服務解釋說我們想使用中文。它從相應的資源包中檢索訊息。我們將看到以下包含中文驗證訊息的回應:
{
"fieldErrors": [
{
"field": "email",
"message": "不得是空的"
}
]
}
這些是 Java Bean Validation 提供的標準驗證訊息。我們可以從 Hibernate 驗證器中找到詳盡的訊息列表,它作為預設的驗證實作。
然而,我們看到的消息看起來不太好。我們可能想要更改驗證訊息以提供更清晰的資訊。讓我們採取行動來修改標準化訊息。
8.2.被覆蓋的訊息
我們可以覆蓋 Java Bean Validation 實作中定義的預設訊息。我們需要做的就是定義一個具有基本名稱ValidationMessages.properties
的屬性檔:
javax.validation.constraints.NotEmpty.message=The field cannot be empty
使用相同的基本名稱,我們也將為中文建立另一個屬性檔案ValidationMessages_zh.properties
:
javax.validation.constraints.NotEmpty.message=本欄不能留空
再次呼叫相同服務時,回應訊息將替換為我們定義的訊息:
{
"fieldErrors": [
{
"field": "email",
"message": "The field cannot be empty"
}
]
}
然而,儘管覆蓋了訊息,驗證訊息看起來仍然是通用的。該訊息本身並沒有透露哪個欄位出了問題。讓我們繼續在錯誤訊息中包含欄位名稱。
8.3.客製化留言
在這種情況下,我們將深入研究自訂驗證訊息。我們之前在CustomValidationMessages
資源包中定義了所有自訂訊息。
然後,我們將新訊息{validation.email.notEmpty}
套用到User
DTO 的驗證註解。大括號表示該訊息是一個屬性鍵,將其連結到資源包中的對應訊息:
public class User {
@NotEmpty(message = "{validation.email.notEmpty}")
private String email;
// getter and setter
}
當我們向服務發起請求時,我們將看到以下訊息:
{
"fieldErrors": [
{
"field": "email",
"message": "Email cannot be empty"
}
]
}
8.4.內插訊息
透過在訊息中包含欄位名稱,我們顯著改進了訊息。然而,在處理許多領域時會出現潛在的挑戰。想像一個場景,我們有 30 個字段,每個字段需要三種不同類型的驗證。這將導致每個本地化資源包中產生 90 個驗證訊息。
我們可以利用訊息插值來解決這個問題。插值訊息對佔位符進行操作,在將其呈現給使用者之前,這些佔位符會動態替換為實際值。在我們之前提到的場景中,這種方法將驗證訊息的數量減少到 33 個,其中包含 30 個欄位名稱和 3 個唯一的驗證訊息。
Java Bean Validation 不支援具有自訂佔位符的驗證訊息。但是,我們可以定義包含附加屬性的自訂驗證。
這次,我們使用新的自訂註釋@FieldNotEmpty
來註釋User
。在現有message
屬性的基礎上,我們將引入一個新的屬性field
來指示欄位名稱:
public class User {
@FieldNotEmpty(message = "{validation.notEmpty}", field = "{field.personalEmail}")
private String email;
// getter and setter
}
現在,讓我們用兩個屬性定義@FieldNotEmpty
:
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = {FieldNotEmptyValidator.class})
public @interface FieldNotEmpty {
String message() default "{validation.notEmpty}";
String field() default "Field";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@FieldNotEmpty
作為約束運行,並使用FieldNotEmptyValidator
作為驗證器實作:
public class FieldNotEmptyValidator implements ConstraintValidator<FieldNotEmpty, Object> {
private String message;
private String field;
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return (value != null && !value.toString().trim().isEmpty());
}
}
isValid(…)
方法執行驗證邏輯並簡單地確定該value
是否不為空。如果該value
空,它將從請求上下文中檢索屬性field
的本地化訊息以及與當前語言環境相對應的message
。屬性message
被內插以形成完整的訊息。
執行後,我們觀察到以下結果:
{
"fieldErrors": [
{
"field": "email",
"message": "{field.personalEmail} cannot be empty"
}
]
}
已成功檢索message
屬性及其對應的佔位符。但是,我們期望{field.personalEmail}
被實際值取代。
8.5。自訂MessageInterpolator
問題在於預設的MessageInterpolator
。它僅翻譯佔位符一次。我們需要再次對訊息應用插值,以用本地化訊息取代後續佔位符。在這種情況下,我們必須定義一個自訂訊息插值器來取代預設的訊息插值器:
public class RecursiveLocaleContextMessageInterpolator extends AbstractMessageInterpolator {
private static final Pattern PATTERN_PLACEHOLDER = Pattern.compile("\\{([^}]+)\\}");
private final MessageInterpolator interpolator;
public RecursiveLocaleContextMessageInterpolator(ResourceBundleMessageInterpolator interpolator) {
this.interpolator = interpolator;
}
@Override
public String interpolate(MessageInterpolator.Context context, Locale locale, String message) {
int level = 0;
while (containsPlaceholder(message) && (level++ < 2)) {
message = this.interpolator.interpolate(message, context, locale);
}
return message;
}
private boolean containsPlaceholder(String code) {
Matcher matcher = PATTERN_PLACEHOLDER.matcher(code);
return matcher.find();
}
}
RecursiveLocaleContextMessageInterpolator
只是一個裝飾器。當它偵測到訊息包含任何大括號佔位符時,它會使用包裝的MessageInterpolator
重新套用插值。
我們已經完成了實現,現在是時候配置 Spring Boot 來合併它了。我們將向MessageConfig:
@Bean
public MessageInterpolator getMessageInterpolator(MessageSource messageSource) {
MessageSourceResourceBundleLocator resourceBundleLocator = new MessageSourceResourceBundleLocator(messageSource);
ResourceBundleMessageInterpolator messageInterpolator = new ResourceBundleMessageInterpolator(resourceBundleLocator);
return new RecursiveLocaleContextMessageInterpolator(messageInterpolator);
}
@Bean
public LocalValidatorFactoryBean getValidator(MessageInterpolator messageInterpolator) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setMessageInterpolator(messageInterpolator);
return bean;
}
*getMessageInterpolator(…)*方法傳回我們自己的實作。此實作包裝了ResourceBundleMessageInterpolator,
它是Spring Boot中預設的MessageInterpolator
。 getValidator()
用於註冊驗證器以在我們的 Web 服務中使用我們自訂的MessageInterpolator
。
現在,我們已經準備好了,讓我們再次測試一下。我們將得到以下完整的內插訊息,其中佔位符也被本地化訊息取代:
{
"fieldErrors": [
{
"field": "email",
"message": "Personal Email cannot be empty"
}
]
}
9. 結論
在本文中,我們深入研究了在多語言應用程式中傳遞本地化訊息的過程。
我們首先概述了完整實現的所有關鍵步驟,從使用屬性檔案作為訊息儲存庫並以 UTF-8 對其進行編碼開始。 Spring Boot 整合根據客戶端區域設定簡化了訊息檢索。 Java Bean 驗證以及自訂註解和訊息插值允許自訂、特定於語言的錯誤回應。
透過將這些技術結合在一起,我們能夠在 REST Web 服務中提供在地化的驗證回應。
與往常一樣,範例程式碼可在 GitHub 上取得。