Add ResourceBasedMessageSourceConfigurer

Signed-off-by: henriquejsza <henriquejsza@gmail.com>

See gh-51484
This commit is contained in:
henriquejsza
2026-09-02 14:53:00 +01:00
committed by Andy Wilkinson
parent 2772023050
commit 626d5fdf3c
4 changed files with 166 additions and 43 deletions
@@ -16,12 +16,6 @@
package org.springframework.boot.autoconfigure.context;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Duration;
import java.util.List;
import java.util.Properties;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
@@ -35,7 +29,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.MessageSourceRuntimeHints;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
@@ -44,13 +37,10 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.CollectionFactory;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
@@ -62,12 +52,11 @@ import org.springframework.util.StringUtils;
* @author Eddú Meléndez
* @author Marc Becker
* @author Misagh Moayyed
* @author Henrique (henriquejsza)
* @since 1.5.0
*/
@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties(MessageSourceProperties.class)
@ImportRuntimeHints(MessageSourceRuntimeHints.class)
public final class MessageSourceAutoConfiguration {
@@ -75,39 +64,19 @@ public final class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (!CollectionUtils.isEmpty(properties.getBasename())) {
messageSource.setBasenames(properties.getBasename().toArray(new String[0]));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
messageSource.setCommonMessages(loadCommonMessages(properties.getCommonMessages()));
return messageSource;
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
ResourceBasedMessageSourceConfigurer resourceBasedMessageSourceConfigurer(MessageSourceProperties properties) {
return new ResourceBasedMessageSourceConfigurer(properties);
}
private @Nullable Properties loadCommonMessages(@Nullable List<Resource> resources) {
if (CollectionUtils.isEmpty(resources)) {
return null;
}
Properties properties = CollectionFactory.createSortedProperties(false);
for (Resource resource : resources) {
try {
PropertiesLoaderUtils.fillProperties(properties, resource);
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to load common messages from '%s'".formatted(resource), ex);
}
}
return properties;
@Bean
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME,
search = SearchStrategy.CURRENT)
@Conditional(ResourceBundleCondition.class)
ResourceBundleMessageSource messageSource(ResourceBasedMessageSourceConfigurer configurer) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
configurer.configure(messageSource);
return messageSource;
}
protected static class ResourceBundleCondition extends SpringBootCondition {
@@ -0,0 +1,97 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.context;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Duration;
import java.util.List;
import java.util.Properties;
import org.jspecify.annotations.Nullable;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import org.springframework.core.CollectionFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Configure an {@link AbstractResourceBasedMessageSource} with sensible defaults tuned
* using configuration properties.
* <p>
* Can be injected into application code and used to define a custom message source whose
* configuration is based upon that produced by auto-configuration.
*
* @author Henrique (henriquejsza)
* @since 4.2.0
*/
public class ResourceBasedMessageSourceConfigurer {
private final MessageSourceProperties properties;
/**
* Creates a new configurer that will use the given {@code properties}.
* @param properties properties to use
*/
public ResourceBasedMessageSourceConfigurer(MessageSourceProperties properties) {
Assert.notNull(properties, "'properties' must not be null");
this.properties = properties;
}
/**
* Configure the specified message source. The message source can be further tuned and
* default settings can be overridden.
* @param messageSource the {@link AbstractResourceBasedMessageSource} instance to
* configure
*/
public void configure(AbstractResourceBasedMessageSource messageSource) {
Assert.notNull(messageSource, "'messageSource' must not be null");
if (!CollectionUtils.isEmpty(this.properties.getBasename())) {
messageSource.setBasenames(this.properties.getBasename().toArray(new String[0]));
}
if (this.properties.getEncoding() != null) {
messageSource.setDefaultEncoding(this.properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(this.properties.isFallbackToSystemLocale());
Duration cacheDuration = this.properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(this.properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(this.properties.isUseCodeAsDefaultMessage());
messageSource.setCommonMessages(loadCommonMessages(this.properties.getCommonMessages()));
}
private @Nullable Properties loadCommonMessages(@Nullable List<Resource> resources) {
if (CollectionUtils.isEmpty(resources)) {
return null;
}
Properties properties = CollectionFactory.createSortedProperties(false);
for (Resource resource : resources) {
try {
PropertiesLoaderUtils.fillProperties(properties, resource);
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to load common messages from '%s'".formatted(resource), ex);
}
}
return properties;
}
}
@@ -40,8 +40,12 @@ import org.springframework.context.NoSuchMessageException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MessageSourceAutoConfiguration}.
@@ -53,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Marc Becker
* @author Misagh Moayyed
* @author Phillip Webb
* @author Henrique (henriquejsza)
*/
class MessageSourceAutoConfigurationTests {
@@ -65,6 +70,44 @@ class MessageSourceAutoConfigurationTests {
.isEqualTo("Foo message"));
}
@Test
void resourceBasedMessageSourceConfigurerIsAvailableWithoutResourceBundle() {
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(ResourceBasedMessageSourceConfigurer.class)
.doesNotHaveBean(ResourceBundleMessageSource.class));
}
@Test
void resourceBasedMessageSourceConfigurerCanConfigureUserDefinedMessageSource() {
this.contextRunner.withUserConfiguration(CustomResourceBasedMessageSourceConfiguration.class)
.withPropertyValues("spring.messages.cache-duration=10s", "spring.messages.fallback-to-system-locale=false",
"spring.messages.always-use-message-format=true",
"spring.messages.use-code-as-default-message=true")
.run((context) -> {
assertThat(context).hasSingleBean(ResourceBasedMessageSourceConfigurer.class)
.hasSingleBean(ReloadableResourceBundleMessageSource.class);
assertThat(context.getBean(ReloadableResourceBundleMessageSource.class))
.hasFieldOrPropertyWithValue("cacheMillis", 10_000L)
.hasFieldOrPropertyWithValue("fallbackToSystemLocale", false)
.hasFieldOrPropertyWithValue("alwaysUseMessageFormat", true)
.hasFieldOrPropertyWithValue("useCodeAsDefaultMessage", true);
});
}
@Test
@WithTestMessagesPropertiesResource
void autoConfiguredMessageSourceUsesUserDefinedConfigurer() {
ResourceBasedMessageSourceConfigurer configurer = mock(ResourceBasedMessageSourceConfigurer.class);
this.contextRunner.withBean(ResourceBasedMessageSourceConfigurer.class, () -> configurer)
.withPropertyValues("spring.messages.basename=test/messages")
.run((context) -> {
assertThat(context).hasSingleBean(ResourceBasedMessageSourceConfigurer.class)
.hasSingleBean(ResourceBundleMessageSource.class);
assertThat(context.getBean(ResourceBasedMessageSourceConfigurer.class)).isSameAs(configurer);
then(configurer).should().configure(context.getBean(ResourceBundleMessageSource.class));
});
}
@Test
@WithTestMessagesPropertiesResource
void propertiesBundleWithSlashIsDetected() {
@@ -256,6 +299,18 @@ class MessageSourceAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomResourceBasedMessageSourceConfiguration {
@Bean
ReloadableResourceBundleMessageSource messageSource(ResourceBasedMessageSourceConfigurer configurer) {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
configurer.configure(messageSource);
return messageSource;
}
}
@Configuration(proxyBeanMethods = false)
static class CustomBeanNameMessageSourceConfiguration {
@@ -23,3 +23,5 @@ TIP: The configprop:spring.messages.basename[] property supports a list of locat
The configprop:spring.messages.common-messages[] property supports a list of property file resources.
See javadoc:org.springframework.boot.autoconfigure.context.MessageSourceProperties[] for more supported options.
If you need to use a different implementation of javadoc:org.springframework.context.support.AbstractResourceBasedMessageSource[], Spring Boot provides a javadoc:org.springframework.boot.autoconfigure.context.ResourceBasedMessageSourceConfigurer[] bean that you can use to apply the same settings as the auto-configured message source.