Set embeddedValueResolver on WebConversionService

Closes gh-8923
This commit is contained in:
Moritz Halbritter
2026-04-02 08:57:22 +02:00
committed by Moritz Halbritter
parent d04cb90d19
commit a04b65ea74
6 changed files with 103 additions and 5 deletions
@@ -31,6 +31,7 @@ import org.springframework.format.number.money.Jsr354NumberFormatAnnotationForma
import org.springframework.format.number.money.MonetaryAmountFormatter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringValueResolver;
/**
* {@link org.springframework.format.support.FormattingConversionService} dedicated to web
@@ -53,7 +54,23 @@ public class WebConversionService extends DefaultFormattingConversionService {
* @since 2.3.0
*/
public WebConversionService(DateTimeFormatters dateTimeFormatters) {
super(false);
this(null, dateTimeFormatters);
}
/**
* Create a new WebConversionService that configures formatters with the provided
* date, time, and date-time formats, or registers the default if no custom format is
* provided. The given {@code embeddedValueResolver} is used to resolve embedded
* values such as property placeholders in
* {@link org.springframework.format.annotation.DateTimeFormat#pattern()} patterns.
* @param embeddedValueResolver the embedded value resolver to use, or {@code null}
* @param dateTimeFormatters the formatters to use for date, time, and date-time
* formatting
* @since 4.1.0
*/
public WebConversionService(@Nullable StringValueResolver embeddedValueResolver,
DateTimeFormatters dateTimeFormatters) {
super(embeddedValueResolver, false);
if (dateTimeFormatters.isCustomized()) {
addFormatters(dateTimeFormatters);
}
@@ -29,8 +29,12 @@ import java.time.format.FormatStyle;
import java.util.Calendar;
import java.util.Date;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.annotation.DateTimeFormat;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -178,10 +182,27 @@ class WebConversionServiceTests {
assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isOne();
}
@Test
void embeddedValueResolverIsAppliedToAnnotationPattern() throws Exception {
WebConversionService conversionService = new WebConversionService(
(value) -> value.replace("${my.date.format}", "yyyy-MM-dd"), new DateTimeFormatters());
TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date"));
Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant());
assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class)))
.isEqualTo("2000-01-02");
}
private void customDateFormat(Object input) {
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters().dateFormat("dd*MM*yyyy"));
assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018");
}
static class FormattedDate {
@DateTimeFormat(pattern = "${my.date.format}")
@Nullable Date date;
}
}
@@ -59,6 +59,7 @@ import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversi
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format;
import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -73,6 +74,7 @@ import org.springframework.http.CacheControl;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.validation.Validator;
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
@@ -312,7 +314,8 @@ public final class WebFluxAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ WebProperties.class, ServerProperties.class })
@ImportRuntimeHints(WebFluxValidatorRuntimeHints.class)
static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration {
static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration
implements EmbeddedValueResolverAware {
private final WebFluxProperties webFluxProperties;
@@ -322,6 +325,8 @@ public final class WebFluxAutoConfiguration {
private final @Nullable WebFluxRegistrations webFluxRegistrations;
private @Nullable StringValueResolver embeddedValueResolver;
EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties,
ServerProperties serverProperties, ObjectProvider<WebFluxRegistrations> webFluxRegistrations) {
this.webFluxProperties = webFluxProperties;
@@ -334,7 +339,7 @@ public final class WebFluxAutoConfiguration {
@Override
public FormattingConversionService webFluxConversionService() {
Format format = this.webFluxProperties.getFormat();
WebConversionService conversionService = new WebConversionService(
WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver,
new DateTimeFormatters().dateFormat(format.getDate())
.timeFormat(format.getTime())
.dateTimeFormat(format.getDateTime()));
@@ -342,6 +347,11 @@ public final class WebFluxAutoConfiguration {
return conversionService;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Bean
@Override
public Validator webFluxValidator() {
@@ -74,10 +74,12 @@ import org.springframework.context.i18n.LocaleContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseCookie;
@@ -340,6 +342,17 @@ class WebFluxAutoConfigurationTests {
});
}
@Test
void embeddedValueResolverIsAppliedToConversionService() throws Exception {
this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date"));
Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant());
assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class)))
.isEqualTo("2000-01-02");
});
}
@Test
void validatorWhenNoValidatorShouldUseDefault() {
this.contextRunner.run((context) -> {
@@ -1333,4 +1346,11 @@ class WebFluxAutoConfigurationTests {
}
static class FormattedDate {
@DateTimeFormat(pattern = "${my.date.format}")
@Nullable Date date;
}
}
@@ -67,6 +67,7 @@ import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use;
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format;
import org.springframework.context.ApplicationContext;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -87,6 +88,7 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
@@ -438,7 +440,8 @@ public final class WebMvcAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
@ImportRuntimeHints(MvcValidatorRuntimeHints.class)
static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration
implements ResourceLoaderAware, EmbeddedValueResolverAware {
private final Resources resourceProperties;
@@ -450,6 +453,8 @@ public final class WebMvcAutoConfiguration {
private final @Nullable WebMvcRegistrations mvcRegistrations;
private @Nullable StringValueResolver embeddedValueResolver;
@SuppressWarnings("NullAway.Init")
private ResourceLoader resourceLoader;
@@ -566,7 +571,7 @@ public final class WebMvcAutoConfiguration {
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(
WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver,
new DateTimeFormatters().dateFormat(format.getDate())
.timeFormat(format.getTime())
.dateTimeFormat(format.getDateTime()));
@@ -574,6 +579,11 @@ public final class WebMvcAutoConfiguration {
return conversionService;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Bean
@Override
public Validator mvcValidator() {
@@ -78,11 +78,13 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
@@ -509,6 +511,17 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void embeddedValueResolverIsAppliedToConversionService() throws Exception {
this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date"));
Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant());
assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class)))
.isEqualTo("2000-01-02");
});
}
@Test
void noMessageCodesResolver() {
this.contextRunner.run(
@@ -1756,4 +1769,11 @@ class WebMvcAutoConfigurationTests {
}
static class FormattedDate {
@DateTimeFormat(pattern = "${my.date.format}")
@Nullable Date date;
}
}