1. 概述
通常,我們發現自己被要求設計應用程序,這些應用程序必須在多語言環境中傳遞本地化消息。在這些情況下,將消息傳遞給用户的選定語言是一種常見的做法。
當我們收到對 REST Web 服務的請求時,我們必須在處理之前確保傳入的客户端請求滿足預定義的驗證規則。驗證旨在保持數據完整性和增強系統安全性。該服務負責向用户提供信息性消息,指示驗證失敗時請求中的問題。
在本教程中,我們將探討如何在 REST Web 服務中實現向用户傳遞本地化驗證消息。
2. 關鍵步驟
我們的旅程從使用資源包作為存儲本地化消息的倉庫開始。然後我們將資源包與Spring Boot集成,從而能夠在我們的應用程序中檢索本地化消息。
接下來,我們將創建包含請求驗證的Web服務。這展示瞭如何在請求驗證錯誤發生時利用本地化消息。
最後,我們將探索不同類型的本地化消息定製。這些包括覆蓋默認驗證消息、定義自己的資源包以提供自定義驗證消息,以及創建自定義驗證註解以實現動態消息生成。
通過這些步驟,我們將加深對在多語言應用程序中提供精確且特定於語言的反饋的理解。
3. Maven 依賴
在開始之前,我們先添加 Spring Boot Starter Web 和 Spring Boot Starter Validation 依賴,用於 Web 開發和 Java Bean 驗證到 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應用程序開發中,屬性文件通常作為國際化應用程序中本地化消息的存儲庫。這被認為是本地化的常見方法。它通常被稱為資源包。
這些文件是包含鍵值對的純文本文檔。鍵作為消息檢索的標識符,而關聯的值則包含相應語言的本地化消息。
在本教程中,我們將創建兩個屬性文件。
是我們的默認屬性文件,文件名不包含任何區域設置名稱。應用程序在客户端指定不支持的區域設置時,始終回退到其默認語言:
field.personalEmail=Personal Email
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空
我們希望創建一個額外的中文語言屬性文件——。應用程序在客户端指定或變體,如作為區域設置時,切換到中文:
field.personalEmail=個人電郵
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空
我們必須確保所有屬性文件都使用UTF-8編碼。這在處理包含非拉丁字符的消息(如中文、日語和韓語)時尤其重要。這確保我們能夠準確顯示所有消息,而不會有數據損壞的風險。
5. 本地化消息檢索
Spring Boot 通過 接口簡化了本地化消息檢索。它從應用程序的資源包中解析消息,並允許我們無需額外努力獲取不同區域設置的消息。
在使用之前,必須配置 MessageSource 的提供者。在本教程中,我們將使用 作為實現:
它能夠無需重啓服務器即可重新加載消息屬性文件。這在應用程序開發的早期階段非常有用,此時我們希望在不重新部署整個應用程序的情況下看到消息更改。
必須將默認編碼與我們用於屬性文件的 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),其中包含 字段。我們將對此 DTO 類應用 Java Bean 驗證。 字段使用 註解,以確保它不是一個空字符串:
public class User {
@NotEmpty
private String email;
// getters and setters
}
7. REST Service
在本節中,我們將創建一個 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. Locale Selection
在通常情況下,使用Accept-Language HTTP 標頭來定義客户端的語言偏好。
我們可以通過在 Spring Boot 中使用 LocaleResolver 接口,從 HTTP 請求的 Accept-Language 標頭中獲取 locale。
在我們的例子中,我們不需要顯式地定義 LocaleResolver。 Spring Boot 已經為我們提供了一個默認的。
我們的服務然後返回根據這個標頭對應的本地化消息。 如果客户端指定了一個我們的服務不支持的語言,我們的服務將簡單地採用英語作為默認語言。
7.2. Validation
我們使用@Valid註解來標註User DTO 在updateUser(…)方法中。 這表示 Java Bean Validation 在 REST Web 服務被調用時會驗證對象。 驗證發生在後台。 我們將通過BindingResult 對象來檢查驗證結果。
每當存在任何字段錯誤,並且該錯誤由bindingResult.hasFieldErrors(), 確定時, Spring Boot 將為我們根據當前 locale 獲取本地化錯誤消息,並將消息封裝到 field error 實例中。
我們將迭代 BindingResult 中的每個字段錯誤,並將它們收集到一個響應對象中,然後將響應對象發送回客户端。
7.3. Response Objects
如果驗證失敗,服務將返回一個 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. Validation Message Types
Let’s initiate an update request to the REST service /user with the following request body:
{
"email": ""
}
As a reminder, the User object must contain a non-empty email. Therefore, we expect that this request triggers a validation error.
8.1. Standard Message
We’ll see the following typical response with an English message if we don’t provide any language information in the request:
{
"fieldErrors": [
{
"field": "email",
"message": "must not be empty"
}
]
}
Now, let’s initiate another request with the following accept-language HTTP header:
accept-lanaguage: zh-tw
The service interprets that we’d like to use Chinese. It retrieves the message from the corresponding resource bundle. We’ll see the following response that includes the Chinese validation message:
{
"fieldErrors": [
{
"field": "email",
"message": "不得是空的"
}
]
}
These are standard validation messages provided by the Java Bean Validation. We can find an exhaustive list of messages from the Hibernate validator, which serves as the default validation implementation.
However, the messages we saw don’t look nice. We probably want to change the validation message to provide more clarity. Let’s take a move to modify the standardized messages.
8.2. Overridden Message
We can override default messages defined in Java Bean Validation implementation. All we need to do is define a property file that has the basename ValidationMessages.properties:
jakarta.validation.constraints.NotEmpty.message=The field cannot be empty
With the same basename, we’ll create another property file ValidationMessages_zh.properties for Chinese as well:
jakarta.validation.constraints.NotEmpty.message=本欄不能留空
Upon calling the same service again, the response message is replaced by the one we defined:
{
"fieldErrors": [
{
"field": "email",
"message": "The field cannot be empty"
}
]
}
However, the validation message still looks generic despite overriding the message. The message itself doesn’t reveal which field goes wrong. Let’s proceed to include the field name in the error message.
8.3. Customized Message
In this scenario, we’ll dive into customizing validation messages. We defined all customized messages in the CustomValidationMessages resource bundle earlier.
Then, we’ll apply the new message {validation.email.notEmpty} to the validation annotation to the User DTO. The curly bracket indicates the message is a property key linking it to the corresponding message within the resource bundle:
public class User {
@NotEmpty(message = "{validation.email.notEmpty}")
private String email;
// getter and setter
}
We’ll see the following message when we initiate a request to the service:
{
"fieldErrors": [
{
"field": "email",
"message": "Email cannot be empty"
}
]
}
8.4. Interpolated Message
We’ve improved the message significantly by including the field name in the message. However, a potential challenge arises when dealing with many fields. Imagine a scenario where we have 30 fields, and each field requires three different types of validations. This would result in 90 validation messages within each localized resource bundle.
We could utilize message interpolation to address this issue. Interpolated messages operate on placeholders that are replaced dynamically with actual values before presenting them to users. In the scenario that we mentioned before, this approach reduces the number of validation messages to 33, containing 30 field names and three unique validation messages.
@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 operates as a constraint and uses FieldNotEmptyValidator as the validator implementation:
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());
}
}
The isValid(…) method performs the validation logic and simply determines whether the value is not empty. If the value is empty, it retrieves localized messages for the attribute field and message corresponding to the current locale from the request context. The attribute message is interpolated to form a complete message.
Upon execution, we observe the following result:
{
"fieldErrors": [
{
"field": "email",
"message": "{field.personalEmail} cannot be empty"
}
]
}
The message attribute and its corresponding placeholder are successfully retrieved. However, we’re expecting {field.personalEmail} to be replaced by the actual value.
8.5. Custom MessageInterpolator
The problem lies in the default MessageInterpolator. It translates the placeholder for one time only. We need to apply the interpolation to the message again to replace the subsequent placeholder with the localized message. In this case, we have to define a custom message interpolator to:public class RecursiveLocaleContextMessageInterpolator extends AbstractMessageInterpolator {
private static final Pattern PATTERN_PLACEHOLDER = Pattern.compile("\\{([^}]+)\\}");
private final MessageInterpolator interpolator;
public RecursiveLocaleContextMessageInterpolator(MessageSourceResourceBundleLocator 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 is simply a decorator. It reapplies interpolation with the wrapped MessageInterpolator when it detects the message contains any curly bracket placeholder.
We’ve completed the implementation, and it’s time for us to configure Spring Boot to incorporate it. We’ll add two provider methods to 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;
}
The getMessageInterpolator(…) method returns our own implementation. This implementation wraps ResourceBundleMessageInterpolator, which is the default MessageInterpolator in Spring Boot. The getValidator() is for registering the validator to use our customized MessageInterpolator within our web service.
Now, we’re all set, and let’s test it once more. We’ll have the following complete interpolated message with the placeholder replaced by the localized message as well:
{
"fieldErrors": [
{
"field": "email",
"message": "Personal Email cannot be empty"
}
]
}
9. 結論
在本文中,我們深入探討了在多語言應用程序中交付本地化消息的過程。
我們首先制定了完整實施的關鍵步驟的大綱,從使用屬性文件作為消息存儲庫並使用 UTF-8 進行編碼開始。Spring Boot 集成簡化了基於客户端區域設置的首選項的消息檢索。Java Bean Validation,以及自定義註釋和消息插值,允許針對特定語言定製錯誤響應。
通過結合使用這些技術,我們能夠為 REST Web 服務提供本地化驗證響應。