diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc index 0f3d79bc71..1ead038524 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc @@ -3,11 +3,18 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-message-codecs[See equivalent in the Reactive stack]# -You can customize `HttpMessageConverter` in Java configuration by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`] -(to replace the default converters created by Spring MVC) or by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`] -(to customize the default converters or add additional converters to the default ones). +You can set the `HttpMessageConverter` instances to use in Java configuration, +replacing the ones used by default, by overriding +{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]. +You can also customize the list of configured message converters at the end by overriding +{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. + +TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any +`HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a +Boot application, prefer to use the +https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html[HttpMessageConverters] +mechanism. Or alternatively, use `extendMessageConverters` to modify message converters +at the end. The following example adds XML and Jackson JSON converters with a customized `ObjectMapper` instead of the default ones: diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 090ec54859..2e9178c787 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -389,31 +389,30 @@ public final class HttpRequestValues { Map uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap()); Object bodyValue = this.bodyValue; + if (this.multipartBuilder != null) { + Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request parts, not both"); + bodyValue = this.multipartBuilder.build(); + } if (!CollectionUtils.isEmpty(this.requestParams)) { - - boolean isFormData = (this.headers != null && - MediaType.APPLICATION_FORM_URLENCODED.equals(this.headers.getContentType())); - - if (isFormData) { - Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request params, not both"); + if (hasContentType(MediaType.APPLICATION_FORM_URLENCODED)) { + Assert.isTrue(this.multipartBuilder == null, "Cannot add parts to form data request"); + Assert.isTrue(bodyValue == null && this.body == null, "Cannot set body of form data request"); bodyValue = new LinkedMultiValueMap<>(this.requestParams); } else if (uri != null) { + // insert into prepared URI uri = UriComponentsBuilder.fromUri(uri) .queryParams(UriUtils.encodeQueryParams(this.requestParams)) .build(true) .toUri(); } else { + // append to URI template uriVars = (uriVars.isEmpty() ? new HashMap<>() : uriVars); uriTemplate = appendQueryParams(uriTemplate, uriVars, this.requestParams); } } - else if (this.multipartBuilder != null) { - Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request parts, not both"); - bodyValue = this.multipartBuilder.build(); - } HttpHeaders headers = HttpHeaders.EMPTY; if (this.headers != null) { @@ -432,6 +431,10 @@ public final class HttpRequestValues { bodyValue, this.body, this.bodyElementType); } + private boolean hasContentType(MediaType mediaType) { + return (this.headers != null && mediaType.equals(this.headers.getContentType())); + } + private String appendQueryParams( String uriTemplate, Map uriVars, MultiValueMap requestParams) { diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java index 46c7c32080..39aa9e6ac8 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java @@ -50,7 +50,7 @@ class HttpRequestValuesTests { @ParameterizedTest @ValueSource(strings = {"POST", "PUT", "PATCH"}) @SuppressWarnings("unchecked") - void requestParamAsFormData(String httpMethod) { + void formData(String httpMethod) { HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.valueOf(httpMethod)) .setContentType(MediaType.APPLICATION_FORM_URLENCODED) @@ -65,7 +65,7 @@ class HttpRequestValuesTests { } @Test - void requestParamAsQueryParamsInUriTemplate() { + void queryParamsWithUriTemplate() { HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST) .setUriTemplate("/path") @@ -99,23 +99,25 @@ class HttpRequestValuesTests { } @Test - void requestParamAsQueryParamsInUri() { + void queryParamsWithPreparedUri() { + + URI uri = URI.create("/my%20path"); HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST) - .setUri(URI.create("/path")) + .setUri(uri) .addRequestParameter("param1", "1st value") .addRequestParameter("param2", "2nd value A", "2nd value B") .build(); assertThat(requestValues.getUri().toString()) - .isEqualTo("/path?param1=1st%20value¶m2=2nd%20value%20A¶m2=2nd%20value%20B"); + .isEqualTo("/my%20path?param1=1st%20value¶m2=2nd%20value%20A¶m2=2nd%20value%20B"); } @Test void requestPart() { - HttpHeaders entityHeaders = new HttpHeaders(); - entityHeaders.add("foo", "bar"); - HttpEntity entity = new HttpEntity<>("body", entityHeaders); + HttpHeaders headers = new HttpHeaders(); + headers.add("foo", "bar"); + HttpEntity entity = new HttpEntity<>("body", headers); HttpRequestValues requestValues = HttpRequestValues.builder() .addRequestPart("form field", "form value") @@ -129,4 +131,24 @@ class HttpRequestValuesTests { assertThat(map.getFirst("entity")).isEqualTo(entity); } + @Test + void requestPartAndRequestParam() { + + HttpRequestValues requestValues = HttpRequestValues.builder() + .setUriTemplate("/path") + .addRequestPart("form field", "form value") + .addRequestParameter("query param", "query value") + .build(); + + String uriTemplate = requestValues.getUriTemplate(); + assertThat(uriTemplate).isNotNull(); + + assertThat(uriTemplate).isEqualTo("/path?{queryParam0}={queryParam0[0]}"); + + @SuppressWarnings("unchecked") + MultiValueMap> map = (MultiValueMap>) requestValues.getBodyValue(); + assertThat(map).hasSize(1); + assertThat(map.getFirst("form field").getBody()).isEqualTo("form value"); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java index d9fb705de5..a963cee8d7 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java @@ -65,9 +65,9 @@ class RequestPartArgumentResolverTests { Object body = this.client.getRequestValues().getBodyValue(); assertThat(body).isInstanceOf(MultiValueMap.class); + @SuppressWarnings("unchecked") MultiValueMap> map = (MultiValueMap>) body; - assertThat(map.getFirst("part1").getBody()).isEqualTo("part 1"); assertThat(map.getFirst("part2")).isEqualTo(part2); assertThat(((Mono) map.getFirst("part3").getBody()).block()).isEqualTo("part 3"); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index 15df671c0c..916b868521 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -175,7 +175,7 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa } // For ProblemDetail, fall back on RFC 7807 format - if (bestMediaType == null && elementType.toClass().equals(ProblemDetail.class)) { + if (bestMediaType == null && ProblemDetail.class.isAssignableFrom(elementType.toClass())) { bestMediaType = selectMediaType(exchange, () -> getMediaTypesFor(elementType), this.problemMediaTypes); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 6cd9596a04..6c1de10a36 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -139,9 +139,9 @@ public class ResponseBodyResultHandlerTests { } private void testProblemDetailMediaType(MockServerWebExchange exchange, MediaType expectedMediaType) { - ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + MyProblemDetail problemDetail = new MyProblemDetail(HttpStatus.BAD_REQUEST); - Method method = on(TestRestController.class).returning(ProblemDetail.class).resolveMethod(); + Method method = on(TestRestController.class).returning(MyProblemDetail.class).resolveMethod(); HandlerResult result = getHandlerResult(new TestRestController(), problemDetail, method); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); @@ -196,7 +196,7 @@ public class ResponseBodyResultHandlerTests { return null; } - public ProblemDetail handleToProblemDetail() { + public MyProblemDetail handleToProblemDetail() { return null; } @@ -217,4 +217,13 @@ public class ResponseBodyResultHandlerTests { } } + + private static class MyProblemDetail extends ProblemDetail { + + public MyProblemDetail(HttpStatus status) { + super(status.value()); + } + + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 8055d62e25..eb329f47e0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -166,10 +166,14 @@ public interface WebMvcConfigurer { *

