diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java index 942dffed95c..bf0d706849f 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java @@ -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 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 { diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/ResourceBasedMessageSourceConfigurer.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/ResourceBasedMessageSourceConfigurer.java new file mode 100644 index 00000000000..f16d97c426f --- /dev/null +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/ResourceBasedMessageSourceConfigurer.java @@ -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. + *

+ * 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 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; + } + +} diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java index b3b3e9466ea..4b035cf35b5 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java @@ -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 { diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc index a0e15b4862c..79c0758298b 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc @@ -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.