Provide config properties for configuring WebFlux's locale resolution

Previously, the locale context resolver used with WebFlux could only be
configured by provided a custom LocaleContextResolver bean. By
constrast, when using Spring MVC, the spring.mvc.locale and
spring.mvc.locale-resolver properties could be used to configure the
locale and the resolver (fixed or Accept header) respectively.

This commit introduces spring.web.locale and spring.web.locale-resolver
properties and deprecates their spring.mvc equivalents. The new
properties can be used to configure locale resolution with either
Spring MVC or WebFlux.

Closes gh-23449
This commit is contained in:
Andy Wilkinson 2020-10-27 10:43:29 +00:00
parent ef89eb6dfb
commit 1c4b4cb0cd
7 changed files with 184 additions and 19 deletions

View File

@ -72,7 +72,7 @@ public class DocumentConfigurationProperties extends DefaultTask {
.addSection("mail").withKeyPrefixes("spring.mail", "spring.sendgrid").addSection("cache")
.withKeyPrefixes("spring.cache").addSection("server").withKeyPrefixes("server").addSection("web")
.withKeyPrefixes("spring.hateoas", "spring.http", "spring.servlet", "spring.jersey", "spring.mvc",
"spring.resources", "spring.webflux")
"spring.resources", "spring.web", "spring.webflux")
.addSection("json").withKeyPrefixes("spring.jackson", "spring.gson").addSection("rsocket")
.withKeyPrefixes("spring.rsocket").addSection("templating")
.withKeyPrefixes("spring.freemarker", "spring.groovy", "spring.mustache", "spring.thymeleaf")

View File

