12.1 国际化

March 4, 2026 · View on GitHub

Properties的定义

Java 中的多语言字符串模板一般保存在properties资源文件中,且遵照以下的命名规范:<资源名>_<语言代码>.properties。例如,用户可以定义中英文两种多语言资源文件,并将其放在 resources 目录下,其中定义的资源名为 message:

  • 文件名为 message_en.properties,表示英文资源文件,内容为
HELLO=Hello, {0}!
  • 文件名为 message_cn.properties,表示中文资源文件,内容为
HELLO=你好, {0}!

可以看到,两种语言的 Key 完全一致,只是 Value 是对应语言的字符串。这样应用程序就可以通过 Locale 对象和 Key 精确调用到某个具体的属性值了。

StringResource

FIT 框架提供了 StringResource 接口用于为应用程序的国际化提供字符串管理能力,定义了方法getMessage()和getMessageWithDefault()用于获取在指定语言环境中使用的指定键的字符串资源,其中,参数locale表示目标语言环境,key表示资源的键,defaultMessage表示资源的默认值,args表示资源的格式化参数:

// 获取消息,无默认值
String getMessage(Locale locale, String key, Object... args);

// 获取消息,支持指定默认值
String getMessageWithDefault(Locale locale, String key, String defaultMessage, Object... args);

FIT 内部提供了三个实现类:

  1. EmptyStringResource:为StringResource提供空实现,返回一个null值。
  2. ResourceBundleStringResource:提供一个基于ResourceBundle的实现,可用以获取指定资源的在多语言环境下使用的消息资源,其中,ResourceBundle是java.util提供的一个用于管理和访问国际化资源的类。
  3. StringResourceComposite:为StringResource提供组合模式的实现,可将一系列的消息提供程序组合为一个实例。

这些实现类为框架内部使用,开发者通过 StringResources.forPlugin(Plugin) 工厂方法获取 StringResource 实例,示例如下:

StringResource resource = StringResources.forPlugin(plugin);
String messageEn = resource.getMessage(new Locale("en"), "HELLO", "world");

地区解析器

FIT 框架提供了 LocaleResolver 接口用于从 HTTP 请求中解析用户的地区设置,该接口定义了两个核心方法:

