1. Overview
1.概述
Frequently, we find ourselves tasked with designing applications that must deliver localized messages within a multilingual environment. In such scenarios, delivering messages in the user’s selected language is a common practice.
我们经常会发现,在设计应用程序时,必须在多语言环境中传递本地化信息。在这种情况下,用用户选择的语言发送信息是一种常见的做法。
When we receive client requests to a REST web service, we must ensure the incoming client requests meet the predefined validation rules before processing them. Validations aim to maintain data integrity and enhance system security. The service is responsible for providing informative messages to indicate what’s wrong with the request whenever the validation fails.
当我们收到客户对 REST 网络服务的请求时,我们必须确保传入的客户请求符合预定义的验证规则,然后再进行处理。验证的目的是维护数据完整性和增强系统安全性。服务负责在验证失败时提供信息,说明请求出了什么问题。
In this tutorial, we’ll explore the implementation of delivering localized validation messages in a REST web service.
在本教程中,我们将探讨在 REST 网络服务中交付本地化验证消息的实现方法。
2. Essential Steps
2.基本步骤
Our journey begins with utilizing resource bundles as a repository for storing localized messages. We’ll then integrate resource bundles with Spring Boot which allows us to retrieve localized messages in our application.
我们的旅程从利用资源包作为存储本地化消息的存储库开始。然后,我们将把资源包与 Spring Boot 集成,这样就可以在应用程序中检索本地化消息。
After that, we’ll jump on to web service creation containing request validation. This showcases how localized messages are utilized in the event of a validation error during a request.
之后,我们将跳转到包含请求验证的网络服务创建。这将展示在请求过程中出现验证错误时如何使用本地化信息。
Finally, we’ll explore different kinds of localized message customization. These include overriding the default validation messages, defining our own resource bundle to provide custom validation messages, and creating a custom validation annotation for dynamic message generation.
最后,我们将探索不同类型的本地化消息定制。其中包括覆盖默认验证信息、定义我们自己的资源包以提供自定义验证信息,以及为动态信息生成创建自定义验证注解。
Through these steps, we’ll refine our understanding of delivering precise and language-specific feedback in multilingual applications.
通过这些步骤,我们将进一步了解如何在多语言应用程序中提供精确的、针对特定语言的反馈。
3. Maven Dependency
3.Maven 依赖性
Before we get started, let’s add the Spring Boot Starter Web and Spring Boot Starter Validation dependencies for web development and Java Bean Validation to pom.xml:
在开始之前,我们先将用于 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>
The latest version of these can be found on Maven Central.
最新版本可在 Maven Central 上找到。
4. Localized Messages Storage
4.本地化信息存储
In Java application development, property files commonly function as repositories for localized messages in internationalized applications. It’s considered a conventional approach to localization. It’s often named a property resource bundle.
在 Java 应用程序开发中,属性文件通常用作 国际化应用程序中本地化信息的存储库。它被认为是本地化的一种传统方法。它通常被命名为资源包。
These files are plain text documents comprising key-value pairs. The key functions as an identifier for message retrieval, while the associated value holds the localized message in the corresponding language.
这些文件是由键值对组成的纯文本文件。键是信息检索的标识符,而相关的值则是相应语言的本地化信息。
In this tutorial, we’ll create two property files.
在本教程中,我们将创建两个属性文件。
CustomValidationMessages.properties is our default property file where the file name doesn’t contain any locale name. The application always falls back to its default language whenever the client specifies a locale that isn’t supported:
CustomValidationMessages.properties是我们的默认属性文件,其中的文件名不包含任何区域名称。只要客户端指定了不支持的语言,应用程序就会返回到默认语言:
field.personalEmail=Personal Email
validation.notEmpty={field} cannot be empty
validation.email.notEmpty=Email cannot be empty
We’d like to create an additional Chinese language property file as well – CustomValidationMessages_zh.properties. The application language switches to Chinese whenever the client specifies either zh or variants such as zh-tw as the locale:
我们还想创建一个额外的中文语言属性文件 – CustomValidationMessages_zh.properties 。只要客户端指定 zh 或 变体(如 zh-tw 作为本地语言),应用程序语言就会切换为中文:
field.personalEmail=個人電郵
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空
We must ensure that all property files are encoded in UTF-8. This becomes particularly crucial when handling messages that include non-Latin characters like Chinese, Japanese, and Korean. This assurance guarantees that we’ll display all messages accurately without the risk of corruption.
我们必须确保所有属性文件都以 UTF-8 编码。在处理包含中文、日文和韩文等非拉丁字符的信息时,这一点尤为重要。 这一保证可确保我们准确显示所有信息,而不会有损坏的风险。
5. Localized Messages Retrieval
5.本地化信息检索
Spring Boot simplifies the localized message retrieval through the MessageSource interface. It resolves messages from resource bundles in the application and enables us to obtain messages for different locales without additional effort.
Spring Boot 通过 MessageSource 接口简化了本地化消息检索。它可以解析应用程序中资源捆绑包的消息,使我们无需额外工作即可获取不同本地语言的消息。
We must configure the provider of MessageSource in Spring Boot before we can make use of it. In this tutorial, we’ll use ReloadableResourceBundleMessageSource as the implementation.
我们必须在 Spring Boot 中配置 MessageSource 的提供程序,然后才能使用它。在本教程中,我们将使用 ReloadableResourceBundleMessageSource 作为实现。
It’s capable of reloading message property files without server restart. This is very useful when we’re in the initial stage of application development when we want to see the message changes without redeploying the whole application.
它能够在不重启服务器的情况下重新加载消息属性文件。当我们处于应用程序开发的初始阶段,希望在不重新部署整个应用程序的情况下查看消息变化时,这一点非常有用。
We have to align the default encoding with the UTF-8 encoding that we’re using for our property files:
我们必须使默认编码与属性文件使用的 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 Validation
6.Bean验证
In the validation process, a Data Transfer Object (DTO) named User is used, which contains an email field. We’ll apply Java Bean Validation to validate this DTO class. The email field is annotated with @NotEmpty to make sure it isn’t an empty string. This annotation is a standard Java Bean Validation annotation:
在验证过程中,使用了名为 User 的数据传输对象 (DTO),其中包含一个 email 字段。我们将应用 Java Bean Validation 来验证该 DTO 类。email 字段被注释为 @NotEmpty 以确保它不是空字符串。该注解是标准的 Java Bean 验证注解:
public class User {
@NotEmpty
private String email;
// getters and setters
}
7. REST Service
7.REST 服务
In this section, we’ll create a REST service, UserService, which is responsible for updating specific user information via the PUT method based on the request body:
在本节中,我们将创建一个 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
7.1.地区选择
It’s a common practice to employ the Accept-Language HTTP header to define the client’s language preference.
通常的做法是使用 Accept-Language HTTP 标头来定义客户端的语言偏好。
We can obtain the locale from the Accept-Language header in the HTTP request by using the LocaleResolver interface in Spring Boot. In our case, we don’t have to explicitly define a LocaleResolver. Spring Boot provides a default one for us.
我们可以通过使用 Spring Boot 中的 LocaleResolver 接口,从 HTTP 请求中的 Accept-Language 标头获取 locale。在我们的案例中,我们无需显式定义 LocaleResolver 。Spring Boot 会为我们提供一个默认接口。
Our service then returns the appropriate localized messages in accordance with this header. In situations where the client designates a language that our service doesn’t support, our service simply adopts English as the default language.
然后,我们的服务会根据该标头返回相应的本地化信息。如果客户指定的语言我们的服务不支持,我们的服务会直接将英语作为默认语言。
7.2. Validation
7.2 验证
We annotate User DTO with @Valid in the updateUser(…) method. This indicates that Java Bean Validation validates the object when the REST web service is called. Validation occurs behind the scenes. We’ll examine the validation outcomes via the BindingResult object.
我们在 updateUser(…) 方法中使用 @Valid 对 User DTO 进行注解。这表明 Java Bean 验证会在调用 REST Web 服务时验证对象。我们将通过 BindingResult 对象检查验证结果。
Whenever there is any field error, which is determined by bindingResult.hasFieldErrors(), Spring Boot fetches the localized error message for us according to the current locale and encapsulates the message into a field error instance.
每当出现由 bindingResult.hasFieldErrors() 确定的字段错误时,Spring Boot 都会根据当前的本地语言为我们获取本地化的错误消息,并将消息封装到字段错误实例中。
We’ll iterate each field error in BindingResult and collect them into a response object, and send the response back to the client.
我们将遍历 BindingResult 中的每个字段错误,并将其收集到响应对象中,然后将响应发送回客户端。
7.3. Response Objects
7.3.响应对象
If validation fails, the service returns an UpdateResponse object containing the validation error messages in the specified language:
如果验证失败,服务会返回一个 UpdateResponse 对象,其中包含以指定语言显示的验证错误信息:
public class UpdateResponse {
private List<InputFieldError> fieldErrors;
// getter and setter
}
InputFieldError is a placeholder class to store which field contains the error and what the error message is:
InputFieldError 是一个占位符类,用于存储包含错误的字段以及错误信息:
public class InputFieldError {
private String field;
private String message;
// getter and setter
}
8. Validation Message Types
8.验证信息类型
Let’s initiate an update request to the REST service /user with the following request body:
让我们用以下请求正文向 REST 服务 /user 发起更新请求:
{
"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
8.1 标准信息
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-language HTTP 标头启动另一个请求:
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.
这些是 Java Bean 验证提供的标准验证信息。我们可以从作为默认验证实现的 Hibernate 验证器中找到详尽的信息列表。
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
8.2.重载信息
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:
我们可以覆盖 Java Bean 验证实现中定义的默认消息。我们只需定义一个以 ValidationMessages.properties 为基名的属性文件:
ValidationMessages.properties。
javax.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:
使用相同的基名,我们将为中文创建另一个属性文件ValidationMessages_zh.properties:
javax.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
8.3.自定义信息
In this scenario, we’ll dive into customizing validation messages. We defined all customized messages in the CustomValidationMessages resource bundle earlier.
在此场景中,我们将深入 自定义验证消息。我们在 CustomValidationMessages 资源包中定义了所有自定义消息。
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:
然后,我们将把新消息 {validation.email.notEmpty} 应用到 User DTO 的验证注解中。大括号表示该消息是一个属性键,将其链接到资源捆绑中的相应消息: {validation.email.notEmpty}</em
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
8.4.插值信息
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.
通过在信息中加入字段名称,我们大大改进了信息。不过,在处理多个字段时,可能会出现一个难题。假设我们有 30 个字段,每个字段需要三种不同类型的验证。这将在每个本地化资源包中产生 90 条验证信息。
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.
我们可以利用信息插值来解决这一问题。插值消息对占位符进行操作,并在向用户显示之前动态替换为实际值。在我们之前提到的应用场景中,这种方法可将验证消息的数量减少到 33 个,其中包含 30 个字段名和三个唯一的验证消息。
Java Bean Validation doesn’t support a validation message with a self-defined placeholder. However, we can define a custom validation that includes additional attributes.
Java Bean 验证不支持自定义占位符的验证消息。不过,我们可以定义一个包含附加属性的 自定义验证。
This time, we annotate the User with a new customized annotation @FieldNotEmpty. Based on the existing message attribute, we’ll introduce a new attribute field to indicate the field name:
这次,我们将使用新的自定义注解 @FieldNotEmpty 来注解 User 。在现有 message 属性的基础上,我们将引入一个新的属性 field 来表示字段名称:
public class User {
@FieldNotEmpty(message = "{validation.notEmpty}", field = "{field.personalEmail}")
private String email;
// getter and setter
}
Now, let us define @FieldNotEmpty with two attributes:
现在,让我们定义带有两个属性的 @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 operates as a constraint and uses FieldNotEmptyValidator as the validator implementation:
@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());
}
}
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.
isValid(…)方法执行验证逻辑,并简单地确定 value 是否为空。如果value为空,它将从请求上下文中检索与当前本地语言相对应的属性 field 和 message 的本地化信息。属性 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.
成功检索到 message 属性及其相应的占位符。但是,我们希望 {field.personalEmail} 被实际值取代。
8.5. Custom MessageInterpolator
8.5.自定义MessageInterpolator</em
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 replace the default one:
问题出在默认的 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 is simply a decorator. It reapplies interpolation with the wrapped MessageInterpolator when it detects the message contains any curly bracket placeholder.
RecursiveLocaleContextMessageInterpolator 只是一个 装饰器。当检测到消息中包含任何大括号占位符时,它会使用封装的 MessageInterpolator 重新应用插值。
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:
我们已经完成了实现,现在是配置 Spring Boot 以将其纳入的时候了。我们将在 MessageConfig 中添加两个提供程序方法:</em
@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.
getMessageInterpolator(…) 方法返回我们自己的实现。该实现封装了 ResourceBundleMessageInterpolator,它是 Spring Boot 中默认的 MessageInterpolator。getValidator() 用于注册验证器,以便在 Web 服务中使用我们自定义的 MessageInterpolator 。
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. Conclusion
9.结论
In this article, we dived into the process of delivering localized messages within multilingual applications.
在本文中,我们深入探讨了在多语言应用程序中传递本地化信息的过程。
We began with an outline for all the key steps for a complete implementation, starting from using property files as message repositories and encoding them in UTF-8. Spring Boot integration simplifies message retrieval based on client locale preferences. Java Bean Validation, along with custom annotations and message interpolation, allows for tailored, language-specific error responses.
我们首先概述了完整实施的所有关键步骤,从使用属性文件作为消息存储库并以 UTF-8 编码开始。Spring Boot 集成简化了基于客户端地域偏好的消息检索。Java Bean 验证以及自定义注释和消息插值允许定制特定语言的错误响应。
By incorporating these techniques together, we’re able to provide localized validation responses in REST web services.
将这些技术结合在一起,我们就能在 REST 网络服务中提供本地化的验证响应。
As always, the sample code is available over on GitHub.
与往常一样,示例代码可在 GitHub 上获取。