缘起
最近一段时间在开发 IDEA 插件, UI 界面需要使用到国际化配置, 于是就看了看 IDEA 是怎么实现的, 发现很简单, 正好能用到框架开发上.
打算为每个 atom-kernel
模块配置一个国际化配置, 同时将错误信息配置化. 因为是框架底层的组件, 如果使用 Spring Boot 的 i18n 实现就太重了, 因此需要一种超轻量级的实现方式.
IDEA 中如何实现 i18n
IDEA 使用 ResourceBundle
这个类实现了 i18n, 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
public abstract class AbstractBundle { private static final Logger LOG = Logger.getInstance("#com.intellij.AbstractBundle"); private Reference<ResourceBundle> myBundle; @NonNls private final String myPathToBundle;
protected AbstractBundle(@NonNls @NotNull String pathToBundle) { myPathToBundle = pathToBundle; }
@NotNull public String getMessage(@NotNull String key, @NotNull Object... params) { return CommonBundle.message(getBundle(), key, params); }
private ResourceBundle getBundle() { ResourceBundle bundle = com.intellij.reference.SoftReference.dereference(myBundle); if (bundle == null) { bundle = getResourceBundle(myPathToBundle, getClass().getClassLoader()); myBundle = new SoftReference<>(bundle); } return bundle; }
private static final Map<ClassLoader, Map<String, ResourceBundle>> ourCache = ConcurrentFactoryMap.createWeakMap(k -> ContainerUtil.createConcurrentSoftValueMap());
public static ResourceBundle getResourceBundle(@NotNull String pathToBundle, @NotNull ClassLoader loader) { Map<String, ResourceBundle> map = ourCache.get(loader); ResourceBundle result = map.get(pathToBundle); if (result == null) { try { ResourceBundle.Control control = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES); result = ResourceBundle.getBundle(pathToBundle, Locale.getDefault(), loader, control); } catch (MissingResourceException e) { LOG.info("Cannot load resource bundle from *.properties file, falling back to slow class loading: " + pathToBundle); ResourceBundle.clearCache(loader); result = ResourceBundle.getBundle(pathToBundle, Locale.getDefault(), loader); } map.put(pathToBundle, result); } return result; } }
|
实现自己的 Bundle 类:
1 2 3 4 5 6 7 8 9 10 11 12
| public class IdeBundle extends AbstractBundle { public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) { return INSTANCE.getMessage(key, params); }
public static final String BUNDLE = "messages.IdeBundle"; private static final IdeBundle INSTANCE = new IdeBundle();
private IdeBundle() { super(BUNDLE); } }
|
添加配置文件:
还需要在 classpath 中添加一个 messages 目录, 然后添加 IdeBundle.properties
配置文件, 或者可以直接使用 IDEA 新增资源包, 需要注意的是, messages.IdeBundle
表示在 messages
目录下的 IdeBundle.properties
文件, 一定不能错:
1 2
| error.malformed.url=Malformed url: {0} browsers.explorer=Internet Explorer
|
使用方式:
1 2 3 4
| # 有占位符的 IdeBundle.message("error.malformed.url", "xxx") # 无占位符 IdeBundle.message("browsers.explorer")
|
从上面可以看出, 直接 JDK 自带的 ResourceBundle
类实现了国际化和占位符替换的功能, 刚好符合我的要求, 因此打算使用这种方式来实现一下.
实现逻辑
我需要的功能:
- 国际化;
- 占位符替换;
IDEA 的 AbstractBundle
有很多我不需要的功能, 做了一些简单的修改后完全符合我的要求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
| @Slf4j public abstract class AbstractBundle { private static final Map<ClassLoader, Map<String, ResourceBundle>> CACHE = ConcurrentFactoryMap.createWeakMap(k -> new ConcurrentSoftValueHashMap<>()); @NonNls private final String myPathToBundle; private Reference<ResourceBundle> myBundle;
@Contract(pure = true) protected AbstractBundle(@NonNls @NotNull String pathToBundle) { this.myPathToBundle = pathToBundle; }
@NotNull public Supplier<String> getLazyMessage(@NotNull String key, Object... params) { return () -> this.getMessage(key, params); }
@NotNull public String getMessage(@NotNull String key, Object... params) { return message(this.getResourceBundle(), key, params); }
@Nls @NotNull public static String message(@NotNull ResourceBundle bundle, @NotNull String key, Object... params) { return messageOrDefault(bundle, key, null, params); }
public ResourceBundle getResourceBundle() { ResourceBundle bundle = SoftReference.dereference(this.myBundle); if (bundle == null) { bundle = this.getResourceBundle(this.myPathToBundle, this.getClass().getClassLoader()); this.myBundle = new SoftReference<>(bundle); } return bundle; }
@NotNull public ResourceBundle getResourceBundle(@NotNull String pathToBundle, @NotNull ClassLoader loader) { Map<String, ResourceBundle> map = CACHE.get(loader); ResourceBundle result = map.get(pathToBundle); if (result == null) { try { ResourceBundle.Control control = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES); result = this.findBundle(pathToBundle, loader, control); } catch (MissingResourceException e) { log.info("无法从 *.properties 文件中加载资源包,降级为慢的类加载: " + pathToBundle); ResourceBundle.clearCache(loader); result = ResourceBundle.getBundle(pathToBundle, Locale.getDefault(), loader); } map.put(pathToBundle, result); } return result; }
protected ResourceBundle findBundle(@NotNull String pathToBundle, @NotNull ClassLoader loader, @NotNull ResourceBundle.Control control) { return ResourceBundle.getBundle(pathToBundle, Locale.getDefault(), loader, control); }
public static String messageOrDefault(@Nullable ResourceBundle bundle, @NotNull String key, @Nullable String defaultValue, @NotNull Object... params) { if (bundle != null) { String value; try { value = bundle.getString(key); } catch (MissingResourceException e) { value = useDefaultValue(bundle, key, defaultValue); } return postprocessValue(bundle, value, params); }
return defaultValue; }
@NotNull static String useDefaultValue(ResourceBundle bundle, @NotNull String key, @Nullable String defaultValue) { if (defaultValue != null) { return defaultValue; }
log.error("在资源包中 {} 未找到键: [{}]", bundle.getBaseBundleName(), key); return StringPool.NULL_STRING; }
static String postprocessValue(@NotNull ResourceBundle bundle, @NotNull String value, @NotNull Object @NotNull [] params) { if (params.length > 0 && value.indexOf('{') >= 0) { if (value.contains("{0")) { Locale locale = bundle.getLocale(); try { MessageFormat format = locale != null ? new MessageFormat(value, locale) : new MessageFormat(value); OrdinalFormat.apply(format); value = format.format(params); } catch (IllegalArgumentException e) { value = "!format 错误: `" + value + "`!"; } } else { value = StrFormatter.format(value, params); } }
return value; } }
|
创建绑定类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public final class CoreBundle extends DynamicBundle { @NonNls private static final String BUNDLE = "i18n.CoreBundle"; private static final CoreBundle INSTANCE = new CoreBundle();
@Contract(pure = true) private CoreBundle() { super(BUNDLE); }
@NotNull public static String message(@NotNull String key, Object... params) { return INSTANCE.getMessage(key, params); }
public static @NotNull Supplier<String> messagePointer(@NotNull String key, Object... params) { return INSTANCE.getLazyMessage(key, params); } }
|
上面的代码基本上是固定的写法, 只需要修改 BUNDLE
即可, 然后创建国际化配置文件:
测试一下:
1 2 3 4 5 6 7 8 9 10 11 12
| @Slf4j class CoreBundleTest { @Test void test_1() { log.info("{}", CoreBundle.message("code.param.verify.error")); log.info("{}", CoreBundle.message("aaa")); log.info("{}", CoreBundle.messagePointer("code.param.verify.error", "aaaaa").get()); } }
|
输出:
1 2 3 4
| [main] INFO io.github.atom.kernel.core.CoreBundleTest - 参数校验失败: [{}] [main] ERROR io.github.atom.kernel.core.bundle.AbstractBundle - 在资源包 [i18n.CoreBundle] 未找到键: [aaa] [main] INFO io.github.atom.kernel.core.CoreBundleTest - N/A [main] INFO io.github.atom.kernel.core.CoreBundleTest - 参数校验失败: [aaaaa]
|