Does Spring MessageSource Support Multiple Class Path?

Viewed 28015

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?

7 Answers

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

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.

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

Related