spring boot 使用ReloadableResourceBundleMessageSource的坑

根據網上的例子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'

雖然網上也有很多資料但是找到沒找到問題的關鍵。。

  • 爲什麼會出現上述問題呢? 下面我們源碼分析一波
  1. 首先定位問題在ReloadableResourceBundleMessageSource 的類
  2. 在ReloadableResourceBundleMessageSource 的配置我們只配置了basename,所以問題接着就定位在basename
  3. 從問題拋出的異常點入手,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")是有區別的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章