Skip to content

Commit d0cd7af

Browse files
committed
Introduce hints support in advices
This commit introduces RequestBodyAdvice#determineReadHints and ResponseBodyAdvice#determineWriteHints in order to be able to support SmartHttpMessageConverter hints, as well as related `@JsonView` support. See gh-33798
1 parent 71987a8 commit d0cd7af

File tree

9 files changed

+213
-28
lines changed

9 files changed

+213
-28
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.core.MethodParameter;
2222
import org.springframework.http.MediaType;
23+
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
2324
import org.springframework.http.converter.HttpMessageConverter;
2425
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
2526
import org.springframework.http.converter.json.MappingJacksonValue;
@@ -39,7 +40,8 @@ public abstract class AbstractMappingJacksonResponseBodyAdvice implements Respon
3940

4041
@Override
4142
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
42-
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
43+
return AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType) ||
44+
AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
4345
}
4446

4547
@Override
@@ -50,6 +52,9 @@ public boolean supports(MethodParameter returnType, Class<? extends HttpMessageC
5052
if (body == null) {
5153
return null;
5254
}
55+
if (AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType)) {
56+
return body;
57+
}
5358
MappingJacksonValue container = getOrCreateContainer(body);
5459
beforeBodyWriteInternal(container, contentType, returnType, request, response);
5560
return container;

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -175,7 +175,7 @@ RequestResponseBodyAdviceChain getAdvice() {
175175
ResolvableType targetResolvableType = null;
176176
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
177177
for (HttpMessageConverter<?> converter : this.messageConverters) {
178-
Class<HttpMessageConverter<?>> converterClass = (Class<HttpMessageConverter<?>>) converter.getClass();
178+
Class<? extends HttpMessageConverter<?>> converterClass = (Class<? extends HttpMessageConverter<?>>) converter.getClass();
179179
ConverterType converterTypeToUse = null;
180180
if (converter instanceof GenericHttpMessageConverter<?> genericConverter) {
181181
if (genericConverter.canRead(targetType, contextClass, contentType)) {
@@ -195,25 +195,25 @@ else if (targetClass != null && converter.canRead(targetClass, contentType)) {
195195
}
196196
if (converterTypeToUse != null) {
197197
if (message.hasBody()) {
198-
HttpInputMessage msgToUse =
199-
getAdvice().beforeBodyRead(message, parameter, targetType, converterClass);
198+
HttpInputMessage msgToUse = this.advice.beforeBodyRead(message, parameter, targetType, converterClass);
200199
body = switch (converterTypeToUse) {
201200
case BASE -> ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse);
202201
case GENERIC -> ((GenericHttpMessageConverter<?>) converter).read(targetType, contextClass, msgToUse);
203-
case SMART -> ((SmartHttpMessageConverter<?>) converter).read(targetResolvableType, msgToUse, null);
202+
case SMART -> ((SmartHttpMessageConverter<?>) converter).read(targetResolvableType, msgToUse,
203+
this.advice.determineReadHints(parameter, targetType, (Class<SmartHttpMessageConverter<?>>) converterClass));
204204
};
205-
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterClass);
205+
body = this.advice.afterBodyRead(body, msgToUse, parameter, targetType, converterClass);
206206
}
207207
else {
208-
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterClass);
208+
body = this.advice.handleEmptyBody(null, message, parameter, targetType, converterClass);
209209
}
210210
break;
211211
}
212212

213213
}
214214

215215
if (body == NO_VALUE && noContentType && !message.hasBody()) {
216-
body = getAdvice().handleEmptyBody(
216+
body = this.advice.handleEmptyBody(
217217
null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
218218
}
219219
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -324,7 +324,7 @@ else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
324324
}
325325
}
326326
else if (converter instanceof SmartHttpMessageConverter smartConverter) {
327-
targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forMethodParameter(returnType));
327+
targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forType(targetType));
328328
if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) {
329329
converterTypeToUse = ConverterType.SMART;
330330
}
@@ -343,7 +343,9 @@ else if (converter.canWrite(valueType, selectedMediaType)){
343343
switch (converterTypeToUse) {
344344
case BASE -> converter.write(body, selectedMediaType, outputMessage);
345345
case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage);
346-
case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, selectedMediaType, outputMessage, null);
346+
case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType,
347+
selectedMediaType, outputMessage, getAdvice().determineWriteHints(body, returnType,
348+
selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass()));
347349
}
348350
}
349351
else {

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,12 +18,17 @@
1818

1919
import java.io.IOException;
2020
import java.lang.reflect.Type;
21+
import java.util.Collections;
22+
import java.util.Map;
2123

2224
import com.fasterxml.jackson.annotation.JsonView;
25+
import org.jspecify.annotations.Nullable;
2326

2427
import org.springframework.core.MethodParameter;
2528
import org.springframework.http.HttpInputMessage;
29+
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
2630
import org.springframework.http.converter.HttpMessageConverter;
31+
import org.springframework.http.converter.SmartHttpMessageConverter;
2732
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
2833
import org.springframework.http.converter.json.MappingJacksonInputMessage;
2934
import org.springframework.util.Assert;
@@ -52,14 +57,28 @@ public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter {
5257
public boolean supports(MethodParameter methodParameter, Type targetType,
5358
Class<? extends HttpMessageConverter<?>> converterType) {
5459

55-
return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) &&
56-
methodParameter.getParameterAnnotation(JsonView.class) != null);
60+
return methodParameter.getParameterAnnotation(JsonView.class) != null &&
61+
(AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType) ||
62+
AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType));
5763
}
5864

