diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java index 518d9b3f88e..144d48dd225 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -20,6 +20,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonValue; @@ -39,7 +40,8 @@ public abstract class AbstractMappingJacksonResponseBodyAdvice implements Respon @Override public boolean supports(MethodParameter returnType, Class> converterType) { - return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); + return AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType) || + AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } @Override @@ -50,6 +52,9 @@ public abstract class AbstractMappingJacksonResponseBodyAdvice implements Respon if (body == null) { return null; } + if (AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType)) { + return body; + } MappingJacksonValue container = getOrCreateContainer(body); beforeBodyWriteInternal(container, contentType, returnType, request, response); return container; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 4817cb7ece3..e0f54a924b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -175,7 +175,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements ResolvableType targetResolvableType = null; message = new EmptyBodyCheckingHttpInputMessage(inputMessage); for (HttpMessageConverter converter : this.messageConverters) { - Class> converterClass = (Class>) converter.getClass(); + Class> converterClass = (Class>) converter.getClass(); ConverterType converterTypeToUse = null; if (converter instanceof GenericHttpMessageConverter genericConverter) { if (genericConverter.canRead(targetType, contextClass, contentType)) { @@ -195,17 +195,17 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements } if (converterTypeToUse != null) { if (message.hasBody()) { - HttpInputMessage msgToUse = - getAdvice().beforeBodyRead(message, parameter, targetType, converterClass); + HttpInputMessage msgToUse = this.advice.beforeBodyRead(message, parameter, targetType, converterClass); body = switch (converterTypeToUse) { case BASE -> ((HttpMessageConverter) converter).read(targetClass, msgToUse); case GENERIC -> ((GenericHttpMessageConverter) converter).read(targetType, contextClass, msgToUse); - case SMART -> ((SmartHttpMessageConverter) converter).read(targetResolvableType, msgToUse, null); + case SMART -> ((SmartHttpMessageConverter) converter).read(targetResolvableType, msgToUse, + this.advice.determineReadHints(parameter, targetType, (Class>) converterClass)); }; - body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterClass); + body = this.advice.afterBodyRead(body, msgToUse, parameter, targetType, converterClass); } else { - body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterClass); + body = this.advice.handleEmptyBody(null, message, parameter, targetType, converterClass); } break; } @@ -213,7 +213,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements } if (body == NO_VALUE && noContentType && !message.hasBody()) { - body = getAdvice().handleEmptyBody( + body = this.advice.handleEmptyBody( null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 32084d33e5c..68b8e5e408c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -324,7 +324,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } else if (converter instanceof SmartHttpMessageConverter smartConverter) { - targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forMethodParameter(returnType)); + targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forType(targetType)); if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) { converterTypeToUse = ConverterType.SMART; } @@ -343,7 +343,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe switch (converterTypeToUse) { case BASE -> converter.write(body, selectedMediaType, outputMessage); case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage); - case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, selectedMediaType, outputMessage, null); + case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, + selectedMediaType, outputMessage, getAdvice().determineWriteHints(body, returnType, + selectedMediaType, (Class>) converter.getClass())); } } else { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java index d8a87952dba..a5c9e7befcd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2025 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. @@ -18,12 +18,17 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonView; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonInputMessage; import org.springframework.util.Assert; @@ -52,14 +57,28 @@ public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter { public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { - return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) && - methodParameter.getParameterAnnotation(JsonView.class) != null); + return methodParameter.getParameterAnnotation(JsonView.class) != null && + (AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType) || + AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType)); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class> selectedConverterType) throws IOException { + if (AbstractJacksonHttpMessageConverter.class.isAssignableFrom(selectedConverterType)) { + return inputMessage; + } + + return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), getJsonView(methodParameter)); + } + + @Override + public @Nullable Map determineReadHints(MethodParameter parameter, Type targetType, Class> converterType) { + return Collections.singletonMap(JsonView.class.getName(), getJsonView(parameter)); + } + + private static Class getJsonView(MethodParameter methodParameter) { JsonView ann = methodParameter.getParameterAnnotation(JsonView.class); Assert.state(ann != null, "No JsonView annotation"); @@ -68,8 +87,6 @@ public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter { throw new IllegalArgumentException( "@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter); } - - return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]); + return classes[0]; } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java index ea7f7b8bad6..11525ad218f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2025 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. @@ -16,7 +16,11 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.util.Collections; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonView; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; @@ -55,6 +59,15 @@ public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBo protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + bodyContainer.setSerializationView(getJsonView(returnType)); + } + + @Override + public @Nullable Map determineWriteHints(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType) { + return Collections.singletonMap(JsonView.class.getName(), getJsonView(returnType)); + } + + private static Class getJsonView(MethodParameter returnType) { JsonView ann = returnType.getMethodAnnotation(JsonView.class); Assert.state(ann != null, "No JsonView annotation"); @@ -63,8 +76,6 @@ public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBo throw new IllegalArgumentException( "@JsonView only supported for response body advice with exactly 1 class argument: " + returnType); } - - bodyContainer.setSerializationView(classes[0]); + return classes[0]; } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java index 0d5f204c094..5cf589915a2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -18,12 +18,14 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.Type; +import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; /** * Allows customizing the request before its body is read and converted into an @@ -36,6 +38,7 @@ import org.springframework.http.converter.HttpMessageConverter; * {@code @ControllerAdvice} in which case they are auto-detected. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.2 */ public interface RequestBodyAdvice { @@ -63,6 +66,20 @@ public interface RequestBodyAdvice { HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) throws IOException; + /** + * Invoked to determine read hints if the converter is a {@link SmartHttpMessageConverter}. + * @param parameter the target method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, for example, for {@code HttpEntity}. + * @param converterType the selected converter type + * @return the hints determined otherwise {@code null} + * @since 7.0 + */ + default @Nullable Map determineReadHints(MethodParameter parameter, + Type targetType, Class> converterType) { + return null; + } + /** * Invoked third (and last) after the request body is converted to an Object. * @param body set to the converter Object before the first advice is called @@ -90,5 +107,4 @@ public interface RequestBodyAdvice { @Nullable Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType); - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java index 5d7fa5ecd5c..cd5c0b694de 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -20,7 +20,9 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jspecify.annotations.Nullable; @@ -28,6 +30,7 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.util.CollectionUtils; @@ -36,7 +39,7 @@ import org.springframework.web.method.ControllerAdviceBean; /** * Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each * instance may be (and is most likely) wrapped with - * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}. + * {@link ControllerAdviceBean ControllerAdviceBean}. * * @author Rossen Stoyanchev * @since 4.2 @@ -176,4 +179,38 @@ class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyA } } + @Override + public @Nullable Map determineReadHints(MethodParameter parameter, Type targetType, Class> converterType) { + Map hints = null; + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + Map adviceHints = advice.determineReadHints(parameter, targetType, converterType); + if (adviceHints != null) { + if (hints == null) { + hints = new HashMap<>(adviceHints.size()); + } + hints.putAll(adviceHints); + } + } + } + return hints; + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable Map determineWriteHints(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType) { + Map hints = null; + for (ResponseBodyAdvice advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { + if (advice.supports(returnType, selectedConverterType)) { + Map adviceHints = ((ResponseBodyAdvice) advice).determineWriteHints(body, returnType, selectedContentType, selectedConverterType); + if (adviceHints != null) { + if (hints == null) { + hints = new HashMap<>(adviceHints.size()); + } + hints.putAll(adviceHints); + } + } + } + return hints; + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java index d8f582fadbb..27930462ad1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 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. @@ -16,11 +16,14 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.util.Map; + import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; @@ -35,6 +38,7 @@ import org.springframework.http.server.ServerHttpResponse; * will be auto-detected by both. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.1 * @param the body type */ @@ -65,4 +69,18 @@ public interface ResponseBodyAdvice { Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); + /** + * Invoked to determine write hints if the converter is a {@link SmartHttpMessageConverter}. + * @param body the body to be written + * @param returnType the return type of the controller method + * @param selectedContentType the content type selected through content negotiation + * @param selectedConverterType the converter type selected to write to the response + * @return the hints determined otherwise {@code null} + * @since 7.0 + */ + default @Nullable Map determineWriteHints(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType) { + return null; + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java index c2bbb5a5776..1e4127fb8c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java @@ -17,10 +17,14 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.context.support.StaticApplicationContext; @@ -29,7 +33,9 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; @@ -52,6 +58,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link RequestResponseBodyAdviceChain}. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.2 */ class RequestResponseBodyAdviceChainTests { @@ -131,6 +138,20 @@ class RequestResponseBodyAdviceChainTests { assertThat(actual).isEqualTo(this.body); } + @Test + void controllerAdviceWithHints() { + Object fooAdviceBean = createControllerAdviceBean(FooHintControllerAdvice.class); + Object barAdviceBean = createControllerAdviceBean(BarHintControllerAdvice.class); + RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(List.of(fooAdviceBean, barAdviceBean)); + + Map readHints = chain.determineReadHints(this.paramType, this.paramType.getGenericParameterType(), + JacksonJsonHttpMessageConverter.class); + assertThat(readHints).containsExactlyInAnyOrderEntriesOf(Map.of("foo", "String", "bar", "String")); + + Map writeHints = chain.determineWriteHints(this.body, this.returnType, this.contentType, this.converterType); + assertThat(writeHints).containsExactlyInAnyOrderEntriesOf(Map.of("foo", "body", "bar", "body")); + } + private ControllerAdviceBean createControllerAdviceBean(Class beanType) { StaticApplicationContext applicationContext = new StaticApplicationContext(); applicationContext.registerSingleton(beanType.getSimpleName(), beanType); @@ -174,6 +195,64 @@ class RequestResponseBodyAdviceChainTests { } } + @ControllerAdvice + private static class FooHintControllerAdvice extends RequestBodyAdviceAdapter implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public @Nullable String beforeBodyWrite(@Nullable String body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + return body; + } + + @Override + public @Nullable Map determineWriteHints(@Nullable String body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType) { + return Collections.singletonMap("foo", Objects.requireNonNull(body)); + } + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + return true; + } + + @Override + public @Nullable Map determineReadHints(MethodParameter parameter, Type targetType, Class> converterType) { + return Collections.singletonMap("foo", parameter.getParameterType().getSimpleName()); + } + + } + + @ControllerAdvice + private static class BarHintControllerAdvice extends RequestBodyAdviceAdapter implements ResponseBodyAdvice { + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public @Nullable String beforeBodyWrite(@Nullable String body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + return body; + } + + @Override + public @Nullable Map determineWriteHints(@Nullable String body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType) { + return Collections.singletonMap("bar", Objects.requireNonNull(body)); + } + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + return true; + } + + @Override + public @Nullable Map determineReadHints(MethodParameter parameter, Type targetType, Class> converterType) { + return Collections.singletonMap("bar", parameter.getParameterType().getSimpleName()); + } + } + @SuppressWarnings("unused") @ResponseBody