1. 概述

在 Java 中,异常(Exception)用于表示程序运行过程中出现的错误。除了抛出异常之外,我们通常还会附带一条消息来提供更多上下文信息。

本文将通过重写 getLocalizedMessage 方法,实现异常消息的中英文本地化支持。

2. 资源包(ResourceBundle)

我们需要一种机制,可以通过 messageKeyLocale 来查找对应的本地化消息内容。为此,我们可以封装一个简单的工具类来访问 ResourceBundle,以获取英文和法文的消息翻译:

public class Messages {

    public static String getMessageForLocale(String messageKey, Locale locale) {
        return ResourceBundle.getBundle("messages", locale)
          .getString(messageKey);
    }

}

这个 Messages 类会使用 ResourceBundle 加载位于 classpath 根目录下的属性文件。我们准备了两个文件:

# messages.properties
message.exception = I am an exception.
# messages_fr.properties
message.exception = Je suis une exception.

3. 本地化异常类

我们的自定义异常类将根据默认的 Locale 来决定使用哪种语言的消息。可以通过 Locale.getDefault() 获取当前系统默认的语言环境。

⚠️ 如果是服务端应用,应该从 HTTP 请求头中提取客户端的语言偏好,而不是依赖系统的默认 Locale。

为此,我们提供一个构造函数来接收 Locale 参数。接下来创建一个继承自 Exception 的本地化异常类,并重写 getLocalizedMessage() 方法:

public class LocalizedException extends Exception {

    private final String messageKey;
    private final Locale locale;

    public LocalizedException(String messageKey) {
        this(messageKey, Locale.getDefault());
    }

    public LocalizedException(String messageKey, Locale locale) {
        this.messageKey = messageKey;
        this.locale = locale;
    }

    @Override
    public String getLocalizedMessage() {
        return Messages.getMessageForLocale(messageKey, locale);
    }
}

4. 整合测试

我们可以通过单元测试验证不同语言环境下的异常消息是否正确加载。下面分别是针对英文和法文的测试用例:

@Test
public void givenUsEnglishProvidedLocale_whenLocalizingMessage_thenMessageComesFromDefaultMessage() {
    LocalizedException localizedException = new LocalizedException("message.exception", Locale.US);
    String usEnglishLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(usEnglishLocalizedExceptionMessage).isEqualTo("I am an exception.");
}

@Test
public void givenFranceFrenchProvidedLocale_whenLocalizingMessage_thenMessageComesFromFrenchTranslationMessages() {
    LocalizedException localizedException = new LocalizedException("message.exception", Locale.FRANCE);
    String franceFrenchLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(franceFrenchLocalizedExceptionMessage).isEqualTo("Je suis une exception.");
}

同时,我们也测试使用默认 Locale 的情况:

@Test
public void givenUsEnglishDefaultLocale_whenLocalizingMessage_thenMessageComesFromDefaultMessages() {
    Locale.setDefault(Locale.US);

    LocalizedException localizedException = new LocalizedException("message.exception");
    String usEnglishLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(usEnglishLocalizedExceptionMessage).isEqualTo("I am an exception.");
}

@Test
public void givenFranceFrenchDefaultLocale_whenLocalizingMessage_thenMessageComesFromFrenchTranslationMessages() {
    Locale.setDefault(Locale.FRANCE);

    LocalizedException localizedException = new LocalizedException("message.exception");
    String franceFrenchLocalizedExceptionMessage = localizedException.getLocalizedMessage();

    assertThat(franceFrenchLocalizedExceptionMessage).isEqualTo("Je suis une exception.");
}

5. 注意事项

5.1. 日志记录中的异常处理

✅ 不同的日志框架对异常消息的获取方式不一样:

  • Log4J、Log4J2 和 Logback 默认调用的是 getMessage()
  • java.util.logging 则调用的是 getLocalizedMessage()

因此,如果你希望日志输出也能体现本地化效果,建议重写 getMessage() 方法并让它返回 getLocalizedMessage() 的结果,这样可以避免因为日志框架不同导致本地化失效的问题。

5.2. 服务端多请求场景

⚠️ 在服务端应用中,如果多个请求共享同一个 JVM 实例,修改 Locale.setDefault() 会影响所有请求的语言环境。

所以推荐的做法是:不要依赖全局默认 Locale,而是在异常构造时显式传入 Locale 参数。

6. 总结

实现 Java 异常消息本地化其实很简单:

  1. 创建多个语言版本的 .properties 文件作为资源包
  2. 自定义异常类并重写 getLocalizedMessage() 方法
  3. 可选地重写 getMessage() 以适配日志框架的行为

完整示例代码可参考 GitHub 项目地址


原始标题:Localizing Exception Messages in Java