5965
@Override
6066
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter,
6167
Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException {
6268

69+
if (AbstractJacksonHttpMessageConverter.class.isAssignableFrom(selectedConverterType)) {
70+
return inputMessage;
71+
}
72+
73+
return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), getJsonView(methodParameter));
74+
}
75+
76+
@Override
77+
public @Nullable Map<String, Object> determineReadHints(MethodParameter parameter, Type targetType, Class<? extends SmartHttpMessageConverter<?>> converterType) {
78+
return Collections.singletonMap(JsonView.class.getName(), getJsonView(parameter));
79+
}
80+
81+
private static Class<?> getJsonView(MethodParameter methodParameter) {
6382
JsonView ann = methodParameter.getParameterAnnotation(JsonView.class);
6483
Assert.state(ann != null, "No JsonView annotation");
6584

@@ -68,8 +87,6 @@ public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodPara
6887
throw new IllegalArgumentException(
6988
"@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter);
7089
}
71-
72-
return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]);
90+
return classes[0];
7391
}
74-
7592
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,11 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.util.Collections;
20+
import java.util.Map;
21+
1922
import com.fasterxml.jackson.annotation.JsonView;
23+
import org.jspecify.annotations.Nullable;
2024

2125
import org.springframework.core.MethodParameter;
2226
import org.springframework.http.MediaType;
@@ -55,6 +59,15 @@ public boolean supports(MethodParameter returnType, Class<? extends HttpMessageC
5559
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
5660
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
5761

62+
bodyContainer.setSerializationView(getJsonView(returnType));
63+
}
64+
65+
@Override
66+
public @Nullable Map<String, Object> determineWriteHints(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType) {
67+
return Collections.singletonMap(JsonView.class.getName(), getJsonView(returnType));
68+
}
69+
70+
private static Class<?> getJsonView(MethodParameter returnType) {
5871
JsonView ann = returnType.getMethodAnnotation(JsonView.class);
5972
Assert.state(ann != null, "No JsonView annotation");
6073

@@ -63,8 +76,6 @@ protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaT
6376
throw new IllegalArgumentException(
6477
"@JsonView only supported for response body advice with exactly 1 class argument: " + returnType);
6578
}
66-
67-
bodyContainer.setSerializationView(classes[0]);
79+
return classes[0];
6880
}
69-
7081
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,12 +18,14 @@
1818

1919
import java.io.IOException;
2020
import java.lang.reflect.Type;
21+
import java.util.Map;
2122

2223
import org.jspecify.annotations.Nullable;
2324

2425
import org.springframework.core.MethodParameter;
2526
import org.springframework.http.HttpInputMessage;
2627
import org.springframework.http.converter.HttpMessageConverter;
28+
import org.springframework.http.converter.SmartHttpMessageConverter;
2729