@ -0,0 +1,74 @@
/*
* Copyright 2012-2020 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.web;
import java.util.Locale;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties Configuration properties} for general web concerns.
*
* @author Andy Wilkinson
* @since 2.4.0
*/
@ConfigurationProperties("spring.web")
public class WebProperties {
/**
* Locale to use. By default, this locale is overridden by the "Accept-Language"
* header.
*/
private Locale locale;
/**
* Define how the locale should be resolved.
*/
private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
public Locale getLocale() {
return this.locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
public LocaleResolver getLocaleResolver() {
return this.localeResolver;
}
public void setLocaleResolver(LocaleResolver localeResolver) {
this.localeResolver = localeResolver;
}
public enum LocaleResolver {
/**
* Always use the configured locale.
*/
FIXED,
/**
* Use the "Accept-Language" header or the configured locale if the header is not
* set.
*/
ACCEPT_HEADER
}
}

View File

@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigura
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format;
@ -69,6 +70,8 @@ import org.springframework.web.reactive.result.method.annotation.ArgumentResolve
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
/**
@ -216,15 +219,19 @@ public class WebFluxAutoConfiguration {
* Configuration equivalent to {@code @EnableWebFlux}.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration {
private final WebFluxProperties webFluxProperties;
private final WebProperties webProperties;
private final WebFluxRegistrations webFluxRegistrations;
public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties,
public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties,
ObjectProvider<WebFluxRegistrations> webFluxRegistrations) {
this.webFluxProperties = webFluxProperties;
this.webProperties = webProperties;
this.webFluxRegistrations = webFluxRegistrations.getIfUnique();
}
@ -269,7 +276,12 @@ public class WebFluxAutoConfiguration {
@Override
@ConditionalOnMissingBean
public LocaleContextResolver localeContextResolver() {
return super.localeContextResolver();
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleContextResolver(this.webProperties.getLocale());
}
AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
localeContextResolver.setDefaultLocale(this.webProperties.getLocale());
return localeContextResolver;
}
}

View File

@ -20,6 +20,7 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@ -51,6 +52,7 @@ import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format;
@ -358,23 +360,27 @@ public class WebMvcAutoConfiguration {
* Configuration equivalent to {@code @EnableWebMvc}.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final WebProperties webProperties;
private final ListableBeanFactory beanFactory;
private final WebMvcRegistrations mvcRegistrations;
private ResourceLoader resourceLoader;
public EnableWebMvcConfiguration(ResourceProperties resourceProperties,
ObjectProvider<WebMvcProperties> mvcPropertiesProvider,
ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) {
public EnableWebMvcConfiguration(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
WebProperties webProperties, ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
ListableBeanFactory beanFactory) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcPropertiesProvider.getIfAvailable();
this.mvcProperties = mvcProperties;
this.webProperties = webProperties;
this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
this.beanFactory = beanFactory;
}
@ -426,13 +432,18 @@ public class WebMvcAutoConfiguration {
@Override
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale", matchIfMissing = true)
@SuppressWarnings("deprecation")
public LocaleResolver localeResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.webProperties.getLocale());
}
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale()
: this.mvcProperties.getLocale();
localeResolver.setDefaultLocale(locale);
return localeResolver;
}

View File

@ -121,6 +121,8 @@ public class WebMvcProperties {
this.messageCodesResolverFormat = messageCodesResolverFormat;
}
@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.web.locale")
public Locale getLocale() {
return this.locale;
}
@ -129,6 +131,8 @@ public class WebMvcProperties {
this.locale = locale;
}
@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.web.locale-resolver")
public LocaleResolver getLocaleResolver() {
return this.localeResolver;
}
@ -543,6 +547,12 @@ public class WebMvcProperties {
}
/**
* Locale resolution options.
* @deprecated since 2.4.0 in favor of
* {@link org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver}
*/
@Deprecated
public enum LocaleResolver {
/**

View File

@ -44,6 +44,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.ConversionService;
@ -54,7 +55,10 @@ import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
@ -74,6 +78,7 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.util.pattern.PathPattern;
@ -454,6 +459,54 @@ class WebFluxAutoConfigurationTests {
});
}
@Test
void defaultLocaleContextResolver() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(LocaleContextResolver.class);
LocaleContextResolver resolver = context.getBean(LocaleContextResolver.class);
assertThat(((AcceptHeaderLocaleContextResolver) resolver).getDefaultLocale()).isNull();
});
}
@Test
void whenFixedLocalContextResolverIsUsedThenAcceptLanguagesHeaderIsIgnored() {
this.contextRunner.withPropertyValues("spring.web.locale:en_UK", "spring.web.locale-resolver=fixed")
.run((context) -> {
MockServerHttpRequest request = MockServerHttpRequest.get("/")
.acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class);
assertThat(localeContextResolver).isInstanceOf(FixedLocaleContextResolver.class);
LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange);
assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK"));
});
}
@Test
void whenAcceptHeaderLocaleContextResolverIsUsedThenAcceptLanguagesHeaderIsHonoured() {
this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> {
MockServerHttpRequest request = MockServerHttpRequest.get("/")
.acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class);
assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class);
LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange);
assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("nl_NL"));
});
}
@Test
void whenAcceptHeaderLocaleContextResolverIsUsedAndHeaderIsAbsentThenConfiguredLocaleIsUsed() {
this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> {
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class);
assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class);
LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange);
assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK"));
});
}
@Test
void customLocaleContextResolver() {
this.contextRunner.withUserConfiguration(LocaleContextResolverConfiguration.class)

View File

@ -38,6 +38,8 @@ import javax.servlet.http.HttpServletResponse;
import javax.validation.ValidatorFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
@ -289,10 +291,11 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void overrideLocale() {
this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK", "spring.mvc.locale-resolver=fixed")
.run((loader) -> {
@ParameterizedTest
@ValueSource(strings = { "mvc", "web" })
void overrideLocale(String mvcOrWeb) {
this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK",
"spring." + mvcOrWeb + ".locale-resolver=fixed").run((loader) -> {
// mock request and set user preferred locale
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(StringUtils.parseLocaleString("nl_NL"));
@ -306,9 +309,10 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void useAcceptHeaderLocale() {
this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK").run((loader) -> {
@ParameterizedTest
@ValueSource(strings = { "mvc", "web" })
void useAcceptHeaderLocale(String mvcOrWeb) {
this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK").run((loader) -> {
// mock request and set user preferred locale
MockHttpServletRequest request = new MockHttpServletRequest();
request.addPreferredLocale(StringUtils.parseLocaleString("nl_NL"));
@ -321,9 +325,10 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void useDefaultLocaleIfAcceptHeaderNoSet() {
this.contextRunner.withPropertyValues("spring.mvc.locale:en_UK").run((context) -> {
@ParameterizedTest
@ValueSource(strings = { "mvc", "web" })
void useDefaultLocaleIfAcceptHeaderNoSet(String mvcOrWeb) {
this.contextRunner.withPropertyValues("spring." + mvcOrWeb + ".locale:en_UK").run((context) -> {
// mock request and set user preferred locale
MockHttpServletRequest request = new MockHttpServletRequest();
LocaleResolver localeResolver = context.getBean(LocaleResolver.class);