-
Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
I am not sure about reasoning behind it, but I'll be happy if you could at least address the extensibility issue which I'll outline. In that case, feel free to change the title to something more appropriate.
I am trying to use ReloadableResourceBundleMessageSource to load messages defined within bundles across several JARs (internal libraries, modules).
I have defined the following.
@Bean
MessageSource internalMessageSource(ResourceLoader resourceLoader) {
var messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"classpath:i18n/messages",
"file:i18n/messages");
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
messageSource.setResourceLoader(resourceLoader);
return messageSource;
}I have placed messages.properties into src/main/resources/i18n across two different JARs and used those two JARs as a dependency to my Spring Boot application.
Obtaining any message with key located within messages.properties in any of those external JARs will result in NoSuchMessageException.
While debugging from where the properties were loaded, I concluded that they are loaded from Spring Boot application class path, due to usage of ResourceLoader.loadResource(..) rather than ResourcePatternResolver.loadResources(...)
If the latter was used, I could specify my basenames as classpath*:i18n/messages and let them be merged.
I have extended the ReloadableResourceBundleMessageSource to use ResourcePatternResolver when possible, but I ran into the issue that many fields used by methods that required extending are private. Some I had to override, and others I had to use reflection to get to.
So, I ask either one, or ideally both of you
- Would you consider supporting using Resource Patterns as basenames in
ReloadableResourceBundleMessageSource? - Would you consider making
ReloadableResourceBundleMessageSourceextensible to add such functionality by adding (protected) getters to internal class fields?
I am working with Spring Framework version 6.2.15, but it would be okay if any or both of those could make it to 7.0 series if they are deemed to major for 6.2 series.
Below is my implementation that seems to work so far, but I have not yet thoroughly tested it.
import static org.springframework.util.ReflectionUtils.findField;
import static org.springframework.util.ReflectionUtils.getField;
import static org.springframework.util.ReflectionUtils.makeAccessible;
import static java.util.Objects.requireNonNullElseGet;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
public class MultiResourceBundleMessageSource extends ReloadableResourceBundleMessageSource
implements ResourceLoaderAware {
private ResourceLoader resourceLoader = new DefaultResourceLoader();
private List<String> fileExtensions = List.of(".properties", ".xml");
private Properties fileEncodings;
private final ConcurrentMap<String, PropertiesHolder> cachedProperties;
@SuppressWarnings("unchecked")
public MultiResourceBundleMessageSource() {
Field field = findField(ReloadableResourceBundleMessageSource.class, "cachedProperties");
Assert.notNull(field, "cachedProperties field must not be null");
makeAccessible(field);
this.cachedProperties = (ConcurrentMap<String, PropertiesHolder>) getField(field, this);
Assert.notNull(cachedProperties, "Cached Properties map must not be null");
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = requireNonNullElseGet(resourceLoader, DefaultResourceLoader::new);
super.setResourceLoader(resourceLoader);
}
@Override
public void setFileExtensions(List<String> fileExtensions) {
Assert.isTrue(
!CollectionUtils.isEmpty(fileExtensions),
"At least one file extension is required");
for (String extension : fileExtensions) {
Assert.isTrue(
extension.startsWith("."),
() -> "File extension '" + extension + "' should start with '.'");
}
this.fileExtensions = Collections.unmodifiableList(fileExtensions);
super.setFileExtensions(fileExtensions);
}
@Override
public void setFileEncodings(Properties fileEncodings) {
this.fileEncodings = fileEncodings;
super.setFileEncodings(fileEncodings);
}
@Override
protected PropertiesHolder refreshProperties(
String filename, //
ReloadableResourceBundleMessageSource.PropertiesHolder propHolder) {
long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
Properties properties = newProperties();
fillProperties(properties, filename);
PropertiesHolder updatedPropHolder = new PropertiesHolder();
if (!properties.isEmpty()) {
updatedPropHolder = new PropertiesHolder(properties, System.currentTimeMillis());
}
updatedPropHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, updatedPropHolder);
return updatedPropHolder;
}
private void fillProperties(Properties properties, String filename) {
List<EncodedResource> resources = new ArrayList<>();
String encoding = getFileEncoding(filename);
for (String fileExtension : this.fileExtensions) {
String filenameWithExtension = filename + fileExtension;
if (resourceLoader instanceof ResourcePatternResolver rpr) {
addResources(resources, filenameWithExtension, encoding, rpr);
} else {
addResources(resources, filenameWithExtension, encoding);
}
if (!resources.isEmpty()) {
break;
}
}
for (EncodedResource res : resources) {
try {
PropertiesLoaderUtils.fillProperties(properties, res);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
private void addResources(List<EncodedResource> resources, String filename, String encoding) {
Resource res = resourceLoader.getResource(filename);
if (res.exists()) {
resources.add(new EncodedResource(res, encoding));
}
}
private void addResources(
List<EncodedResource> resources,
String resourcePattern,
String encoding,
ResourcePatternResolver rpr) {
try {
Resource[] res = rpr.getResources(resourcePattern);
if (ObjectUtils.isEmpty(res)) {
return;
}
for (Resource r : res) {
if (r.exists()) {
resources.add(new EncodedResource(r, encoding));
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private String getFileEncoding(String filename) {
if (this.fileEncodings != null) {
return this.fileEncodings.getProperty(filename);
}
return getDefaultEncoding();
}
}