2830
/**
2931
* Allows customizing the request before its body is read and converted into an
@@ -36,6 +38,7 @@
3638
* {@code @ControllerAdvice} in which case they are auto-detected.
3739
*
3840
* @author Rossen Stoyanchev
41+
* @author Sebastien Deleuze
3942
* @since 4.2
4043
*/
4144
public interface RequestBodyAdvice {
@@ -63,6 +66,20 @@ boolean supports(MethodParameter methodParameter, Type targetType,
6366
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
6467
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
6568

69+
/**
70+
* Invoked to determine read hints if the converter is a {@link SmartHttpMessageConverter}.
71+
* @param parameter the target method parameter
72+
* @param targetType the target type, not necessarily the same as the method
73+
* parameter type, for example, for {@code HttpEntity<String>}.
74+
* @param converterType the selected converter type
75+
* @return the hints determined otherwise {@code null}
76+
* @since 7.0
77+
*/
78+
default @Nullable Map<String, Object> determineReadHints(MethodParameter parameter,
79+
Type targetType, Class<? extends SmartHttpMessageConverter<?>> converterType) {
80+
return null;
81+
}
82+
6683
/**
6784
* Invoked third (and last) after the request body is converted to an Object.
6885
* @param body set to the converter Object before the first advice is called
@@ -90,5 +107,4 @@ Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter
90107
@Nullable Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
91108
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
92109

93-
94110
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,14 +20,17 @@
2020
import java.lang.reflect.Type;
2121
import java.util.ArrayList;
2222
import java.util.Collections;
23+
import java.util.HashMap;
2324
import java.util.List;
25+
import java.util.Map;
2426

2527
import org.jspecify.annotations.Nullable;
2628

2729
import org.springframework.core.MethodParameter;
2830
import org.springframework.http.HttpInputMessage;
2931
import org.springframework.http.MediaType;
3032
import org.springframework.http.converter.HttpMessageConverter;
33+
import org.springframework.http.converter.SmartHttpMessageConverter;
3134
import org.springframework.http.server.ServerHttpRequest;
3235
import org.springframework.http.server.ServerHttpResponse;
3336
import org.springframework.util.CollectionUtils;
@@ -36,7 +39,7 @@
3639
/**
3740
* Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each
3841
* instance may be (and is most likely) wrapped with
39-
* {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
42+
* {@link ControllerAdviceBean ControllerAdviceBean}.
4043
*
4144
* @author Rossen Stoyanchev
4245
* @since 4.2
@@ -176,4 +179,38 @@ else if (ResponseBodyAdvice.class == adviceType) {
176179
}
177180
}
178181

182+
@Override
183+
public @Nullable Map<String, Object> determineReadHints(MethodParameter parameter, Type targetType, Class<? extends SmartHttpMessageConverter<?>> converterType) {
184+
Map<String, Object> hints = null;
185+
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
186+
if (advice.supports(parameter, targetType, converterType)) {
187+
Map<String, Object> adviceHints = advice.determineReadHints(parameter, targetType, converterType);
188+
if (adviceHints != null) {
189+
if (hints == null) {
190+
hints = new HashMap<>(adviceHints.size());
191+
}
192+
hints.putAll(adviceHints);
193+
}
194+
}
195+
}
196+
return hints;
197+
}
198+
199+
@Override
200+
@SuppressWarnings("unchecked")
201+
public @Nullable Map<String, Object> determineWriteHints(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType) {
202+
Map<String, Object> hints = null;
203+
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
204+
if (advice.supports(returnType, selectedConverterType)) {
205+
Map<String, Object> adviceHints = ((ResponseBodyAdvice<Object>) advice).determineWriteHints(body, returnType, selectedContentType, selectedConverterType);
206+
if (adviceHints != null) {
207+
if (hints == null) {
208+
hints = new HashMap<>(adviceHints.size());
209+
}
210+
hints.putAll(adviceHints);
211+
}
212+
}
213+
}
214+
return hints;
215+
}
179216
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.util.Map;
20+
1921
import org.jspecify.annotations.Nullable;
2022

2123
import org.springframework.core.MethodParameter;
2224
import org.springframework.http.MediaType;
2325
import org.springframework.http.converter.HttpMessageConverter;
26+
import org.springframework.http.converter.SmartHttpMessageConverter;
2427
import org.springframework.http.server.ServerHttpRequest;
2528
import org.springframework.http.server.ServerHttpResponse;
2629

@@ -35,6 +38,7 @@
3538
* will be auto-detected by both.
3639
*
3740
* @author Rossen Stoyanchev
41+
* @author Sebastien Deleuze
3842
* @since 4.1
3943
* @param <T> the body type
4044
*/
@@ -65,4 +69,18 @@ public interface ResponseBodyAdvice<T> {
6569
Class<? extends HttpMessageConverter<?>> selectedConverterType,
6670
ServerHttpRequest request, ServerHttpResponse response);
6771

72+
/**
73+
* Invoked to determine write hints if the converter is a {@link SmartHttpMessageConverter}.
74+
* @param body the body to be written
75+
* @param returnType the return type of the controller method
76+
* @param selectedContentType the content type selected through content negotiation
77+
* @param selectedConverterType the converter type selected to write to the response
78+
* @return the hints determined otherwise {@code null}
79+
* @since 7.0
80+
*/
81+
default @Nullable Map<String, Object> determineWriteHints(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
82+
Class<? extends HttpMessageConverter<?>> selectedConverterType) {
83+
return null;
84+
}
85+
6886
}

0 commit comments

Comments
 (0)