public interface LocaleResolver {
    /**
     * 解析用户的地区设置。
     *
     * @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);
}

其中,resolveLocale() 方法用于从请求中解析地区信息,setLocale() 方法用于在响应中设置地区信息。

FIT 提供了 DefualtLocaleResolver 实现类,该类支持从多个来源解析地区信息,按优先级顺序为:

  1. Cookie:从名为 locale 的 Cookie 中获取地区信息。
  2. 请求头:从 Accept-Language 请求头中解析地区信息。

注意:URL 请求参数中的 locale 参数由 LocaleResolveFilter 负责解析,其优先级高于上述来源。详见下方"地区解析过滤器"章节。

地区解析过滤器

LocaleResolveFilter 是一个 HTTP 过滤器,用于自动处理请求的地区解析和响应的地区设置。该过滤器会在请求处理过程中自动执行以下操作:

  • 使用配置的 LocaleResolver 解析请求中的地区信息。
  • 将解析得到的地区信息设置到 LocaleContextHolder 中。
  • 根据 LocaleResolver 的实现逻辑在响应中设置相应的地区状态。
  • 在请求处理完成后清理线程本地的地区信息。

使用示例如下:

@Component
public class WebConfig {
    @Bean
    public LocaleResolveFilter localeResolveFilter() {
        return new LocaleResolveFilter(new DefualtLocaleResolver());
    }
}

地区上下文管理

FIT 框架提供了 LocaleContextHolder 类用于在当前线程中管理地区信息,该类使用 ThreadLocal 机制确保多线程环境下的地区信息隔离。主要方法包括:

public class LocaleContextHolder {
    /**
     * 设置当前线程的地区上下文。
     *
     * @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();
    }
}

使用示例:

public class testLocaleContextHolder {
    public void test() {
        try {
            // 设置当前线程的地区为英文。
            LocaleContextHolder.setLocale(Locale.ENGLISH);
            // 获取当前线程的地区。
            Locale currentLocale = LocaleContextHolder.getLocale();
        } finally {
            // 请在使用完毕后尽快清理当前线程的地区信息。
            LocaleContextHolder.clear();
        }
    }
}

校验消息国际化

FIT 框架的校验功能支持国际化,实现通过 LocaleContextMessageInterpolator 类实现校验消息的多语言支持。该类会根据 LocaleContextHolder 中设置的地区信息自动选择对应语言的校验消息模板。

12.2 Event事件

12.2.1 相关接口

FIT 支持不同插件之间通过发布-订阅机制进行解耦的通信,提供了三个事件相关的接口供调用方使用,分别是Event、EventPublisher、EventHandler,其中,Publisher订阅Event,并将Event分发到各个EventHandler做出响应,定义如下:

  1. Event接口定义了事件,含一个接口方法publisher(),开发者需要通过扩展及实现该接口来自定义事件属性。
public interface Event {
    Object publisher();
}
  1. EventPublisher是事件的发布程序的接口:
public interface EventPublisher {
    <E extends Event> void publishEvent(E event);
}

FIT 提供了两种EventPublisher实现类供开发者使用:

实现类获取方式说明
FitRuntimeEventPublisherFitRuntime该类为 FitRuntime 提供事件发布程序,适合发布应用级事件时调用,应用级事件表示事件的发布和处理有可能需要跨插件,与整个应用程序相关,如应用启动、服务连接等事件。
PluginEventPublisherPlugin该类为 Plugin 提供事件发布程序,适合发布插件级事件时调用,插件级事件表示事件的发布和处理都在本插件内,不与整个应用程序相关。
  1. EventHandler是为事件提供处理程序的接口:
public interface EventHandler<E extends Event> {
    void handleEvent(@Nonnull E event);
}

12.2.2 自定义Event事件

首先,通过实现Event接口来进行事件的自定义,示例如下:

public class MyEvent implements Event {
    private final User user;
    private final Object publisher;

    public MyEvent(User user, Object publisher) {
        this.user = user;
        this.publisher = publisher;
    }

    public User getUser() {
        return this.user;
    }

    @Override
    public Object publisher() {
        return this.publisher;
    }
}

此处,通过构造函数入参的方式来提供当前事件的发布者,通常为发布事件的对象本身,为事件处理类提供事件发布者的信息。

通过实现EventHandler接口来自定义接收到事件后作出的响应,添加注解@Component使之生成Bean,可自动被EventPublisher检测到,示例如下:

@Component
public class MyEventHandler implements EventHandler<MyEvent> {
    @Override
    public void handleEvent(MyEvent event) {
        System.out.println(event.getUser().getName());
    }
}

定义Event以及EventHandler后,需要使用EventPublisher对事件进行发布,FIT 提供了FitRuntimeEventPublisher和PluginEventPublisher 两个实现类。

FitRuntimeEventPublisher适用于发布应用级事件时使用,可通过依赖注入的方式获取FitRuntime,使用方式示例如下:

@Component
public class UserController {
    private static int counter = 0;

    private final FitRuntime runtime ;

    public UserController(FitRuntime runtime) {
        this.runtime = runtime;
    }

    @GetMapping(path = "/user")
    public User getUser(@RequestParam("name") String name, @RequestParam("age") String age) {
        User user = new User(name, age, ++counter);
        this.runtime.publisherOfEvents().publishEvent(new MyEvent(user, this));
        return user;
    }
}

PluginEventPublisher适用于发布插件级事件时使用,可通过依赖注入的方式获取Plugin,使用方式示例如下:

@Component
public class UserController {
    private static int counter = 0;

    private final Plugin plugin;

    public UserController(Plugin plugin) {
        this.plugin = plugin;
    }

    @GetMapping(path = "/user")
    public User getUser(@RequestParam("name") String name, @RequestParam("age") String age) {
        User user = new User(name, age, ++counter);
        this.plugin.publisherOfEvents().publishEvent(new MyEvent(user, this));
        return user;
    }
}

12.2.3 FIT 生命周期事件

FIT 框架启动过程中提供了一系列事件供开发者监听和处理,允许开发者在 FIT 的启动阶段执行自定义逻辑,这些类都属于Events的内部类:

事件类实现接口说明
PreparedFitRuntimePreparedEvent事件在 FIT 初始化结束,准备启动时触发,指示 FIT 运行时环境已准备就绪
StartedFitRuntimeStartedEvent事件在 FIT 启动成功后触发,指示 FIT 运行时环境已启动
FailedFitRuntimeFailedEvent事件在 FIT 启动时捕获到异常时触发,指示 FIT 运行时环境启动失败

定义一个EventHandler类处理Prepared类事件,可通过runtime()函数获取运行时环境,也可通过duration()函数获取 FIT 运行时环境准备就绪的时长,示例如下:

@Component
public class MyEventHandler implements EventHandler<FitRuntimePreparedEvent> {
    @Override
    public void handleEvent(FitRuntimePreparedEvent event) {
        System.out.println(event.duration());
    }
}

定义一个EventHandler类处理Started类事件,可通过runtime()函数获取运行时环境,也可通过duration()函数获取 FIT 运行时环境启动的时长,示例如下:

@Component
public class MyEventHandler implements EventHandler<FitRuntimeStartedEvent> {
    @Override
    public void handleEvent(FitRuntimeStartedEvent event) {
        System.out.println(event.duration());
    }
}

定义一个EventHandler类处理Failed类事件,可通过runtime()函数获取运行时环境,也可通过cause()函数获取 FIT 运行时环境启动失败的原因,示例如下:

@Component
public class MyEventHandler implements EventHandler<FitRuntimeFailedEvent> {
    @Override
    public void handleEvent(FitRuntimeFailedEvent event) {
        System.out.println(event.cause().getMessage());
    }
}

12.3 Validation 插件

FIT 框架提供了基于 Hibernate Validator 的数据校验功能,支持对方法参数进行自动校验。

  • fit-validation-hibernate-javax:基于 javax.validation 规范,适用于传统 Java EE 环境
  • fit-validation-hibernate-jakarta:基于 jakarta.validation 规范,适用于 Jakarta EE 环境

约束由约束注解和约束验证实现的组合定义。约束注解可以应用于类、方法、字段或其他约束注解(在组合的情况下)。

12.3.1 依赖

当前 FIT 实现中同时存在两套 Validation 插件,分别对应 javax.validation 和 jakarta.validation 标准。 两套校验插件同时运行,通过责任链的方式校验 FIT 程序中需要被校验的参数。

用户可根据自己的开发需要,自行选择对应的标准依赖:

传统 Java EE 环境(javax):

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

Jakarta EE 环境:

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.1.1</version>
</dependency>

12.3.2 注解用法

基本用法

  1. 类级别校验:在类上添加 @Validated 注解,框架会自动对该类的所有方法参数进行校验。
@Component
@Validated
public class UserController {
    public void createUser(@NotBlank(message = "用户名不能为空") String username,
            @Min(value = 18, message = "年龄必须大于等于 18") Integer age) {
        // 业务逻辑
    }
}
  1. 复杂对象校验:对于复杂对象,需要在参数前添加 @Valid 注解,并在对象内部字段上添加校验注解。
public class UserDto {
    @NotBlank(message = "名称不能为空")
    private String name;

    @Min(value = 0, message = "年龄必须大于等于 0")
    @Max(value = 150, message = "年龄必须小于等于 150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Pattern(regexp = "^[a-zA-Z]+$", message = "代码只能包含字母")
    private String code;

    // getter/setter...
}
@PostMapping("/user")
public void createUser(@RequestBody @Valid UserDto userDto) {
    // 业务逻辑
}

泛型类型校验

  1. 泛型类型参数校验:方法的某个入参是泛型类型,如 Collection、Map 的参数类型,需要在校验对象前添加 @Valid 注解(按照 Hibernate Validator reference 推荐,但您仍然可以以 @Valid List 这样的方式添加注解,但不能以 @Valid List<List> 的方式添加注解),否则添加的约束规则不生效,并在泛型内部类型对需要校验的字段添加所需的校验注解。
// List 类型参数校验
public void createUsers(List<@Valid UserDto> users) {
    // 会对 List 中每个 UserDto 对象进行校验
}

// Map 类型参数校验
public void processUserData(Map<String, @Valid UserDto> userMap) {
    // 会对 Map 中每个 UserDto 值进行校验
}

// Set 类型参数校验
public void updateUsers(Set<@Valid UserDto> users) {
    // 会对 Set 中每个 UserDto 对象进行校验
}

嵌套对象校验

  1. 嵌套校验支持:FIT 支持对嵌套校验,即当泛型类型中内嵌泛型类型、非基本数据类型的字段中包含非基本数据类型时,需要在嵌套字段上添加 @Valid 注解。
public class CompanyDto {
    @NotBlank(message = "公司名称不能为空")
    private String name;

    @Min(value = 0, message = "员工数量不能为负数")
    private Integer employeeCount;

    // 嵌套单个对象校验
    @Valid
    @NotNull(message = "经理信息不能为空")
    private UserDto manager;

    // 嵌套集合对象校验
    @Valid
    @Size(min = 1, message = "至少需要一个员工")
    private List<UserDto> employees;

    // 嵌套 Map 对象校验
    @Valid
    private Map<String, DepartmentDto> departments;

    // getter/setter...
}

public class DepartmentDto {
    @NotBlank(message = "部门名称不能为空")
    private String departmentName;

    @Valid
    private List<UserDto> members;

    // getter/setter...
}

// 使用示例
public void createCompany(@Valid CompanyDto company) {
    // 会递归校验 company 及其所有嵌套对象
}

// 复杂嵌套泛型校验
public void processComplexData(Map<String, List<@Valid CompanyDto>> companyGroups) {
    // 会校验 Map 中每个 List,以及 List 中每个 CompanyDto 及其嵌套对象
}
  1. 校验失败处理:方法被调用时,如果传入的实际参数与约束规则不符,会直接抛出 ConstraintViolationException,表明参数校验失败。

支持的校验注解

框架支持完整的 Bean Validation 注解集合:

空值校验

  • @NotNull:值不能为 null
  • @NotEmpty:值不能为 null 且不能为空(适用于字符串、集合、数组、键值对)
  • @NotBlank:字符串不能为 null、空字符串或只包含空白字符
  • @Null:值必须为 null

布尔值校验

  • @AssertTrue:值必须为 true
  • @AssertFalse:值必须为 false

数值校验

  • @Min(value):数值必须大于等于指定值
  • @Max(value):数值必须小于等于指定值
  • @DecimalMin(value):小数值必须大于等于指定值
  • @DecimalMax(value):小数值必须小于等于指定值
  • @Positive:数值必须为正数(大于 0)
  • @PositiveOrZero:数值必须为正数或零(大于等于 0)
  • @Negative:数值必须为负数(小于 0)
  • @NegativeOrZero:数值必须为负数或零(小于等于 0)
  • @Digits(integer, fraction):数字格式校验,integer 指定整数位数,fraction 指定小数位数

大小校验

  • @Size(min, max):字符串、集合、数组、键值对的大小必须在指定范围内

时间校验

  • @Past:日期必须是过去的时间
  • @PastOrPresent:日期必须是过去或现在的时间
  • @Future:日期必须是未来的时间
  • @FutureOrPresent:日期必须是未来或现在的时间

格式校验

  • @Pattern(regexp):字符串必须匹配指定的正则表达式
  • @Email:字符串必须是有效的邮箱格式

使用示例

public class CompleteValidationDto {
    @NotNull(message = "ID 不能为空")
    private Long id;

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在 2-20 之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Min(value = 18, message = "年龄必须大于等于 18")
    @Max(value = 100, message = "年龄必须小于等于 100")
    private Integer age;

    @DecimalMin(value = "0.0", message = "薪资不能为负数")
    @DecimalMax(value = "999999.99", message = "薪资不能超过 999999.99")
    @Digits(integer = 6, fraction = 2, message = "薪资格式不正确")
    private BigDecimal salary;

    @Past(message = "生日必须是过去的日期")
    private LocalDate birthday;

    @Future(message = "合同到期日必须是未来的日期")
    private LocalDate contractEndDate;

    @AssertTrue(message = "必须同意用户协议")
    private Boolean agreeTerms;

    @Size(min = 1, max = 5, message = "技能数量必须在 1-5 之间")
    @Valid
    private List<SkillDto> skills;

    // getter/setter...
}

分组校验

支持校验分组功能,可以在不同场景下应用不同的校验规则:

public class UserDto {
    // 分组接口定义
    public interface BasicGroup {}

    public interface AdvancedGroup {}

    public interface StudentGroup {}

    public interface TeacherGroup {}

    @NotBlank(message = "名称不能为空")
    private String name;

    @Max(value = 200, message = "高级组年龄必须小于等于 200", groups = AdvancedGroup.class)
    private Integer age;

    // getter/setter...
}
@Component
@Validated(UserDto.AdvancedGroup.class)
public class AdvancedUserService {
    public void processAdvancedUser(@Valid UserDto user) {
        // 只会应用 AdvancedGroup 分组的校验规则
    }
}

12.3.3 校验原理

核心组件

  1. ValidationHandler:校验入口类,通过 AOP 方式拦截带有 @Validated 注解的类或方法,自动触发校验逻辑。
  2. Hibernate Validator:底层使用 Hibernate Validator 作为校验引擎。
  3. ConstraintViolationException:校验失败时抛出的异常,包含详细的错误信息。

校验流程

  1. 当调用带有 @Validated 注解的类的方法时,ValidationHandler 会拦截该调用
  2. 检查方法参数是否包含校验注解
  3. 使用 Hibernate Validator 对参数进行校验
  4. 如果校验失败,抛出 ConstraintViolationException 异常
  5. 可通过全局异常处理器捕获并处理校验异常

12.3.4 版本差异

两个插件的主要差异:

特性fit-validation-hibernate-javaxfit-validation-hibernate-jakarta
校验规范javax.validationjakarta.validation
Hibernate 版本6.2.5.Final9.0.1.Final
适用环境传统 Java EEJakarta EE
包名空间javax.validation.*jakarta.validation.*

选择建议:

  • 新项目推荐使用 Jakarta 版本
  • 现有项目根据依赖情况选择对应版本
  • 两个版本功能完全一致,仅包名空间不同

12.3.5 校验的国际化

与校验扩展一样,需要手动处理报错信息中的内容,以完成国际化功能