By default, all built-in converters are configured as long as the * corresponding 3rd party libraries such Jackson JSON, JAXB2, and others * are present on the classpath. - *

Note use of this method turns off default converter - * registration. Alternatively, use - * {@link #extendMessageConverters(java.util.List)} to modify that default - * list of converters. + *

Note that use of this method turns off default converter + * registration. However, in a Spring Boot application the + * {@code WebMvcAutoConfiguration} adds any {@code HttpMessageConverter} + * beans as well as default converters. Hence, in a Boot application use + * HttpMessageConverters. + * Alternatively, for any scenario, use + * {@link #extendMessageConverters(java.util.List)} to modify the configured + * list of message converters. * @param converters initially an empty list of converters */ default void configureMessageConverters(List> converters) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 1f7406353a..1f85b4c1f6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -1,4 +1,4 @@ -/* +`/* * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -374,7 +374,7 @@ class RequestResponseBodyMethodProcessorTests { } private void testProblemDetailMediaType(String expectedContentType) throws Exception { - ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + MyProblemDetail problemDetail = new MyProblemDetail(HttpStatus.BAD_REQUEST); this.servletRequest.setRequestURI("/path"); @@ -805,7 +805,7 @@ class RequestResponseBodyMethodProcessorTests { } @SuppressWarnings("ConstantConditions") - ProblemDetail handleAndReturnProblemDetail() { + MyProblemDetail handleAndReturnProblemDetail() { return null; } @@ -821,6 +821,15 @@ class RequestResponseBodyMethodProcessorTests { } + private static class MyProblemDetail extends ProblemDetail { + + public MyProblemDetail(HttpStatus status) { + super(status.value()); + } + + } + + private static abstract class MyParameterizedController { @SuppressWarnings("unused")