diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java index c5d8e7091f..27762ba049 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,8 +115,9 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage /** * Set the default charset to use for parsing properties files. * Used if no file-specific charset is specified for a file. - *

Default is none, using the {@code java.util.Properties} - * default encoding: ISO-8859-1. + *

The effective default is the {@code java.util.Properties} + * default encoding: ISO-8859-1. A {@code null} value indicates + * the platform default encoding. *

Only applies to classic properties files, not to XML files. * @param defaultEncoding the default charset */ diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java index dad357807b..0bfa58cdad 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -58,6 +58,17 @@ import org.springframework.util.ClassUtils; * Note that the JDK's standard ResourceBundle treats dots as package separators: * This means that "test.theme" is effectively equivalent to "test/theme". * + *

On the classpath, bundle resources will be read with the locally configured + * {@link #setDefaultEncoding encoding}: by default, ISO-8859-1; consider switching + * this to UTF-8, or to {@code null} for the platform default encoding. On the JDK 9+ + * module path where locally provided {@link ResourceBundle.Control} handles are not + * supported, this MessageSource always falls back to {@link ResourceBundle#getBundle} + * retrieval with the platform default encoding: UTF-8 with a ISO-8859-1 fallback on + * JDK 9+ (configurable through the "java.util.PropertyResourceBundle.encoding" system + * property). Note that {@link #loadBundle(Reader)}/{@link #loadBundle(InputStream)} + * won't be called in this case either, effectively ignoring overrides in subclasses. + * Consider implementing a JDK 9 {@code java.util.spi.ResourceBundleProvider} instead. + * * @author Rod Johnson * @author Juergen Hoeller * @see #setBasenames @@ -80,7 +91,8 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou * This allows for very efficient hash lookups, significantly faster * than the ResourceBundle class's own cache. */ - private final Map> cachedResourceBundles = new ConcurrentHashMap<>(); + private final Map> cachedResourceBundles = + new ConcurrentHashMap<>(); /** * Cache to hold already generated MessageFormats. @@ -90,7 +102,16 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou * very efficient hash lookups without concatenated keys. * @see #getMessageFormat */ - private final Map>> cachedBundleMessageFormats = new ConcurrentHashMap<>(); + private final Map>> cachedBundleMessageFormats = + new ConcurrentHashMap<>(); + + @Nullable + private volatile MessageSourceControl control = new MessageSourceControl(); + + + public ResourceBundleMessageSource() { + setDefaultEncoding("ISO-8859-1"); + } /** @@ -220,40 +241,71 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { ClassLoader classLoader = getBundleClassLoader(); Assert.state(classLoader != null, "No bundle ClassLoader set"); - String defaultEncoding = getDefaultEncoding(); - if ((defaultEncoding != null && !"ISO-8859-1".equals(defaultEncoding)) || - !isFallbackToSystemLocale() || getCacheMillis() >= 0) { + MessageSourceControl control = this.control; + if (control != null) { try { - return ResourceBundle.getBundle(basename, locale, classLoader, new MessageSourceControl()); + return ResourceBundle.getBundle(basename, locale, classLoader, control); } catch (UnsupportedOperationException ex) { // Probably in a Jigsaw environment on JDK 9+ - throw new IllegalStateException( - "Custom ResourceBundleMessageSource configuration requires custom ResourceBundle.Control " + - "which is not supported in current system environment (e.g. JDK 9+ module path deployment): " + - "consider using defaults (ISO-8859-1 encoding, fallback to system locale, unlimited caching)", - ex); + this.control = null; + String encoding = getDefaultEncoding(); + if (encoding != null && logger.isInfoEnabled()) { + logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" + + encoding + "' but ResourceBundle.Control not supported in current system environment: " + + ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + + "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + + "for participating in the platform default and therefore avoiding this log message."); + } } } - else { - return ResourceBundle.getBundle(basename, locale, classLoader); - } + + // Fallback: plain getBundle lookup without Control handle + return ResourceBundle.getBundle(basename, locale, classLoader); } /** * Load a property-based resource bundle from the given reader. + *

This will be called in case of a {@link #setDefaultEncoding "defaultEncoding"}, + * including {@link ResourceBundleMessageSource}'s default ISO-8859-1 encoding. + * Note that this method can only be called with a {@link ResourceBundle.Control}: + * When running on the JDK 9+ module path where such control handles are not + * supported, any overrides in custom subclasses will effectively get ignored. *

The default implementation returns a {@link PropertyResourceBundle}. * @param reader the reader for the target resource * @return the fully loaded bundle * @throws IOException in case of I/O failure * @since 4.2 + * @see #loadBundle(InputStream) * @see PropertyResourceBundle#PropertyResourceBundle(Reader) */ protected ResourceBundle loadBundle(Reader reader) throws IOException { return new PropertyResourceBundle(reader); } + /** + * Load a property-based resource bundle from the given input stream, + * picking up the default properties encoding on JDK 9+. + *

This will only be called with {@link #setDefaultEncoding "defaultEncoding"} + * set to {@code null}, explicitly enforcing the platform default encoding + * (which is UTF-8 with a ISO-8859-1 fallback on JDK 9+ but configurable + * through the "java.util.PropertyResourceBundle.encoding" system property). + * Note that this method can only be called with a {@link ResourceBundle.Control}: + * When running on the JDK 9+ module path where such control handles are not + * supported, any overrides in custom subclasses will effectively get ignored. + *

The default implementation returns a {@link PropertyResourceBundle}. + * @param inputStream the input stream for the target resource + * @return the fully loaded bundle + * @throws IOException in case of I/O failure + * @since 5.1 + * @see #loadBundle(Reader) + * @see PropertyResourceBundle#PropertyResourceBundle(InputStream) + */ + protected ResourceBundle loadBundle(InputStream inputStream) throws IOException { + return new PropertyResourceBundle(inputStream); + } + /** * Return a MessageFormat for the given bundle and code, * fetching already generated MessageFormats from the cache. @@ -284,7 +336,8 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou if (msg != null) { if (codeMap == null) { codeMap = new ConcurrentHashMap<>(); - Map> existing = this.cachedBundleMessageFormats.putIfAbsent(bundle, codeMap); + Map> existing = + this.cachedBundleMessageFormats.putIfAbsent(bundle, codeMap); if (existing != null) { codeMap = existing; } @@ -359,9 +412,9 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou final String resourceName = toResourceName(bundleName, "properties"); final ClassLoader classLoader = loader; final boolean reloadFlag = reload; - InputStream stream; + InputStream inputStream; try { - stream = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + inputStream = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { InputStream is = null; if (reloadFlag) { URL url = classLoader.getResource(resourceName); @@ -382,13 +435,17 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou catch (PrivilegedActionException ex) { throw (IOException) ex.getException(); } - if (stream != null) { + if (inputStream != null) { String encoding = getDefaultEncoding(); - if (encoding == null) { - encoding = "ISO-8859-1"; + if (encoding != null) { + try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) { + return loadBundle(bundleReader); + } } - try (InputStreamReader bundleReader = new InputStreamReader(stream, encoding)) { - return loadBundle(bundleReader); + else { + try (InputStream bundleStream = inputStream) { + return loadBundle(bundleStream); + } } } else {