问题描述
公司最近出了个需求,是i18n国际化文件需要在springboot生成的jar包外生效.集成i18n国际化网上的文章很多就不在赘述了.但是一直无法在jar包外生效.因为每次都要替换jar包里面的文件比较麻烦.从部署程序的需求上来说,倒是比较合理.所以记录一下解决过程.
不生效的原因
我们通过配置文件中的key"spring.messages.basename",找到对应的使用类ResourceBundleCondition
我们看到了,问题就出现在最下面的红框classpath*了,因为用的是classpath,而不是filepath,所以限制了只能在jar包里面被找到.
解决办法
既然原因找到了,那就要考虑如何解决了.
我开发的时候先是使用i18n加强功能基础上开发的,之前的一些i18n加强功能已经有其他文章介绍了,就不摘抄了(文章后面会有完成的类源码,包含了此部分)
加强i18n功能.文章链接: https://segmentfault.com/a/1190000014538512
上面链接里面写了一个MessageResourceExtension类(下面会用到),继承了ResourceBundleMessageSource.
跟着代码断点查找发现用了org.springframework.context.MessageSource.getMessage(String code, @Nullable Object[] args, Locale locale)
中间的断点不进行一步一步追踪了.
最后发现是在java.util.ResourceBundle#loadBundle中加载的i18n文件
代码如下:
private static ResourceBundle loadBundle(CacheKey cacheKey,
List<String> formats,
Control control,
boolean reload) {
// Here we actually load the bundle in the order of formats
// specified by the getFormats() value.
Locale targetLocale = cacheKey.getLocale();
ResourceBundle bundle = null;
int size = formats.size();
for (int i = 0; i < size; i++) {
String format = formats.get(i);
try {
bundle = control.newBundle(cacheKey.getName(), targetLocale, format,
cacheKey.getLoader(), reload);
} catch (LinkageError error) {
// We need to handle the LinkageError case due to
// inconsistent case-sensitivity in ClassLoader.
// See 6572242 for details.
cacheKey.setCause(error);
} catch (Exception cause) {
cacheKey.setCause(cause);
}
if (bundle != null) {
// Set the format in the cache key so that it can be
// used when calling needsReload later.
cacheKey.setFormat(format);
bundle.name = cacheKey.getName();
bundle.locale = targetLocale;
// Bundle provider might reuse instances. So we should make
// sure to clear the expired flag here.
bundle.expired = false;
break;
}
}
return bundle;
}
不难发现control.newBundle方法才是关键,spring的org.springframework.context.support.ResourceBundleMessageSource.MessageSourceControl就是继承了此类,但是,我们需要自己做国际化路径文件的配置,怎么才能把自己的类注入呢?
向上翻找发现org.springframework.context.support.ResourceBundleMessageSource#getResourceBundle(String basename, Locale locale)如图:
所以我们首先要写一个容纳自定义的国际化文件路径的类,参照spring的org.springframework.context.support.ResourceBundleMessageSource.MessageSourceControl就好,之后自己的改造一下,以下为我自己的代码:
private class I18nMessageSourceControl extends ResourceBundle.Control {
@Override
@Nullable
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
// Special handling of default encoding
if (format.equals("java.properties")) {
String bundleName = toBundleName(baseName, locale);
final String resourceName = toResourceName(bundleName, "properties");
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream inputStream;
try {
inputStream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
InputStream is = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
is = connection.getInputStream();
// 如果jar包部署存在同级目录下国际化文件,优先读取同级目录文件
if (url.getProtocol().equalsIgnoreCase("jar")) {
is = getBufferedInputStream(resourceName, is);
}
}
}
}
else {
is = classLoader.getResourceAsStream(resourceName);
// 如果jar包部署存在同级目录下国际化文件,优先读取同级目录文件
URL url = classLoader.getResource(resourceName);
if (url.getProtocol().equalsIgnoreCase("jar")) {
is = getBufferedInputStream(resourceName, is);
}
}
return is;
});
}
catch (PrivilegedActionException ex) {
throw (IOException) ex.getException();