bigywater.tech
Java · 352

ResourceBundle:国际化资源的查找与回退

国际化消息文件的查找规则有一些反直觉的地方:默认 Locale 会混进回退链,编码有静默回退,缓存永不过期。这篇用临时目录里的真实文件逐项验证。

fr 而不是 base
默认 Locale = fr 时,请求没有对应文件的 en 或 ja 得到的 bundle
0xE9 在末尾
properties 文件最后一个字节是非法 UTF-8 时 getBundle 抛 MissingResourceException(其它位置的非法字节则静默回退 Latin-1)
-2
默认 Control.getTimeToLive,即 bundle 缓存永不过期

候选 Locale 列表

实测 · 本机执行输出
  [candidates]
        zh-CN        → [zh_CN_#Hans, zh__#Hans, zh_CN, zh, (root)]
        zh-Hans-CN   → [zh_CN_#Hans, zh__#Hans, zh_CN, zh, (root)]
        zh-Hant-TW   → [zh_TW_#Hant, zh__#Hant, zh_TW, zh, (root)]
        fr-CA        → [fr_CA, fr, (root)]
        en           → [en, (root)]
        sr-Latn-RS   → [sr_RS_#Latn, sr__#Latn, sr_RS, sr, (root)]
        und          → [(root)]
    Control.getFallbackLocale(…, ja) 在默认 Locale = en_US 时 = en_US

回退与父级补缺

实测 · 本机执行输出
  [fallback]
    默认 Locale = en_US
        getBundle(Msg, ja    ) who → base  [bundle locale=(root)]
        getBundle(Msg, fr-CA ) who → français  [bundle locale=fr]
        getBundle(Msg, zh-CN ) who → 简体中文  [bundle locale=zh_CN]
        getBundle(Msg, zh-TW ) who → 中文(zh)  [bundle locale=zh]
        getBundle(Msg, de-AT ) who → Deutsch  [bundle locale=de]
        getBundle(Msg, en    ) who → base  [bundle locale=(root)]
    默认 Locale = fr
        getBundle(Msg, ja    ) who → français  [bundle locale=fr]
        getBundle(Msg, fr-CA ) who → français  [bundle locale=fr]
        getBundle(Msg, zh-CN ) who → 简体中文  [bundle locale=zh_CN]
        getBundle(Msg, zh-TW ) who → 中文(zh)  [bundle locale=zh]
        getBundle(Msg, de-AT ) who → Deutsch  [bundle locale=de]
        getBundle(Msg, en    ) who → français  [bundle locale=fr]
    默认 Locale = de
        getBundle(Msg, ja    ) who → Deutsch  [bundle locale=de]
        getBundle(Msg, fr-CA ) who → français  [bundle locale=fr]
        getBundle(Msg, zh-CN ) who → 简体中文  [bundle locale=zh_CN]
        getBundle(Msg, zh-TW ) who → 中文(zh)  [bundle locale=zh]
        getBundle(Msg, de-AT ) who → Deutsch  [bundle locale=de]
        getBundle(Msg, en    ) who → Deutsch  [bundle locale=de]
    Control.NO_FALLBACK,默认 Locale = fr,请求 ja → base
    zh_CN 缺 onlyzh 时从父级 zh 取:只在 zh;缺 onlybase 时从根取:only in base
    zh_CN.getKeys() = [onlybase, onlyzh, who];keySet() = [onlybase, onlyzh, who];containsKey(onlyfr) = false
    缺失键:MissingResourceException: Can't find resource for bundle java.util.PropertyResourceBundle, key nope(className=java.util.PropertyResourceBundle,key=nope)
    缺失 bundle:Can't find bundle for base name NoSuchBundle, locale en_US

properties 的编码

实测 · 本机执行输出
  [encoding]
    UTF-8 文件:name = 中文 é ü 😀;escaped(\uXXXX)= 中文
    ISO-8859-1 字节(含 0xE9、0xEF,不是合法 UTF-8):name = café naïve(读取没有报错,按 Latin-1 回退)
    同一个文件里既有合法的 UTF-8 中文又有一个 0xE9(后面还有内容):b = 中文,c = café(整个文件按 ISO-8859-1 重读,中文变成乱码)
    0xE9 恰好是文件的最后一个字节:MissingResourceException: Can't find bundle for base name N, locale ;cause = java.nio.charset.MalformedInputException: Input length = 1
    file.encoding / native.encoding / stdout.encoding = UTF-8 / UTF-8 / UTF-8;PropertyResourceBundle 读取不依赖它们
    手工用 Reader 构造 PropertyResourceBundle(ISO_8859_1):café naïve
    对照:java.util.Properties.load(InputStream) 读同一个 UTF-8 文件:name = 中文 é ü 😀(按 Latin-1 解码)

类 bundle 对 properties

实测 · 本机执行输出
  [class]
    同时有 Pri_en.class 与 Pri_en.properties 时 getBundle 得到 Pri_en,greet = hello-from-CLASS,onlyprops 存在吗? false
    非字符串值:getObject(num) = 42(Integer);getStringArray(arr) = [a, b]
    getString(num) → ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
    clearCache 后 FORMAT_PROPERTIES 只找 properties:hello-from-PROPERTIES
    clearCache 后 FORMAT_CLASS 只找 class:hello-from-CLASS

缓存

实测 · 本机执行输出
  [cache]
    修改文件后再 getBundle:同一个对象? true;v = 1(缓存里仍是旧值)
    clearCache(loader) 之后:同一个对象? false;v = 2
    另一个类加载器(同一个目录)加载:同一个对象? false;v = 2
    默认 Control.getTimeToLive = -2(TTL_NO_EXPIRATION_CONTROL = -2,TTL_DONT_CACHE = -1)
    TTL=300 ms 的自定义 Control:立刻再取 v = 1
    过了 400 ms(过期且 needsReload 为 true)再取 v = 2
    缓存命中的 getBundle + getString:239 ns;只 getString:5 ns(重复一次 193 ns,自写计时,sink=true)

分步演示

ResourceBundle
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,未命名模块(类路径 + URLClassLoader),测试文件写在临时目录
    • 没有验证:命名模块(module-info 与 ResourceBundleProvider)里的查找行为、JDK 17 / 27 的差异、非字符串 key 的 properties、Spring MessageSource 等框架的封装、0xE9 在文件末尾时抛异常的具体原因(只看到异常与 cause,没有读源码)
    京ICP备2026067502号-1京公网安备11010502063487号