我们都知道MessageSource用于国际化,先来看看源码是个啥,其实啥也不是就是一个方法
public interface MessageSource {
/**
* 解析传入的message,如果没有就返回defaultMessage
*/
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
其中看到了MessageSourceResolvable,在我的印象中在SpringMVC中XXXHandler和XXXXResolver可真不少。在这里也可以看下MessageSourceResolvable是个什么玩意
public interface MessageSourceResolvable {
String[] getCodes();
Object[] getArguments();
String getDefaultMessage();
清爽如你所见。。。。说白了就是将参数包装了一下,去除各种null的情况
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
可以看一下默认实现:
public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable {
private final String[] codes;
private final Object[] arguments;
private final String defaultMessage;
public DefaultMessageSourceResolvable(String code) {
this(new String[] {code}, null, null);
}
public DefaultMessageSourceResolvable(String[] codes) {
this(codes, null, null);
}
public DefaultMessageSourceResolvable(String[] codes, String defaultMessage) {
this(codes, null, defaultMessage);
}
public DefaultMessageSourceResolvable(String[] codes, Object[] arguments) {
this(codes, arguments, null);
}
public DefaultMessageSourceResolvable(String[] codes, Object[] arguments, String defaultMessage) {
this.codes = codes;
this.arguments = arguments;
this.defaultMessage = defaultMessage;
}
public DefaultMessageSourceResolvable(MessageSourceResolvable resolvable) {
this(resolvable.getCodes(), resolvable.getArguments(), resolvable.getDefaultMessage());
}
@Override
public String[] getCodes() {
return this.codes;
}
public String getCode() {
return (this.codes != null && this.codes.length > 0) ? this.codes[this.codes.length - 1] : null;
}
@Override
public Object[] getArguments() {
return this.arguments;
}
@Override
public String getDefaultMessage() {
return this.defaultMessage;
}
protected final String resolvableToString() {
StringBuilder result = new StringBuilder();
result.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ","));
result.append("]; arguments [" + StringUtils.arrayToDelimitedString(this.arguments, ","));
result.append("]; default message [").append(this.defaultMessage).append(']');
return result.toString();
}
@Override
public String toString() {
return getClass().getName() + ": " + resolvableToString();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MessageSourceResolvable)) {
return false;
}
MessageSourceResolvable otherResolvable = (MessageSourceResolvable) other;
return ObjectUtils.nullSafeEquals(getCodes(), otherResolvable.getCodes()) &&
ObjectUtils.nullSafeEquals(getArguments(), otherResolvable.getArguments()) &&
ObjectUtils.nullSafeEquals(getDefaultMessage(), otherResolvable.getDefaultMessage());
}
@Override
public int hashCode() {
int hashCode = ObjectUtils.nullSafeHashCode(getCodes());
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArguments());
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getDefaultMessage());
return hashCode;
}
}
和我们预料的也没啥两样。。。
* @see org.springframework.context.support.ResourceBundleMessageSource * @see org.springframework.context.support.ReloadableResourceBundleMessageSource其中ResourceBundleMessageSource是基于java.util.ResourceBundle实现的,而ReloadableResourceBunldMessageSource则可以在不重启VM的情况下实现资源的重新加载。
套路又来了,这次套路和上面的ResourceLoader类似,都是有一天突然感觉以前的接口貌似有一个功能忘了写了,而且现在现在需要一气呵成毫无违和感,肿么办。。那么就再一次拓展接口。。。Spring中很多这样的套路
public interface HierarchicalMessageSource extends MessageSource {
void setParentMessageSource(MessageSource parent);
/**
* Return the parent of this MessageSource, or {@code null} if none.
*/
MessageSource getParentMessageSource();
}
再来看一个套路,现在拓展也拓展了,但是我想只针对MessageSource提供功能的支持,那么应该怎么办呢?第一反应就MessageSourceUtils。没错我第一反应也是这,但是在Spring中人家可是这样一个名称,因为XXXUtils一般都是公共工具类,而这个只针对MessageSource那么人家叫了MessageSourceSupport,实现获取MessageSource的一些基本方法,看看工具都有啥
public abstract class MessageSourceSupport {
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
protected final Log logger = LogFactory.getLog(getClass());
private boolean alwaysUseMessageFormat = false;
private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage =
new HashMap<String, Map<Locale, MessageFormat>>();
public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {
this.alwaysUseMessageFormat = alwaysUseMessageFormat;
}
protected boolean isAlwaysUseMessageFormat() {
return this.alwaysUseMessageFormat;
}
protected String renderDefaultMessage(String defaultMessage, Object[] args, Locale locale) {
return formatMessage(defaultMessage, args, locale);
}
protected String formatMessage(String msg, Object[] args, Locale locale) {
if (msg == null || (!this.alwaysUseMessageFormat && ObjectUtils.isEmpty(args))) {
return msg;
}
MessageFormat messageFormat = null;
synchronized (this.messageFormatsPerMessage) {
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
if (messageFormatsPerLocale != null) {
messageFormat = messageFormatsPerLocale.get(locale);
}
else {
messageFormatsPerLocale = new HashMap<Locale, MessageFormat>();
this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
}
if (messageFormat == null) {
try {
messageFormat = createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved
if (this.alwaysUseMessageFormat) {
throw ex;
}
// silently proceed with raw message if format not enforced
messageFormat = INVALID_MESSAGE_FORMAT;
}
messageFormatsPerLocale.put(locale, messageFormat);
}
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
synchronized (messageFormat) {
return messageFormat.format(resolveArguments(args, locale));
}
}
protected MessageFormat createMessageFormat(String msg, Locale locale) {
return new MessageFormat((msg != null ? msg : ""), locale);
}
protected Object[] resolveArguments(Object[] args, Locale locale) {
return args;
}
}
可以看到还是基于JDK的MessageFormat的可以参见:http://blog.csdn.net/zhiweianran/article/details/8666992
又来了一波套路,还记得我们曾经提起的DefaultXXX吗,这个大部分是实现某个接口,当你不给我传的时候我就自己来,但是有时候我就是需要一个基本实现的模版而已这个时候AbstractXXX就上线了。同理Spring给处理AbstractMessageSource类
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
private MessageSource parentMessageSource;
private Properties commonMessages;
private boolean useCodeAsDefaultMessage = false;
@Override
public void setParentMessageSource(MessageSource parent) {
this.parentMessageSource = parent;
}
@Override
public MessageSource getParentMessageSource() {
return this.parentMessageSource;
}
public void setCommonMessages(Properties commonMessages) {
this.commonMessages = commonMessages;
}
/**
* Return a Properties object defining locale-independent common messages, if any.
*/
protected Properties getCommonMessages() {
return this.commonMessages;
}
public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) {
this.useCodeAsDefaultMessage = useCodeAsDefaultMessage;
}
protected boolean isUseCodeAsDefaultMessage() {
return this.useCodeAsDefaultMessage;
}
@Override
public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
}
return renderDefaultMessage(defaultMessage, args, locale);
}
@Override
public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale)
throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes == null) {
codes = new String[0];
}
for (String code : codes) {
String msg = getMessageInternal(code, resolvable.getArguments(), locale);
if (msg != null) {
return msg;
}
}
String defaultMessage = resolvable.getDefaultMessage();
if (defaultMessage != null) {
return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);
}
if (codes.length > 0) {
String fallback = getDefaultMessage(codes[0]);
if (fallback != null) {
return fallback;
}
}
throw new NoSuchMessageException(codes.length > 0 ? codes[codes.length - 1] : null, locale);
}
protected String getMessageInternal(String code, Object[] args, Locale locale) {
if (code == null) {
return null;
}
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
// Optimized resolution: no arguments to apply,
// therefore no MessageFormat needs to be involved.
// Note that the default implementation still uses MessageFormat;
// this can be overridden in specific subclasses.
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
// Resolve arguments eagerly, for the case where the message
// is defined in a parent MessageSource but resolvable arguments
// are defined in the child MessageSource.
argsToUse = resolveArguments(args, locale);
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
// Check locale-independent common messages for the given message code.
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
// Not found -> check parent, if any.
return getMessageFromParent(code, argsToUse, locale);
}
protected String getMessageFromParent(String code, Object[] args, Locale locale) {
MessageSource parent = getParentMessageSource();
if (parent != null) {
if (parent instanceof AbstractMessageSource) {
// Call internal method to avoid getting the default code back
// in case of "useCodeAsDefaultMessage" being activated.
return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale);
}
else {
// Check parent MessageSource, returning null if not found there.
return parent.getMessage(code, args, null, locale);
}
}
// Not found in parent either.
return null;
}
protected String getDefaultMessage(String code) {
if (isUseCodeAsDefaultMessage()) {
return code;
}
return null;
}
@Override
protected Object[] resolveArguments(Object[] args, Locale locale) {
if (args == null) {
return new Object[0];
}
List<Object> resolvedArgs = new ArrayList<Object>(args.length);
for (Object arg : args) {
if (arg instanceof MessageSourceResolvable) {
resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));
}
else {
resolvedArgs.add(arg);
}
}
return resolvedArgs.toArray(new Object[resolvedArgs.size()]);
}
protected String resolveCodeWithoutArguments(String code, Locale locale) {
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(new Object[0]);
}
}
return null;
}
protected abstract MessageFormat resolveCode(String code, Locale locale);
}
基本实现了getMessage的功能,只不过这个类采用了模版方法的设计模式,将参数的中code的解决方案延迟到了子类中,下面来看看期待已久的基于ResourceBundle的ResourceBundleMessageSource。。
package org.springframework.context.support;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
public class ResourceBundleMessageSource extends AbstractMessageSource implements BeanClassLoaderAware {
private String[] basenames = new String[0];
private String defaultEncoding;
private boolean fallbackToSystemLocale = true;
private long cacheMillis = -1;
private ClassLoader bundleClassLoader;
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
new HashMap<String, Map<Locale, ResourceBundle>>();
private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats =
new HashMap<ResourceBundle, Map<String, Map<Locale, MessageFormat>>>();
public void setBasename(String basename) {
setBasenames(basename);
}
public void setBasenames(String... basenames) {
if (basenames != null) {
this.basenames = new String[basenames.length];
for (int i = 0; i < basenames.length; i++) {
String basename = basenames[i];
Assert.hasText(basename, "Basename must not be empty");
this.basenames[i] = basename.trim();
}
}
else {
this.basenames = new String[0];
}
}
public void setDefaultEncoding(String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
}
public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
this.fallbackToSystemLocale = fallbackToSystemLocale;
}
public void setCacheSeconds(int cacheSeconds) {
this.cacheMillis = (cacheSeconds * 1000);
}
public void setBundleClassLoader(ClassLoader classLoader) {
this.bundleClassLoader = classLoader;
}
/**
* Return the ClassLoader to load resource bundles with.
* <p>Default is the containing BeanFactory's bean ClassLoader.
* @see #setBundleClassLoader
*/
protected ClassLoader getBundleClassLoader() {
return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader);
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
/**
* Resolves the given message code as key in the registered resource bundles,
* returning the value found in the bundle as-is (without MessageFormat parsing).
*/
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
String result = null;
for (int i = 0; result == null && i < this.basenames.length; i++) {
ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);
if (bundle != null) {
result = getStringOrNull(bundle, code);
}
}
return result;
}
/**
* Resolves the given message code as key in the registered resource bundles,
* using a cached MessageFormat instance per message code.
*/
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
MessageFormat messageFormat = null;
for (int i = 0; messageFormat == null && i < this.basenames.length; i++) {
ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);
if (bundle != null) {
messageFormat = getMessageFormat(bundle, code, locale);
}
}
return messageFormat;
}
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
if (this.cacheMillis >= 0) {
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
// do its native caching, at the expense of more extensive lookup steps.
return doGetBundle(basename, locale);
}
else {
// Cache forever: prefer locale cache over repeated getBundle calls.
synchronized (this.cachedResourceBundles) {
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = new HashMap<Locale, ResourceBundle>();
this.cachedResourceBundles.put(basename, localeMap);
}
localeMap.put(locale, bundle);
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
}
}
}
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
if ((this.defaultEncoding != null && !"ISO-8859-1".equals(this.defaultEncoding)) ||
!this.fallbackToSystemLocale || this.cacheMillis >= 0) {
return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), new MessageSourceControl());
}
else {
// Good old standard call...
return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
}
}
protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
throws MissingResourceException {
synchronized (this.cachedBundleMessageFormats) {
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
Map<Locale, MessageFormat> localeMap = null;
if (codeMap != null) {
localeMap = codeMap.get(code);
if (localeMap != null) {
MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
}
String msg = getStringOrNull(bundle, code);
if (msg != null) {
if (codeMap == null) {
codeMap = new HashMap<String, Map<Locale, MessageFormat>>();
this.cachedBundleMessageFormats.put(bundle, codeMap);
}
if (localeMap == null) {
localeMap = new HashMap<Locale, MessageFormat>();
codeMap.put(code, localeMap);
}
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
return null;
}
}
private String getStringOrNull(ResourceBundle bundle, String key) {
try {
return bundle.getString(key);
}
catch (MissingResourceException ex) {
// Assume key not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
}
/**
* Show the configuration of this MessageSource.
*/
@Override
public String toString() {
return getClass().getName() + ": basenames=[" +
StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";
}
/**
* Custom implementation of Java 6's {@code ResourceBundle.Control},
* adding support for custom file encodings, deactivating the fallback to the
* system locale and activating ResourceBundle's native cache, if desired.
*/
private class MessageSourceControl extends ResourceBundle.Control {
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
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 stream;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction<InputStream>() {
@Override
public InputStream run() throws IOException {
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();
}
}
}
else {
is = classLoader.getResourceAsStream(resourceName);
}
return is;
}
});
}
catch (PrivilegedActionException ex) {
throw (IOException) ex.getException();
}
if (stream != null) {
try {
return (defaultEncoding != null ?
new PropertyResourceBundle(new InputStreamReader(stream, defaultEncoding)) :
new PropertyResourceBundle(stream));
}
finally {
stream.close();
}
}
else {
return null;
}
}
else {
return super.newBundle(baseName, locale, format, loader, reload);
}
}
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
return (fallbackToSystemLocale ? super.getFallbackLocale(baseName, locale) : null);
}
@Override
public long getTimeToLive(String baseName, Locale locale) {
return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale));
}
@Override
public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {
if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) {
cachedBundleMessageFormats.remove(bundle);
return true;
}
else {
return false;
}
}
}
}
其实看起来确实也没有啥,和我们平时使用的ResourceBundle导入Property文件没什么两样就是加入了各种编码,Local的本地化,以及各种Context的路径判断。。。等等。如果加上国际化的实例是不是更爽歪歪呢???对就是这样可以看下这篇文章:http://www.cnblogs.com/liukemng/p/3750117.html
首先配置我们项目的springservlet-config.xml文件添加的内容如下:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<!-- 国际化信息所在的文件名 -->
<property name="basename" value="messages" />
<!-- 如果在国际化资源文件中找不到对应代码的信息,就用这个代码作为名称 -->
<property name="useCodeAsDefaultMessage" value="true" />
</bean>
下面展示的是后台获取的国际化信息:<br/>
${money}<br/>
${date}<br/>
下面展示的是视图中直接绑定的国际化信息:<br/>
<spring:message code="money"/>:<br/>
<spring:eval expression="contentModel.money"></spring:eval><br/>
<spring:message code="date"/>:<br/>
<spring:eval expression="contentModel.date"></spring:eval><br/>
是不是很熟悉了,,其实ReloadableResourceBundleMessageSource,其实就是在ResourceBundleMessageSource的基础上加了一个不断根据当前时间去刷新Property文件内容到内存中,这样虽然不用重启VM但是不断的IO操作带来的损失也是很大的,估计也就是在测试环境用吧。
public class ReloadableResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
private static final String PROPERTIES_SUFFIX = ".properties";
private static final String XML_SUFFIX = ".xml";
private String[] basenames = new String[0];
private String defaultEncoding;
private Properties fileEncodings;
private boolean fallbackToSystemLocale = true;
private long cacheMillis = -1;
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<String, Map<Locale, List<String>>>();
private final ConcurrentMap<String, PropertiesHolder> cachedProperties =
new ConcurrentHashMap<String, PropertiesHolder>();
/** Cache to hold merged loaded properties per locale */
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties =
new ConcurrentHashMap<Locale, PropertiesHolder>();
public void setBasename(String basename) {
setBasenames(basename);
}
public void setBasenames(String... basenames) {
if (basenames != null) {
this.basenames = new String[basenames.length];
for (int i = 0; i < basenames.length; i++) {
String basename = basenames[i];
Assert.hasText(basename, "Basename must not be empty");
this.basenames[i] = basename.trim();
}
}
else {
this.basenames = new String[0];
}
}
public void setDefaultEncoding(String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
}
public void setFileEncodings(Properties fileEncodings) {
this.fileEncodings = fileEncodings;
}
public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
this.fallbackToSystemLocale = fallbackToSystemLocale;
}
public void setCacheSeconds(int cacheSeconds) {
this.cacheMillis = (cacheSeconds * 1000);
}
public void setConcurrentRefresh(boolean concurrentRefresh) {
this.concurrentRefresh = concurrentRefresh;
}
public void setPropertiesPersister(PropertiesPersister propertiesPersister) {
this.propertiesPersister =
(propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister());
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
}
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
if (this.cacheMillis < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
else {
for (String basename : this.basenames) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
}
return null;
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
if (this.cacheMillis < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
else {
for (String basename : this.basenames) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
}
return null;
}
protected PropertiesHolder getMergedProperties(Locale locale) {
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = new Properties();
mergedHolder = new PropertiesHolder(mergedProps, -1);
for (int i = this.basenames.length - 1; i >= 0; i--) {
List<String> filenames = calculateAllFilenames(this.basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() != null) {
mergedProps.putAll(propHolder.getProperties());
}
}
}
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
protected List<String> calculateAllFilenames(String basename, Locale locale) {
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
if (localeMap != null) {
List<String> filenames = localeMap.get(locale);
if (filenames != null) {
return filenames;
}
}
List<String> filenames = new ArrayList<String>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
}
}
}
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<Locale, List<String>>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
return filenames;
}
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
List<String> result = new ArrayList<String>(3);
String language = locale.getLanguage();
String country = locale.getCountry();
String variant = locale.getVariant();
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
return result;
}
protected PropertiesHolder getProperties(String filename) {
PropertiesHolder propHolder = this.cachedProperties.get(filename);
long originalTimestamp = -2;
if (propHolder != null) {
originalTimestamp = propHolder.getRefreshTimestamp();
if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - this.cacheMillis) {
// Up to date
return propHolder;
}
}
else {
propHolder = new PropertiesHolder();
PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder);
if (existingHolder != null) {
propHolder = existingHolder;
}
}
// At this point, we need to refresh...
if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
// A populated but stale holder -> could keep using it.
if (!propHolder.refreshLock.tryLock()) {
// Getting refreshed by another thread already ->
// let's return the existing properties for the time being.
return propHolder;
}
}
else {
propHolder.refreshLock.lock();
}
try {
PropertiesHolder existingHolder = this.cachedProperties.get(filename);
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
return existingHolder;
}
return refreshProperties(filename, propHolder);
}
finally {
propHolder.refreshLock.unlock();
}
}
protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System.currentTimeMillis());
Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if (!resource.exists()) {
resource = this.resourceLoader.getResource(filename + XML_SUFFIX);
}
if (resource.exists()) {
long fileTimestamp = -1;
if (this.cacheMillis >= 0) {
// Last-modified timestamp of file will just be read if caching with timeout.
try {
fileTimestamp = resource.lastModified();
if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
if (logger.isDebugEnabled()) {
logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
}
propHolder.setRefreshTimestamp(refreshTimestamp);
return propHolder;
}
}
catch (IOException ex) {
// Probably a class path resource: cache it forever.
if (logger.isDebugEnabled()) {
logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex);
}
fileTimestamp = -1;
}
}
try {
Properties props = loadProperties(resource, filename);
propHolder = new PropertiesHolder(props, fileTimestamp);
}
catch (IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex);
}
// Empty holder representing "not valid".
propHolder = new PropertiesHolder();
}
}
else {
// Resource does not exist.
if (logger.isDebugEnabled()) {
logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");
}
// Empty holder representing "not found".
propHolder = new PropertiesHolder();
}
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
return propHolder;
}
protected Properties loadProperties(Resource resource, String filename) throws IOException {
InputStream is = resource.getInputStream();
Properties props = new Properties();
try {
if (resource.getFilename().endsWith(XML_SUFFIX)) {
if (logger.isDebugEnabled()) {
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.defaultEncoding;
}
if (encoding != null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, is);
}
}
return props;
}
finally {
is.close();
}
}
/**
* Clear the resource bundle cache.
* Subsequent resolve calls will lead to reloading of the properties files.
*/
public void clearCache() {
logger.debug("Clearing entire resource bundle cache");
this.cachedProperties.clear();
this.cachedMergedProperties.clear();
}
/**
* Clear the resource bundle caches of this MessageSource and all its ancestors.
* @see #clearCache
*/
public void clearCacheIncludingAncestors() {
clearCache();
if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) {
((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors();
}
}
@Override
public String toString() {
return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";
}
/**
* PropertiesHolder for caching.
* Stores the last-modified timestamp of the source file for efficient
* change detection, and the timestamp of the last refresh attempt
* (updated every time the cache entry gets re-validated).
*/
protected class PropertiesHolder {
private final Properties properties;
private final long fileTimestamp;
private volatile long refreshTimestamp = -2;
private final ReentrantLock refreshLock = new ReentrantLock();
/** Cache to hold already generated MessageFormats per message code */
private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats =
new ConcurrentHashMap<String, Map<Locale, MessageFormat>>();
public PropertiesHolder() {
this.properties = null;
this.fileTimestamp = -1;
}
public PropertiesHolder(Properties properties, long fileTimestamp) {
this.properties = properties;
this.fileTimestamp = fileTimestamp;
}
public Properties getProperties() {
return this.properties;
}
public long getFileTimestamp() {
return this.fileTimestamp;
}
public void setRefreshTimestamp(long refreshTimestamp) {
this.refreshTimestamp = refreshTimestamp;
}
public long getRefreshTimestamp() {
return this.refreshTimestamp;
}
public String getProperty(String code) {
if (this.properties == null) {
return null;
}
return this.properties.getProperty(code);
}
public MessageFormat getMessageFormat(String code, Locale locale) {
if (this.properties == null) {
return null;
}
Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
if (localeMap != null) {
MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
String msg = this.properties.getProperty(code);
if (msg != null) {
if (localeMap == null) {
localeMap = new ConcurrentHashMap<Locale, MessageFormat>();
Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
if (existing != null) {
localeMap = existing;
}
}
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
return null;
}
}
}