根據網上的例子MessageSource 配置如下
@Bean(name = "messageSource") public ReloadableResourceBundleMessageSource messageSource() { ReloadableResourceBundleMessageSource messageBundle = new ReloadableResourceBundleMessageSource(); messageBundle.setBasename("messages/messages"); messageBundle.setDefaultEncoding("UTF-8"); return messageBundle; }
接着直接使用:
圖片.png
代碼調用:
@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。
整個繼承圖如下:
圖片.png
所以我們重點關注的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; }
很顯然出現之前的問題爲basename的路徑配置錯誤,資源文件在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")是有區別的