diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java
new file mode 100644
index 000000000..36aa928e2
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java
@@ -0,0 +1,92 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation;
+
+import jakarta.validation.MessageInterpolator;
+import modelengine.fitframework.inspection.Validation;
+import modelengine.fitframework.util.ObjectUtils;
+import modelengine.fitframework.util.i18n.LocaleContextHolder;
+
+import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
+
+import java.util.Locale;
+
+/**
+ * 检验消息处理的代理类。
+ *
+ * 从 {@link LocaleContextHolder} 中获取当前线程设置的 {@link Locale} 并委托 {@link MessageInterpolator} 去处理消息。
+ *
+ *
+ * @author 阮睿
+ * @since 2025-07-31
+ */
+public class LocaleContextMessageInterpolator implements MessageInterpolator {
+ private final MessageInterpolator targetInterpolator;
+ private Locale locale;
+
+ /**
+ * 构造函数。
+ *
+ * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
+ */
+ public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) {
+ this.targetInterpolator = targetInterpolator;
+ this.locale = Locale.getDefault();
+ }
+
+ /**
+ * 构造函数,默认使用 {@link ParameterMessageInterpolator} 作为目标检验消息处理对象。
+ */
+ public LocaleContextMessageInterpolator() {
+ this.targetInterpolator = new ParameterMessageInterpolator();
+ this.locale = Locale.getDefault();
+ }
+
+ /**
+ * 构造函数。
+ *
+ * @param locale 表示当前设置默认的 {@link Locale}。
+ */
+ public LocaleContextMessageInterpolator(Locale locale) {
+ this.targetInterpolator = new ParameterMessageInterpolator();
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ /**
+ * 构造函数。
+ *
+ * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
+ * @param locale 表示当前设置默认的 {@link Locale}。
+ */
+ public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator, Locale locale) {
+ this.targetInterpolator = targetInterpolator;
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ /**
+ * 设置默认的 {@link Locale}。
+ *
+ * @param locale 默认设置的 {@link Locale}。
+ */
+ public void setLocale(Locale locale) {
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context) {
+ if (LocaleContextHolder.getLocale() != null) {
+ return this.targetInterpolator.interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
+ }
+ return this.targetInterpolator.interpolate(messageTemplate, context, this.locale);
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context, Locale locale) {
+ Validation.notNull(locale, "Locale cannot be null.");
+ return this.targetInterpolator.interpolate(messageTemplate, context, locale);
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java
deleted file mode 100644
index fc22cc98a..000000000
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
- * This file is a part of the ModelEngine Project.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-package modelengine.fitframework.validation;
-
-import jakarta.validation.MessageInterpolator;
-
-import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
-
-import java.util.Locale;
-
-/**
- * 地区消息插值器。
- *
- * 作为 Jakarta 消息插值器的代理类,提供地区设置能力。
- *
- *
- * @author 阮睿
- * @since 2025-08-18
- */
-public class LocaleMessageInterpolator implements MessageInterpolator {
- private final MessageInterpolator target;
-
- private Locale locale;
-
- /**
- * 构造函数,使用指定的目标消息插值器初始化实例。
- *
- * @param target 表示目标消息插值器的 {@link MessageInterpolator}。
- */
- public LocaleMessageInterpolator(MessageInterpolator target) {
- this.target = target;
- this.locale = Locale.getDefault();
- }
-
- /**
- * 构造函数,使用指定的地区初始化实例。
- *
- * @param locale 表示指定地区的 {@link Locale}。
- */
- public LocaleMessageInterpolator(Locale locale) {
- this.locale = locale;
- this.target = new ParameterMessageInterpolator();
- }
-
- /**
- * 构造函数,使用指定的目标消息插值器和地区初始化实例。
- *
- * @param target 表示被代理的目标消息插值器的 {@link MessageInterpolator}。
- * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。
- */
- public LocaleMessageInterpolator(MessageInterpolator target, Locale locale) {
- this.target = target;
- this.locale = locale;
- }
-
- /**
- * 构造函数,使用默认地区初始化实例。
- */
- public LocaleMessageInterpolator() {
- this.locale = Locale.getDefault();
- this.target = new ParameterMessageInterpolator();
- }
-
- @Override
- public String interpolate(String messageTemplate, Context context) {
- return this.target.interpolate(messageTemplate, context, this.locale);
- }
-
- @Override
- public String interpolate(String messageTemplate, Context context, Locale locale) {
- return this.target.interpolate(messageTemplate, context, locale);
- }
-
- /**
- * 设置地区。
- *
- * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。
- */
- public void setLocale(Locale locale) {
- this.locale = locale;
- }
-}
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
index ab4805c70..3f71554d0 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
@@ -43,10 +43,10 @@
public class ValidationHandler implements AutoCloseable {
private final ValidatorFactory validatorFactory;
private final Validator validator;
- private final LocaleMessageInterpolator messageInterpolator;
+ private final LocaleContextMessageInterpolator messageInterpolator;
public ValidationHandler() {
- this.messageInterpolator = new LocaleMessageInterpolator();
+ this.messageInterpolator = new LocaleContextMessageInterpolator();
this.validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.messageInterpolator(this.messageInterpolator)
@@ -58,7 +58,7 @@ public ValidationHandler() {
/**
* 设置校验信息语言。
*
- * @param locale 校验语言 {@link Locale}。
+ * @param locale 表示校验语言的 {@link Locale}。
*/
public void setLocale(Locale locale) {
this.messageInterpolator.setLocale(locale);
@@ -163,4 +163,4 @@ private boolean isJakartaConstraintAnnotation(Annotation annotation) {
return "jakarta.validation".equals(packageName) && "Constraint".equals(className);
});
}
-}
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java
new file mode 100644
index 000000000..1a1fa4a31
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java
@@ -0,0 +1,148 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import modelengine.fit.http.client.HttpClassicClientResponse;
+import modelengine.fit.http.entity.Entity;
+import modelengine.fit.http.entity.ObjectEntity;
+import modelengine.fitframework.annotation.Fit;
+import modelengine.fitframework.test.annotation.MvcTest;
+import modelengine.fitframework.test.domain.mvc.MockMvc;
+import modelengine.fitframework.test.domain.mvc.request.MockMvcRequestBuilders;
+import modelengine.fitframework.test.domain.mvc.request.MockRequestBuilder;
+import modelengine.fitframework.validation.data.Company;
+import modelengine.fitframework.validation.data.LocaleValidationController;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * 表示评估国际化校验的测试类。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+@MvcTest(classes = {LocaleValidationController.class})
+@DisplayName("测试地区化验证消息功能")
+public class LocaleValidationControllerTest {
+ @Fit
+ private MockMvc mockMvc;
+
+ private HttpClassicClientResponse> response;
+
+ @AfterEach
+ void teardown() throws IOException {
+ if (this.response != null) {
+ this.response.close();
+ }
+ }
+
+ @Test
+ @DisplayName("测试法文地区的验证消息")
+ void shouldReturnFrenchValidationMessage() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .header("Accept-Language", "fr")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: ne doit pas être nul");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+
+ @Test
+ @DisplayName("测试英文地区的验证消息")
+ void shouldReturnEnglishValidationMessage() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .header("Accept-Language", "en-us")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+
+ @Test
+ @DisplayName("测试URL参数指定地区")
+ void shouldUseLocaleFromUrlParam() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .param("locale", "en-US")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
+ assertThat(this.response.cookies().get("locale").isPresent());
+ assertThat(this.response.cookies().get("locale").get().value()).isEqualTo("en-US");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
index 2b5d9def0..084a3b360 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
@@ -341,7 +341,8 @@ class StudentGroupValidationTests {
public void givenParametersThenGroupValidateHappened() {
// 测试学生年龄验证 - 现在会抛出异常,因为使用了学生分组
Method method = ReflectionUtils.getDeclaredMethod(GroupValidateService.StudentValidateService.class,
- "validateStudentAge", int.class);
+ "validateStudentAge",
+ int.class);
Method handleValidatedMethod = ReflectionUtils.getDeclaredMethod(ValidationHandler.class,
"handle",
JoinPoint.class,
@@ -755,4 +756,21 @@ void testRangeBigDecimalValidation() {
ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {new BigDecimal("5.5")});
assertThat(exception.getMessage()).contains("需要在10和100之间");
}
-}
+
+ @Nested
+ @DisplayName("测试 Locale 默认值为 null 时的情况")
+ public class ValidationHandlerNullTest {
+ @BeforeEach
+ void setUp() {
+ ValidationHandlerTest.this.handler.setLocale(null);
+ }
+
+ @Test
+ @DisplayName("测试@Null注解")
+ void testNullValidation() {
+ Method method = ReflectionUtils.getDeclaredMethod(ValidateService.class, "testNull", String.class);
+ ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {"not null"});
+ assertThat(exception.getMessage()).isNotNull();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
index bba16b88b..bc45d868f 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
@@ -32,9 +32,9 @@ public static class StudentValidateService {
* @param age 表示年龄的 {@code int}。
*/
public void validateStudentAge(
- @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) @Max(
- value = 20, message = "范围要在7~20之内",
- groups = ValidationTestData.StudentGroup.class) int age) {
+ @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class)
+ @Max(value = 20, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class)
+ int age) {
LOG.debug("Validating student age: {}", age);
}
}
@@ -48,9 +48,9 @@ public static class TeacherValidateService {
* @param age 表示年龄的 {@code int}。
*/
public void validateTeacherAge(
- @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) @Max(
- value = 65, message = "范围要在22~65之内",
- groups = ValidationTestData.TeacherGroup.class) int age) {
+ @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class)
+ @Max(value = 65, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class)
+ int age) {
LOG.debug("Validating teacher age: {}", age);
}
}
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java
new file mode 100644
index 000000000..e0df5f8aa
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation.data;
+
+import modelengine.fit.http.util.i18n.LocaleResolveFilter;
+import modelengine.fitframework.annotation.Bean;
+import modelengine.fitframework.annotation.Component;
+
+/**
+ * 表示地区解析过滤器的配置类。
+ *
+ * @author 阮睿
+ * @since 2025-09-11
+ */
+@Component
+public class LocaleResolveFilterConfig {
+ /**
+ * 创建地区解析过滤器 bean 对象。
+ *
+ * @return 表示作为 bean 的地区解析过滤器对象的 {@link LocaleResolveFilter}。
+ */
+ @Bean
+ public LocaleResolveFilter localeResolveFilter() {
+ return new LocaleResolveFilter();
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java
new file mode 100644
index 000000000..872e72659
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation.data;
+
+import jakarta.validation.Valid;
+import modelengine.fit.http.annotation.PostMapping;
+import modelengine.fit.http.annotation.RequestBody;
+import modelengine.fit.http.annotation.RequestMapping;
+import modelengine.fitframework.annotation.Component;
+import modelengine.fitframework.validation.LocaleContextMessageInterpolator;
+import modelengine.fitframework.validation.Validated;
+import modelengine.fitframework.validation.ValidationHandler;
+
+/**
+ * 用于测试 {@link ValidationHandler} 与 {@link LocaleContextMessageInterpolator} 的集成地区验证控制器。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+@Component
+@RequestMapping(path = "/validation/locale", group = "地区验证测试接口")
+@Validated
+public class LocaleValidationController {
+ /**
+ * 使用简单参数测试验证消息的地区化。
+ *
+ * @param company 表示注解验证的测试实体类 {@link Company}。
+ */
+ @PostMapping(path = "/simple", description = "测试简单参数的地区化验证消息")
+ public void validateSimpleParam(@RequestBody @Valid Company company) {}
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java
new file mode 100644
index 000000000..ef9be86f3
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation;
+
+import modelengine.fitframework.inspection.Validation;
+import modelengine.fitframework.util.ObjectUtils;
+import modelengine.fitframework.util.i18n.LocaleContextHolder;
+
+import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
+
+import java.util.Locale;
+
+import javax.validation.MessageInterpolator;
+
+/**
+ * 检验消息处理的代理类。
+ *
+ * 从 {@link LocaleContextHolder} 中获取当前线程设置的 {@link Locale} 并委托 {@link MessageInterpolator} 去处理消息。
+ *
+ *
+ * @author 阮睿
+ * @since 2025-07-31
+ */
+public class LocaleContextMessageInterpolator implements MessageInterpolator {
+ private final MessageInterpolator targetInterpolator;
+ private Locale locale;
+
+ /**
+ * 构造函数。
+ *
+ * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
+ */
+ public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) {
+ this.targetInterpolator = targetInterpolator;
+ this.locale = Locale.getDefault();
+ }
+
+ /**
+ * 构造函数,默认使用 {@link ParameterMessageInterpolator} 作为目标检验消息处理对象。
+ */
+ public LocaleContextMessageInterpolator() {
+ this.targetInterpolator = new ParameterMessageInterpolator();
+ this.locale = Locale.getDefault();
+ }
+
+ /**
+ * 构造函数。
+ *
+ * @param locale 表示当前设置默认的 {@link Locale}。
+ */
+ public LocaleContextMessageInterpolator(Locale locale) {
+ this.targetInterpolator = new ParameterMessageInterpolator();
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ /**
+ * 构造函数。
+ *
+ * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
+ * @param locale 表示当前设置默认的 {@link Locale}。
+ */
+ public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator, Locale locale) {
+ this.targetInterpolator = targetInterpolator;
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ /**
+ * 设置默认的 {@link Locale}。
+ *
+ * @param locale 默认设置的 {@link Locale}。
+ */
+ public void setLocale(Locale locale) {
+ this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context) {
+ if (LocaleContextHolder.getLocale() != null) {
+ return this.targetInterpolator.interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
+ }
+ return this.targetInterpolator.interpolate(messageTemplate, context, this.locale);
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context, Locale locale) {
+ Validation.notNull(locale, "Locale cannot be null.");
+ return this.targetInterpolator.interpolate(messageTemplate, context, locale);
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java
deleted file mode 100644
index 74438d75c..000000000
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
- * This file is a part of the ModelEngine Project.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-package modelengine.fitframework.validation;
-
-import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
-
-import java.util.Locale;
-
-import javax.validation.MessageInterpolator;
-
-/**
- * 地区消息插值器。
- *
- * 作为 Jakarta 消息插值器的代理类,提供地区设置能力。
- *
- *
- * @author 阮睿
- * @since 2025-08-18
- */
-public class LocaleMessageInterpolator implements MessageInterpolator {
- private final MessageInterpolator target;
-
- private Locale locale;
-
- /**
- * 构造函数,使用指定的目标消息插值器初始化实例。
- *
- * @param target 表示目标消息插值器的 {@link MessageInterpolator}。
- */
- public LocaleMessageInterpolator(MessageInterpolator target) {
- this.target = target;
- this.locale = Locale.getDefault();
- }
-
- /**
- * 构造函数,使用指定的地区初始化实例。
- *
- * @param locale 表示指定地区的 {@link Locale}。
- */
- public LocaleMessageInterpolator(Locale locale) {
- this.locale = locale;
- this.target = new ParameterMessageInterpolator();
- }
-
- /**
- * 构造函数,使用指定的目标消息插值器和地区初始化实例。
- *
- * @param target 表示被代理的目标消息插值器的 {@link MessageInterpolator}。
- * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。
- */
- public LocaleMessageInterpolator(MessageInterpolator target, Locale locale) {
- this.target = target;
- this.locale = locale;
- }
-
- /**
- * 构造函数,使用默认地区初始化实例。
- */
- public LocaleMessageInterpolator() {
- this.locale = Locale.getDefault();
- this.target = new ParameterMessageInterpolator();
- }
-
- @Override
- public String interpolate(String messageTemplate, Context context) {
- return this.target.interpolate(messageTemplate, context, this.locale);
- }
-
- @Override
- public String interpolate(String messageTemplate, Context context, Locale locale) {
- return this.target.interpolate(messageTemplate, context, locale);
- }
-
- /**
- * 设置地区。
- *
- * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。
- */
- public void setLocale(Locale locale) {
- this.locale = locale;
- }
-}
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
index 40e9086df..195d773a4 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java
@@ -44,10 +44,10 @@
public class ValidationHandler implements AutoCloseable {
private final ValidatorFactory validatorFactory;
private final Validator validator;
- private final LocaleMessageInterpolator messageInterpolator;
+ private final LocaleContextMessageInterpolator messageInterpolator;
public ValidationHandler() {
- this.messageInterpolator = new LocaleMessageInterpolator();
+ this.messageInterpolator = new LocaleContextMessageInterpolator();
this.validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.messageInterpolator(this.messageInterpolator)
@@ -164,4 +164,4 @@ private boolean isJavaxConstraintAnnotation(Annotation annotation) {
return "javax.validation".equals(packageName) && "Constraint".equals(className);
});
}
-}
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java
new file mode 100644
index 000000000..1a1fa4a31
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java
@@ -0,0 +1,148 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import modelengine.fit.http.client.HttpClassicClientResponse;
+import modelengine.fit.http.entity.Entity;
+import modelengine.fit.http.entity.ObjectEntity;
+import modelengine.fitframework.annotation.Fit;
+import modelengine.fitframework.test.annotation.MvcTest;
+import modelengine.fitframework.test.domain.mvc.MockMvc;
+import modelengine.fitframework.test.domain.mvc.request.MockMvcRequestBuilders;
+import modelengine.fitframework.test.domain.mvc.request.MockRequestBuilder;
+import modelengine.fitframework.validation.data.Company;
+import modelengine.fitframework.validation.data.LocaleValidationController;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * 表示评估国际化校验的测试类。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+@MvcTest(classes = {LocaleValidationController.class})
+@DisplayName("测试地区化验证消息功能")
+public class LocaleValidationControllerTest {
+ @Fit
+ private MockMvc mockMvc;
+
+ private HttpClassicClientResponse> response;
+
+ @AfterEach
+ void teardown() throws IOException {
+ if (this.response != null) {
+ this.response.close();
+ }
+ }
+
+ @Test
+ @DisplayName("测试法文地区的验证消息")
+ void shouldReturnFrenchValidationMessage() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .header("Accept-Language", "fr")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: ne doit pas être nul");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+
+ @Test
+ @DisplayName("测试英文地区的验证消息")
+ void shouldReturnEnglishValidationMessage() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .header("Accept-Language", "en-us")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+
+ @Test
+ @DisplayName("测试URL参数指定地区")
+ void shouldUseLocaleFromUrlParam() {
+ Company invalidCompany = new Company(null);
+
+ MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
+ .param("locale", "en-US")
+ .jsonEntity(invalidCompany)
+ .responseType(Map.class);
+
+ this.response = this.mockMvc.perform(requestBuilder);
+
+ // 获取JSON格式的错误信息
+ String errorMessage = "";
+ if (this.response.entity().isPresent()) {
+ Entity entity = this.response.entity().get();
+ if (entity instanceof ObjectEntity) {
+ ObjectEntity> objectEntity = (ObjectEntity>) entity;
+ Object errorObj = objectEntity.object();
+ if (errorObj instanceof Map) {
+ Map errorMap = (Map) errorObj;
+ errorMessage =
+ errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
+ } else {
+ errorMessage = errorObj.toString();
+ }
+ }
+ }
+
+ assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
+ assertThat(this.response.cookies().get("locale").isPresent());
+ assertThat(this.response.cookies().get("locale").get().value()).isEqualTo("en-US");
+ assertThat(this.response.statusCode()).isEqualTo(500);
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
index b23b8dd09..08e311889 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java
@@ -59,10 +59,10 @@ public class ValidationHandlerTest {
@BeforeEach
void setUp() {
- handler.setLocale(Locale.CHINA);
- when(validated.value()).thenReturn(new Class[0]);
- when(fitRuntime.resolverOfAnnotations()).thenReturn(annotationMetadataResolver);
- when(beanContainer.runtime()).thenReturn(fitRuntime);
+ this.handler.setLocale(Locale.CHINA);
+ when(this.validated.value()).thenReturn(new Class[0]);
+ when(this.fitRuntime.resolverOfAnnotations()).thenReturn(annotationMetadataResolver);
+ when(this.beanContainer.runtime()).thenReturn(fitRuntime);
}
private ConstraintViolationException invokeHandleMethod(Method targetMethod, Object[] args) {
@@ -757,4 +757,21 @@ void testRangeBigDecimalValidation() {
ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {new BigDecimal("5.5")});
assertThat(exception.getMessage()).contains("需要在10和100之间");
}
+
+ @Nested
+ @DisplayName("测试 Locale 默认值为 null 时的情况")
+ public class ValidationHandlerNullTest {
+ @BeforeEach
+ void setUp() {
+ ValidationHandlerTest.this.handler.setLocale(null);
+ }
+
+ @Test
+ @DisplayName("测试@Null注解")
+ void testNullValidation() {
+ Method method = ReflectionUtils.getDeclaredMethod(ValidateService.class, "testNull", String.class);
+ ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {"not null"});
+ assertThat(exception.getMessage()).isNotNull();
+ }
+ }
}
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
index d299bdfd5..ee3dad196 100644
--- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java
@@ -33,9 +33,9 @@ public static class StudentValidateService {
* @param age 表示年龄的 {@code int}。
*/
public void validateStudentAge(
- @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) @Max(
- value = 20, message = "范围要在7~20之内",
- groups = ValidationTestData.StudentGroup.class) int age) {
+ @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class)
+ @Max(value = 20, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class)
+ int age) {
LOG.debug("Validating student age: {}", age);
}
}
@@ -49,9 +49,9 @@ public static class TeacherValidateService {
* @param age 表示年龄的 {@code int}。
*/
public void validateTeacherAge(
- @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) @Max(
- value = 65, message = "范围要在22~65之内",
- groups = ValidationTestData.TeacherGroup.class) int age) {
+ @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class)
+ @Max(value = 65, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class)
+ int age) {
LOG.debug("Validating teacher age: {}", age);
}
}
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java
new file mode 100644
index 000000000..e0df5f8aa
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation.data;
+
+import modelengine.fit.http.util.i18n.LocaleResolveFilter;
+import modelengine.fitframework.annotation.Bean;
+import modelengine.fitframework.annotation.Component;
+
+/**
+ * 表示地区解析过滤器的配置类。
+ *
+ * @author 阮睿
+ * @since 2025-09-11
+ */
+@Component
+public class LocaleResolveFilterConfig {
+ /**
+ * 创建地区解析过滤器 bean 对象。
+ *
+ * @return 表示作为 bean 的地区解析过滤器对象的 {@link LocaleResolveFilter}。
+ */
+ @Bean
+ public LocaleResolveFilter localeResolveFilter() {
+ return new LocaleResolveFilter();
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java
new file mode 100644
index 000000000..41572320a
--- /dev/null
+++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java
@@ -0,0 +1,36 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.validation.data;
+
+import modelengine.fit.http.annotation.PostMapping;
+import modelengine.fit.http.annotation.RequestBody;
+import modelengine.fit.http.annotation.RequestMapping;
+import modelengine.fitframework.annotation.Component;
+import modelengine.fitframework.validation.LocaleContextMessageInterpolator;
+import modelengine.fitframework.validation.Validated;
+import modelengine.fitframework.validation.ValidationHandler;
+
+import javax.validation.Valid;
+
+/**
+ * 用于测试 {@link ValidationHandler} 与 {@link LocaleContextMessageInterpolator} 的集成地区验证控制器。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+@Component
+@RequestMapping(path = "/validation/locale", group = "地区验证测试接口")
+@Validated
+public class LocaleValidationController {
+ /**
+ * 使用简单参数测试验证消息的地区化。
+ *
+ * @param company 表示注解验证的测试实体类 {@link Company}。
+ */
+ @PostMapping(path = "/simple", description = "测试简单参数的地区化验证消息")
+ public void validateSimpleParam(@RequestBody @Valid Company company) {}
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java
new file mode 100644
index 000000000..f6c703d21
--- /dev/null
+++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java
@@ -0,0 +1,105 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fit.http.util.i18n;
+
+import modelengine.fit.http.Cookie;
+import modelengine.fit.http.server.HttpClassicServerRequest;
+import modelengine.fit.http.server.HttpClassicServerResponse;
+import modelengine.fitframework.util.StringUtils;
+
+import java.util.Locale;
+
+/**
+ * 默认地区解析器。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+public class DefualtLocaleResolver implements LocaleResolver {
+ private String cookieName = DEFAULT_COOKIE_NAME;
+ private int cookieMaxAge = DEFAULT_COOKIE_MAX_AGE;
+ private String cookieDomain = DEFAULT_COOKIE_DOMAIN;
+ private String cookiePath = DEFAULT_COOKIE_PATH;
+ private Locale defaultLocale = Locale.getDefault();
+
+ @Override
+ public Locale resolveLocale(HttpClassicServerRequest request) {
+ // 先解析 Cookie,如果没有则解析 Accept-Language 头。
+ String newLocale = request.cookies().get(this.cookieName).map(Cookie::value).orElse(null);
+ if (StringUtils.isNotBlank(newLocale)) {
+ return Locale.forLanguageTag(newLocale);
+ }
+
+ String acceptLanguage = request.headers().first("Accept-Language").orElse(null);
+ if (StringUtils.isNotBlank(acceptLanguage)) {
+ return Locale.forLanguageTag(acceptLanguage);
+ }
+
+ return this.defaultLocale;
+ }
+
+ @Override
+ public void setLocale(HttpClassicServerResponse response, Locale locale) {
+ if (locale != null) {
+ response.cookies()
+ .add(Cookie.builder()
+ .name(this.cookieName)
+ .value(locale.toLanguageTag())
+ .maxAge(this.cookieMaxAge)
+ .domain(this.cookieDomain)
+ .path(this.cookiePath)
+ .build());
+ } else {
+ response.cookies().add(Cookie.builder().name(this.cookieName).maxAge(0).build());
+ }
+ }
+
+ /**
+ * 设置存储地区信息的 Cookie 名称。
+ *
+ * @param cookieName 表示待设置 Cookie 名称的 {@link String}。
+ */
+ public void setCookieName(String cookieName) {
+ this.cookieName = cookieName;
+ }
+
+ /**
+ * 设置存储地区信息的 Cookie 的最大有效期。
+ *
+ * @param cookieMaxAge 表示待设置的 Cookie 最大有效期的 {@code int}。
+ */
+ public void setCookieMaxAge(int cookieMaxAge) {
+ this.cookieMaxAge = cookieMaxAge;
+ }
+
+ /**
+ * 设置存储地区信息的 Cookie 的作用域。
+ *
+ * @param cookieDomain 存储地区信息的 Cookie 作用域的 {@link String}。
+ */
+ public void setCookieDomain(String cookieDomain) {
+ this.cookieDomain = cookieDomain;
+ }
+
+ /**
+ * 设置存储地区信息的 Cookie 的可见 URL 路径。
+ *
+ * @param cookiePath 存储地区信息的 Cookie 作用域的 {@link String}。
+ */
+ public void setCookiePath(String cookiePath) {
+ this.cookiePath = cookiePath;
+ }
+
+ /**
+ * 设置默认地区。
+ *
+ * @param defaultLocale 表示默认地区的 {@link Locale}。
+ */
+ public void setDefaultLocale(Locale defaultLocale) {
+ this.defaultLocale = defaultLocale;
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java
new file mode 100644
index 000000000..e629cfcd5
--- /dev/null
+++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java
@@ -0,0 +1,110 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fit.http.util.i18n;
+
+import modelengine.fit.http.server.DoHttpServerFilterException;
+import modelengine.fit.http.server.HttpClassicServerRequest;
+import modelengine.fit.http.server.HttpClassicServerResponse;
+import modelengine.fit.http.server.HttpServerFilter;
+import modelengine.fit.http.server.HttpServerFilterChain;
+import modelengine.fitframework.annotation.Scope;
+import modelengine.fitframework.inspection.Validation;
+import modelengine.fitframework.util.StringUtils;
+import modelengine.fitframework.util.i18n.LocaleContextHolder;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+/**
+ * 地区解析过滤器,使用 {@link LocaleResolver} 进行地区解析。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+public class LocaleResolveFilter implements HttpServerFilter {
+ private LocaleResolver localeResolver = null;
+ private List matchPatterns = List.of("/**");
+ private List mismatchPatterns = List.of();
+ private Scope scope = Scope.PLUGIN;
+
+ /**
+ * 构造函数。
+ *
+ * @param localeResolver 表示地区解析器的 {@link LocaleResolver}。
+ */
+ public LocaleResolveFilter(LocaleResolver localeResolver) {
+ this.localeResolver = Validation.notNull(localeResolver, "The locale resolver cannot be null.");
+ }
+
+ /**
+ * 默认构造函数。
+ */
+ public LocaleResolveFilter() {
+ this.localeResolver = new DefualtLocaleResolver();
+ }
+
+ @Override
+ public String name() {
+ return "LocaleResolveFilter";
+ }
+
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ @Override
+ public List matchPatterns() {
+ return this.matchPatterns;
+ }
+
+ @Override
+ public List mismatchPatterns() {
+ return this.mismatchPatterns;
+ }
+
+ @Override
+ public void doFilter(HttpClassicServerRequest request, HttpClassicServerResponse response,
+ HttpServerFilterChain chain) throws DoHttpServerFilterException {
+ try {
+ Locale responseLocale = this.resolveLocaleFromParam(request);
+ // 如果参数中带有地区,说明用户想使用新地区执行后续的操作,直接设置地区。
+ if (responseLocale != null) {
+ LocaleContextHolder.setLocale(responseLocale);
+ } else {
+ // 如果参数中不包含地区,则解析请求所带的地区参数。
+ Locale locale = this.localeResolver.resolveLocale(request);
+ LocaleContextHolder.setLocale(locale);
+ }
+
+ // 继续执行后续过滤器。
+ chain.doFilter(request, response);
+
+ if (!response.isCommitted()) {
+ // responseLocale 是用户期望设置的地区,不受 server 端处理的影响。
+ this.localeResolver.setLocale(response, responseLocale);
+ }
+ } finally {
+ LocaleContextHolder.clear();
+ }
+ }
+
+ @Override
+ public Scope scope() {
+ return this.scope;
+ }
+
+ private Locale resolveLocaleFromParam(HttpClassicServerRequest request) {
+ Optional paramLocale = request.queries().first("locale");
+ String localeString = paramLocale.orElse(null);
+ if (StringUtils.isNotBlank(localeString)) {
+ return Locale.forLanguageTag(localeString);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java
new file mode 100644
index 000000000..b4e636e42
--- /dev/null
+++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java
@@ -0,0 +1,56 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fit.http.util.i18n;
+
+import modelengine.fit.http.server.HttpClassicServerRequest;
+import modelengine.fit.http.server.HttpClassicServerResponse;
+
+import java.util.Locale;
+
+/**
+ * 地区解析器接口,用于从 HTTP 请求中解析用户的地区设置。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+public interface LocaleResolver {
+ /**
+ * 表示待设置 cookie 的名称。
+ */
+ public static final String DEFAULT_COOKIE_NAME = "locale";
+
+ /**
+ * 表示待设置 cookie 的自动过期时间。
+ */
+ public static final int DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
+
+ /**
+ * 表示待设置 Cookie 的可见域。
+ */
+ public static final String DEFAULT_COOKIE_DOMAIN = "/";
+
+ /**
+ * 表示待设置 Cookie 的可见 URL 路径。
+ */
+ public static final String DEFAULT_COOKIE_PATH = "/";
+
+ /**
+ * 解析用户的地区设置。
+ *
+ * @param request 表示待解析 HTTP 请求的 {@link HttpClassicServerRequest}。
+ * @return 表示解析出来地区信息的 {@link Locale}。
+ */
+ Locale resolveLocale(HttpClassicServerRequest request);
+
+ /**
+ * 设置地区到返回响应中。
+ *
+ * @param response 表示待设置地区的 HTTP 响应的 {@link HttpClassicServerResponse}。
+ * @param locale 表示待设置地区的 {@link Locale}。
+ */
+ void setLocale(HttpClassicServerResponse response, Locale locale);
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java
new file mode 100644
index 000000000..4ae67f9cc
--- /dev/null
+++ b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.util.i18n;
+
+import java.util.Locale;
+
+/**
+ * 表示存储地区的线程上下文。
+ *
+ * @author 阮睿
+ * @since 2025-08-01
+ */
+public class LocaleContextHolder {
+ private static final ThreadLocal LOCALE_CONTEXT_HOLDER = new ThreadLocal<>();
+
+ /**
+ * 设置当前线程的地区上下文。
+ *
+ * @param locale 表示待存储在当前线程地区上下文的 {@link Locale}。
+ */
+ public static void setLocale(Locale locale) {
+ if (locale != null) {
+ LOCALE_CONTEXT_HOLDER.set(locale);
+ }
+ }
+
+ /**
+ * 获取当前线程的地区。
+ *
+ * @return 表示当前线程上下文存储地区信息的 {@link Locale}。
+ */
+ public static Locale getLocale() {
+ return LOCALE_CONTEXT_HOLDER.get();
+ }
+
+ /**
+ * 清除当前线程的地区上下文。
+ */
+ public static void clear() {
+ LOCALE_CONTEXT_HOLDER.remove();
+ }
+}
\ No newline at end of file
diff --git a/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java
new file mode 100644
index 000000000..6c398ae38
--- /dev/null
+++ b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java
@@ -0,0 +1,72 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fitframework.util.i18n;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Locale;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link LocaleContextHolder} 的单元测试。
+ *
+ * @author 阮睿
+ * @since 2025-09-09
+ */
+@DisplayName("测试 LocaleContextHolder")
+public class LocaleContextHolderTest {
+ @AfterEach
+ void tearDown() {
+ LocaleContextHolder.clear();
+ }
+
+ @Nested
+ @DisplayName("Test method: setLocale and getLocale")
+ class TestSetAndGetLocaleContext {
+ @Test
+ @DisplayName("Given locale with zh_CN then return the same locale")
+ void givenLocaleContextWithZhCNThenReturnSameLocaleContext() {
+ Locale locale = Locale.SIMPLIFIED_CHINESE;
+ LocaleContextHolder.setLocale(locale);
+ assertThat(LocaleContextHolder.getLocale()).isEqualTo(locale);
+ }
+
+ @Test
+ @DisplayName("Given locale with en_US then return the same locale")
+ void givenLocaleContextWithEnUSThenReturnSameLocaleContext() {
+ Locale locale = Locale.US;
+ LocaleContextHolder.setLocale(locale);
+ assertThat(LocaleContextHolder.getLocale()).isEqualTo(locale);
+ }
+
+ @Test
+ @DisplayName("Given null locale then not set and return null")
+ void givenNullLocaleContextThenReturnNull() {
+ LocaleContextHolder.setLocale(null);
+ assertThat(LocaleContextHolder.getLocale()).isNull();
+ }
+ }
+
+ @Nested
+ @DisplayName("Test method: clear")
+ class TestClear {
+ @Test
+ @DisplayName("Given existing locale then clear it")
+ void givenExistingLocaleContextThenClearIt() {
+ Locale locale = Locale.SIMPLIFIED_CHINESE;
+ LocaleContextHolder.setLocale(locale);
+ assertThat(LocaleContextHolder.getLocale()).isNotNull();
+
+ LocaleContextHolder.clear();
+ assertThat(LocaleContextHolder.getLocale()).isNull();
+ }
+ }
+}
\ No newline at end of file