spring boot 使用ReloadableResourceBundleMessageSource的坑

发布时间:2025-12-09 11:50:53 浏览次数:1

根据网上的例子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和路径加载的方式

正确配置

@Configurationpublic 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")是有区别的
refreshlock
需要做网站?需要网络推广?欢迎咨询客户经理 13272073477