根据网上的例子MessageSource 配置如下
@Bean(name = "messageSource")
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageBundle = new ReloadableResourceBundleMessageSource();
messageBundle.setBasename("messages/messages");
messageBundle.setDefaultEncoding("UTF-8");
return messageBundle;
}
接着直接使用:

代码调用:
@Autowired
@Qualifier("messageSource")
private MessageSource messageSource;
//下面在方法种使用
messageSource.getMessage("test", new Object[], SIMPLIFIED_CHINESE);
- 但是在使用过程中我发现出现异常如下:
No message found under "test" for locale 'zh_CN'
虽然网上也有很多资料但是找到没找到问题的关键。。
- 为什么会出现上述问题呢? 下面我们源码分析一波
- 首先定位问题在ReloadableResourceBundleMessageSource 的类
- 在ReloadableResourceBundleMessageSource 的配置我们只配置了basename,所以问题接着就定位在basename
- 从问题抛出的异常点入手,messageSource.getMessage,messageSource是一个接口,真正起作用的是实现类AbstractMessageSource。
整个继承图如下:

所以我们重点关注的AbstractMessageSource的getMessage方法。以其中一个为例分析
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes != null) {
String[] var4 = codes;
int var5 = codes.length;
for(int var6 = 0; var6 < var5; ++var6) {
String code = var4[var6];
//这里去取资源文件中的数据,我们继续跟踪如下
String message = this.getMessageInternal(code, resolvable.getArguments(), locale);
if (message != null) {
return message;
}
}
}
//这里如果没有从配置文件种找到,会走默认,但是我们没有提供默认,所以抛出异常
String defaultMessage = this.getDefaultMessage(resolvable, locale);
if (defaultMessage != null) {
return defaultMessage;
} else {
//这里就是我们异常的触发点
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : null, locale);
}
}
getMessageInternal方法:
protected String getMessageInternal(String code, Object[] args, Locale locale) {
//省略。。。
//如果使用模版,使用下面方法
if (!this.isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String message = this.resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
} else {
//否则如下
argsToUse = this.resolveArguments(args, locale);
MessageFormat messageFormat = this.resolveCode(code, locale);
if (messageFormat != null) {
synchronized(messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
Properties commonMessages = this.getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return this.formatMessage(commonMessage, args, locale);
}
}
//如果还没找到,调用父类放入资源查找
return this.getMessageFromParent(code, argsToUse, locale);
}
}
通过上面的方法很明显resolveCodeWithoutArguments和resolveCode方法就是核心方法,而这两个方法最终也归结为resolveCode:
protected String resolveCodeWithoutArguments(String code, Locale locale) {
MessageFormat messageFormat = this.resolveCode(code, locale);
if (messageFormat != null) {
synchronized(messageFormat) {
return messageFormat.format(new Object[0]);
}
} else {
return null;
}
}
//很明显这个方法没有实现,具体的实现方式,为我们最初定义的ReloadableResourceBundleMessageSource去实现的,回到ReloadableResourceBundleMessageSource类中查看
protected abstract MessageFormat resolveCode(String var1, Locale var2);
回过头我们开始分析我们注入spring的ReloadableResourceBundleMessageSource类
public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements ResourceLoaderAware {
//下面两个属性标示该类支持xml和properties两种资源文件
private static final String PROPERTIES_SUFFIX = ".properties";
private static final String XML_SUFFIX = ".xml";
//编码类型
private Properties fileEncodings;
//默认自动刷新,这也是我们选择 ReloadableResourceBundleMessageSource 而不是用ResourceBundleMessageSource的一个原因
private boolean concurrentRefresh = true;
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
//默认的资源加载器(这里是我们出现问题的关键)
private ResourceLoader resourceLoader = new DefaultResourceLoader();
//缓存我们的文件名
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap();
//缓存资源PropertiesHolder(为内部类,每一个对象都应对的一个资源文件)
private final ConcurrentMap<String, ReloadableResourceBundleMessageSource.PropertiesHolder> cachedProperties = new ConcurrentHashMap();
private final ConcurrentMap<Locale, ReloadableResourceBundleMessageSource.PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap();
public ReloadableResourceBundleMessageSource() {
}
}
//其他方法暂略。。。
- 接着分析它实现了AbstractMessageSource抽象类中的resolveCode方法如下:
protected MessageFormat resolveCode(String code, Locale locale) {
//刷新
if (this.getCacheMillis() < 0L) {
ReloadableResourceBundleMessageSource.PropertiesHolder propHolder = this.getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
} else {
Iterator var10 = this.getBasenameSet().iterator();
//下面两个循环是通过key查找资源。从配置的的多个basename中的多个文件中查找文件
while(var10.hasNext()) {
String basename = (String)var10.next();
List<String> filenames = this.calculateAllFilenames(basename, locale);
Iterator var6 = filenames.iterator();
//第二层循环为路径下的资源文件,还记得前面说PropertiesHolder 其实对应每个国际化的资源文件
while(var6.hasNext()) {
String filename = (String)var6.next();
//this.getProperties(filename);这个方法获取propHolder ,我们继续跟踪这个方法
ReloadableResourceBundleMessageSource.PropertiesHolder propHolder = this.getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
}
- getProperties方法
protected ReloadableResourceBundleMessageSource.PropertiesHolder getProperties(String filename) {
//这一步先从之前缓存中取,第一次没有缓存,所以直接跳过看else中的代码
ReloadableResourceBundleMessageSource.PropertiesHolder propHolder = (ReloadableResourceBundleMessageSource.PropertiesHolder)this.cachedProperties.get(filename);
long originalTimestamp = -2L;
ReloadableResourceBundleMessageSource.PropertiesHolder existingHolder;
if (propHolder != null) {
originalTimestamp = propHolder.getRefreshTimestamp();
if (originalTimestamp == -1L || originalTimestamp > System.currentTimeMillis() - this.getCacheMillis()) {
return propHolder;
}
//新创建PropertiesHolder接着放到缓存
} else {
propHolder = new ReloadableResourceBundleMessageSource.PropertiesHolder();
existingHolder = (ReloadableResourceBundleMessageSource.PropertiesHolder)this.cachedProperties.putIfAbsent(filename, propHolder);
if (existingHolder != null) {
propHolder = existingHolder;
}
}
if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0L) {
if (!propHolder.refreshLock.tryLock()) {
return propHolder;
}
} else {
propHolder.refreshLock.lock();
}
ReloadableResourceBundleMessageSource.PropertiesHolder var6;
try {
//直接从缓存中取PropertiesHolder,并查看是否过期,过期则重新加载
existingHolder = (ReloadableResourceBundleMessageSource.PropertiesHolder)this.cachedProperties.get(filename);
//默认没有定义两者均为-2 所以直接执行刷新操作refreshProperties
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
var6 = existingHolder;
return var6;
}
//刷新资源,该方法会将资源文件加载到propHolder,继续看它是如何加载的
var6 = this.refreshProperties(filename, propHolder);
} finally {
propHolder.refreshLock.unlock();
}
return var6;
}
- refreshProperties加载资源文件
protected ReloadableResourceBundleMessageSource.PropertiesHolder refreshProperties(String filename, ReloadableResourceBundleMessageSource.PropertiesHolder propHolder) {
long refreshTimestamp = this.getCacheMillis() < 0L ? -1L : System.currentTimeMillis();
//可以看到properties和xml文件均能加载,this.resourceLoader.getResource加载核心类,没有配置使用的为spring默认的DefaultResourceLoader
Resource resource = this.resourceLoader.getResource(filename + ".properties");
if (!resource.exists()) {
resource = this.resourceLoader.getResource(filename + ".xml");
}
//如果资源文件存在,添加时间戳,
if (resource.exists()) {
long fileTimestamp = -1L;
if (this.getCacheMillis() >= 0L) {
try {
fileTimestamp = resource.lastModified();
if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
}
propHolder.setRefreshTimestamp(refreshTimestamp);
return propHolder;
}
} catch (IOException var10) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", var10);
}
fileTimestamp = -1L;
}
}
try {
//根据resource, filename生成Properties属性 创建PropertiesHolder对象(Properties就是java 中常用的配置方式,存有我们的国际化数据)
Properties props = this.loadProperties(resource, filename);
propHolder = new ReloadableResourceBundleMessageSource.PropertiesHolder(props, fileTimestamp);
} catch (IOException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Could not parse properties file [" + resource.getFilename() + "]", var9);
}
propHolder = new ReloadableResourceBundleMessageSource.PropertiesHolder();
}
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");
}
propHolder = new ReloadableResourceBundleMessageSource.PropertiesHolder();
}
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
return propHolder;
}
上面方法重点在于两个方法,其一是是否成功生成resource资源,其二为loadProperties属性是否正确。这两种方法如果均为加载我们的资源文件,也都会生成propHolder,但是会取不到数据,也就是前面的错误:No message found under "test" for locale 'zh_CN'
- 所以分析这两个方法:
1) this.resourceLoader.getResource(filename + ".properties");我们没有配置资源加载器,所以这里其作用的为spring的默认资源加载器DefaultResourceLoader
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
Iterator var2 = this.protocolResolvers.iterator();
Resource resource;
do {
//如果/开头使用路径加载
if (!var2.hasNext()) {
if (location.startsWith("/")) {
return this.getResourceByPath(location);
}
//classpath开头使用类路径加载器
if (location.startsWith("classpath:")) {
return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
}
//最后使用url加载(这里是出现之前的问题的关键)
try {
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException var5) {
return this.getResourceByPath(location);
}
}
ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
resource = protocolResolver.resolve(location, this);
} while(resource == null);
return resource;
}
- 最终利用getClassLoader得到当前的类加载器,以及相对路径。将资源文件加载到内存中,实现方式在ReloadableResourceBundleMessageSource的loadProperties方法中,如下:
protected Properties loadProperties(Resource resource, String filename) throws IOException {
InputStream is = resource.getInputStream();
Properties props = this.newProperties();
Properties var9;
try {
if (resource.getFilename().endsWith(".xml")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.loadFromXml(props, is);
} else {
String encoding = null;
if (this.fileEncodings != null) {
encoding = this.fileEncodings.getProperty(filename);
}
if (encoding == null) {
encoding = this.getDefaultEncoding();
}
if (encoding != null) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, is);
}
}
var9 = props;
} finally {
is.close();
}
return var9;
}
- 如果在往下,想知道spring如何利用路径加载资源的,其实它最终在getInputStream方法调用jdk的方法,如下ClassPathResource类中的getInputStream方法(涉及到jvm的类加载机制)
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
//调用jdk clazz类中的加载方式
is = this.clazz.getResourceAsStream(this.path);
} else if (this.classLoader != null) {
//调用ClassLoader类的getResourceAsStream
is = this.classLoader.getResourceAsStream(this.path);
} else {
//调用系统资源加载
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(this.getDescription() + " cannot be opened because it does not exist");
} else {
return is;
}
}
- 在往下就是jdk如何实现的了,具体实现在class类下getResourceAsStream方法:
Class类的getResourceAsStream
public InputStream getResourceAsStream(String name)查找具有给定名称的资源。查找与给定类相关的资源的规则是通过定义类的 class loader 实现的。此方法委托此对象的类加载器。如果此对象通过引导类加载器加载,则此方法将委托给 ClassLoader.getSystemResourceAsStream(java.lang.String)
//双亲委派机制
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
//如果当前加载器为空,获取资源的方式有两种:Class获取和ClassLoader获取
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
- 多说一点,Class和ClassLoader获取资源,两者的区别
@Test
public void testResouce() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader.getResource("").getPath());
System.out.println(this.getClass().getResource("").getPath());
System.out.println(this.getClass().getResource("/").getPath());
System.out.println(System.getProperty("user.dir"));
}
结果如下:
/C:/work/workspaces/nev-gov-moc/moc-portal/target/test-classes/
/C:/work/workspaces/nev-gov-moc/moc-portal/target/test-classes/com/cmdt/yudo/moc/rt/test/
/C:/work/workspaces/nev-gov-moc/moc-portal/target/test-classes/
C:\work\workspaces\nev-gov-moc\moc-portal
所以两者:
Class.getResource("")获取的是相对于当前类的相对路径
Class.getResource("/")获取的是classpath的根路径
ClassLoader.getResource("")获取的是classpath的根路径
在创建ClassPathResource对象时,我们可以指定是按Class的相对路径获取文件还是按ClassLoader来获取。
在往下的具体实现一言半语也说不清了,具体之后在写
总结一下:很显然出现之前的问题为basename的路径配置问题,在DefaultResourceLoader类中getResource方法中我们可以看出,具体使用那种资源加载和配置文件的basenane有关系,如果以classpath:和/开头的两种加载方式最后使用ClassPathResource类(ClassPathContextResource是ClassPathResource子类)资源文件在resource路径下编译后就是类的住目录,所以这里应该使用classpath:为开头,其他两种分别为url和路径加载的方式
正确配置
@Configuration
public class I18nConfig {
@Bean(name = "messageSource")
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageBundle = new ReloadableResourceBundleMessageSource();
messageBundle.setBasename("classpath:messages/messages");
messageBundle.setDefaultEncoding("UTF-8");
return messageBundle;
}
}
- 注意这里messageBundle.setBasename("classpath:messages/messages");的classpath和使用setBasename("messages/messages")是有区别的,而且我的国际化是写在一个通用的maven模块中引用而来,所以也是导致这个问题的一个原因。
网友评论