Does Spring MessageSource Support Multiple Class Path?
Asked Answered
J

7

20

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?

Jocundity answered 8/10, 2010 at 8:24 Comment(0)
P
12

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.

Patellate answered 8/10, 2010 at 9:25 Comment(2)
Thanks! It is something I worried about.Jocundity
For a solution that works look at ajaristi's answerStatampere
M
26

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>
Macro answered 17/12, 2014 at 18:57 Comment(2)
This should be the answer, it gives a solution and it works. ThanksCairngorm
In 2021 this still seems to be the best working answer. Thanks!Hulen
P
12

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.

Patellate answered 8/10, 2010 at 9:25 Comment(2)
Thanks! It is something I worried about.Jocundity
For a solution that works look at ajaristi's answerStatampere
N
9

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>
Nebulous answered 8/10, 2010 at 9:0 Comment(2)
Yes, that's right. But you have to know all plugins beforehand. The soulution should be universal for plugins.Jocundity
You just taught me how to enter package paths in values.Bloodletting
F
3

overriding ReloadableResourceBundleMessageSource::calculateFilenamesForLocale may be better. Then, ReloadableResourceBundleMessageSource::getProperties can get PropertiesHolder from cachedProperties

Frugal answered 17/5, 2018 at 6:3 Comment(1)
Overriding "calculateFilenamesForLocale" is indeed better since it enables you to still use the "Reloadble" part of your ReloadableResourceBundleMessageSourceConfusion
C
2

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.

Cobwebby answered 31/7, 2013 at 18:18 Comment(0)
S
1

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.

Silly answered 26/11, 2017 at 10:23 Comment(0)
D
0

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;
    }
    
}

Dehydrogenate answered 9/2, 2021 at 5:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.