Reputation: 1869
I am designing a plugin system for our web based application using Spring framework. Plugins are jars on classpath. So I am able to get sources such as jsp, see below
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] pages = resolver.getResources("classpath*:jsp/*jsp");
So far so good. But I have a problem with the messageSource. It seems to me that ReloadableResourceBundleMessageSource#setBasename does NOT support multiple class path via the "classpath*:" If I use just "classpath:", I get the messageSource just only from one plugin.
Does anyone have an idea how to register messageSources from all plugins? Does exist such an implementation of MessageSource?
Upvotes: 20
Views: 29480
Reputation: 1
As @Jia Feng said an alternative solution overriding ReloadableResourceBundleMessageSource::calculateFilenamesForLocale.
It works on spring 5.x
public class WildcardReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
List<String> filenames = super.calculateFilenamesForLocale(basename, locale);
List<String> add = new ArrayList<>();
for (String filename : filenames) {
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
add.add(sourcePath);
}
} catch (IOException ignored) {
}
}
filenames.addAll(add);
return filenames;
}
}
Upvotes: 0
Reputation: 31
overriding ReloadableResourceBundleMessageSource::calculateFilenamesForLocale
may be better. Then, ReloadableResourceBundleMessageSource::getProperties
can get PropertiesHolder
from cachedProperties
Upvotes: 3
Reputation: 943
You can take advantage of Java configuration and hierarchical message sources to build a quite simple plugin system. In each pluggable jar drop a class like this:
@Configuration
public class MyPluginConfig {
@Bean
@Qualifier("external")
public HierarchicalMessageSource mypluginMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:my-plugin-messages");
return messageSource;
}
}
and the corresponding my-plugin-messages.properties
files.
In the main application Java config class put something like this:
@Configuration
public class MainConfig {
@Autowired(required = false)
@Qualifier("external")
private List<HierarchicalMessageSource> externalMessageSources = Collections.emptyList();
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource rootMessageSource = new ReloadableResourceBundleMessageSource();
rootMessageSource.setBasenames("classpath:messages");
if (externalMessageSources.isEmpty()) {
// No external message sources found, just main message source will be used
return rootMessageSource;
}
else {
// Wiring detected external message sources, putting main message source as "last resort"
int count = externalMessageSources.size();
for (int i = 0; i < count; i++) {
HierarchicalMessageSource current = externalMessageSources.get(i);
current.setParentMessageSource( i == count - 1 ? rootMessageSource : externalMessageSources.get(i + 1) );
}
return externalMessageSources.get(0);
}
}
}
If the order of plugins is relevant, just put @Order
annotations in each pluggable message source bean.
Upvotes: 1
Reputation: 982
With the solution of @seralex-vi basenames /WEB-INF/messages did not function.
I overwrited the method refreshProperties on the class ReloadableResourceBundleMessageSource wich perform both type of basenames (classpath*: and /WEB-INF/)
public class SmReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
if (filename.startsWith(PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) {
return refreshClassPathProperties(filename, propHolder);
} else {
return super.refreshProperties(filename, propHolder);
}
}
private PropertiesHolder refreshClassPathProperties(String filename, PropertiesHolder propHolder) {
Properties properties = new Properties();
long lastModified = -1;
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder);
properties.putAll(holder.getProperties());
if (lastModified < resource.lastModified())
lastModified = resource.lastModified();
}
} catch (IOException ignored) {
}
return new PropertiesHolder(properties, lastModified);
}
On the spring-context.xml you must have the classpath*: prefix
<bean id="messageSource" class="SmReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/WEB-INF/i18n/enums</value>
<value>/WEB-INF/i18n/messages</value>
<value>classpath*:/META-INF/messages-common</value>
<value>classpath*:/META-INF/enums</value>
</list>
</property>
</bean>
Upvotes: 26
Reputation: 620
As alternative, you could override refreshProperties
method from ReloadableResourceBundleMessageSource
class like below example:
public class MultipleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
Properties properties = new Properties();
long lastModified = -1;
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder);
properties.putAll(holder.getProperties());
if (lastModified < resource.lastModified())
lastModified = resource.lastModified();
}
} catch (IOException ignored) { }
return new PropertiesHolder(properties, lastModified);
}
}
and use it with spring context configuration like ReloadableResourceBundleMessageSource
:
<bean id="messageSource" class="common.utils.MultipleMessageSource">
<property name="basenames">
<list>
<value>classpath:/messages/validation</value>
<value>classpath:/messages/messages</value>
</list>
</property>
<property name="fileEncodings" value="UTF-8"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
I think this should do the trick.
Upvotes: 2
Reputation: 403501
The issue here is not with multiple classpaths or classloaders, but with how many resources the code will try and load for a given path.
The classpath*
syntax is a Spring mechanism, one which allows code to load multiple resources for a given path. Very handy. However, ResourceBundleMessageSource
uses the standard java.util.ResourceBundle
to load the resources, and this is a much simpler, dumber mechanism, which will load the first resource for a given path, and ignore everything else.
I don't really have an easy fix for you. I think you're going to have to ditch ResourceBundleMessageSource
and write a custom implementation of MessageSource
(most likely by subclassing AbstractMessageSource
) which uses PathMatchingResourcePatternResolver
to locate the various resources and expose them via the MessageSource
interface. ResourceBundle
isn't going to be much help.
Upvotes: 12
Reputation: 52645
You could do something similar to below - essentially specify each relevant basename explicitly.
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:com/your/package/source1</value>
<value>classpath:com/your/second/package/source2</value>
<value>classpath:com/your/third/package/source3/value>
<value>classpath:com/your/fourth/package/source4</value>
</list>
</property>
</bean>
Upvotes: 9