Skip to content

Commit f3cc4ab

Browse files
committed
@RequestBody supports java.util.Optional
Issue: SPR-15007 (cherry picked from commit eeb7ae5)
1 parent 442d8a6 commit f3cc4ab

File tree

4 files changed

+140
-92
lines changed

4 files changed

+140
-92
lines changed

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

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import java.lang.annotation.Annotation;
2323
import java.lang.reflect.Type;
2424
import java.util.ArrayList;
25+
import java.util.Collection;
2526
import java.util.Collections;
2627
import java.util.EnumSet;
2728
import java.util.LinkedHashSet;
2829
import java.util.List;
30+
import java.util.Optional;
2931
import java.util.Set;
3032
import javax.servlet.http.HttpServletRequest;
3133

@@ -45,6 +47,7 @@
4547
import org.springframework.http.converter.HttpMessageConverter;
4648
import org.springframework.http.converter.HttpMessageNotReadableException;
4749
import org.springframework.http.server.ServletServerHttpRequest;
50+
import org.springframework.lang.UsesJava8;
4851
import org.springframework.util.Assert;
4952
import org.springframework.validation.Errors;
5053
import org.springframework.validation.annotation.Validated;
@@ -59,6 +62,7 @@
5962
*
6063
* @author Arjen Poutsma
6164
* @author Rossen Stoyanchev
65+
* @author Juergen Hoeller
6266
* @since 3.1
6367
*/
6468
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
@@ -128,33 +132,33 @@ protected RequestResponseBodyAdviceChain getAdvice() {
128132
* reading from the given request.
129133
* @param <T> the expected type of the argument value to be created
130134
* @param webRequest the current request
131-
* @param methodParam the method argument
135+
* @param parameter the method parameter descriptor (may be {@code null})
132136
* @param paramType the type of the argument value to be created
133137
* @return the created method argument value
134138
* @throws IOException if the reading from the request fails
135139
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
136140
*/
137-
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,
141+
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
138142
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
139143

140144
HttpInputMessage inputMessage = createInputMessage(webRequest);
141-
return readWithMessageConverters(inputMessage, methodParam, paramType);
145+
return readWithMessageConverters(inputMessage, parameter, paramType);
142146
}
143147

144148
/**
145149
* Create the method argument value of the expected parameter type by reading
146150
* from the given HttpInputMessage.
147151
* @param <T> the expected type of the argument value to be created
148152
* @param inputMessage the HTTP input message representing the current request
149-
* @param param the method parameter descriptor (may be {@code null})
153+
* @param parameter the method parameter descriptor (may be {@code null})
150154
* @param targetType the target type, not necessarily the same as the method
151155
* parameter type, e.g. for {@code HttpEntity<String>}.
152156
* @return the created method argument value
153157
* @throws IOException if the reading from the request fails
154158
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
155159
*/
156160
@SuppressWarnings("unchecked")
157-
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,
161+
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
158162
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
159163

160164
MediaType contentType;
@@ -170,11 +174,11 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
170174
contentType = MediaType.APPLICATION_OCTET_STREAM;
171175
}
172176

173-
Class<?> contextClass = (param != null ? param.getContainingClass() : null);
177+
Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
174178
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
175179
if (targetClass == null) {
176-
ResolvableType resolvableType = (param != null ?
177-
ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType));
180+
ResolvableType resolvableType = (parameter != null ?
181+
ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType));
178182
targetClass = (Class<T>) resolvableType.resolve();
179183
}
180184

@@ -193,13 +197,12 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
193197
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
194198
}
195199
if (inputMessage.getBody() != null) {
196-
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
200+
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
197201
body = genericConverter.read(targetType, contextClass, inputMessage);
198-
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
202+
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
199203
}
200204
else {
201-
body = null;
202-
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
205+
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
203206
}
204207
break;
205208
}
@@ -210,13 +213,12 @@ else if (targetClass != null) {
210213
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
211214
}
212215
if (inputMessage.getBody() != null) {
213-
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
216+
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
214217
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
215-
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
218+
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
216219
}
217220
else {
218-
body = null;
219-
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
221+
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
220222
}
221223
break;
222224
}
@@ -254,12 +256,12 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques
254256
* Spring's {@link org.springframework.validation.annotation.Validated},
255257
* and custom annotations whose name starts with "Valid".
256258
* @param binder the DataBinder to be used
257-
* @param methodParam the method parameter
258-
* @see #isBindExceptionRequired
259+
* @param parameter the method parameter descriptor
259260
* @since 4.1.5
261+
* @see #isBindExceptionRequired
260262
*/
261-
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
262-
Annotation[] annotations = methodParam.getParameterAnnotations();
263+
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
264+
Annotation[] annotations = parameter.getParameterAnnotations();
263265
for (Annotation ann : annotations) {
264266
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
265267
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
@@ -274,17 +276,28 @@ protected void validateIfApplicable(WebDataBinder binder, MethodParameter method
274276
/**
275277
* Whether to raise a fatal bind exception on validation errors.
276278
* @param binder the data binder used to perform data binding
277-
* @param methodParam the method argument
279+
* @param parameter the method parameter descriptor
278280
* @return {@code true} if the next method argument is not of type {@link Errors}
279281
* @since 4.1.5
280282
*/
281-
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
282-
int i = methodParam.getParameterIndex();
283-
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
283+
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
284+
int i = parameter.getParameterIndex();
285+
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
284286
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
285287
return !hasBindingResult;
286288
}
287289

290+
/**
291+
* Adapt the given argument against the method parameter, if necessary.
292+
* @param arg the resolved argument
293+
* @param parameter the method parameter descriptor
294+
* @return the adapted argument, or the original resolved argument as-is
295+
* @since 4.3.5
296+
*/
297+
protected Object adaptArgumentIfNecessary(Object arg, MethodParameter parameter) {
298+
return (parameter.isOptional() ? OptionalResolver.resolveValue(arg) : arg);
299+
}
300+
288301

289302
private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
290303

@@ -335,4 +348,20 @@ public HttpMethod getMethod() {
335348
}
336349
}
337350

351+
352+
/**
353+
* Inner class to avoid hard-coded dependency on Java 8 Optional type...
354+
*/
355+
@UsesJava8
356+
private static class OptionalResolver {
357+
358+
public static Object resolveValue(Object value) {
359+
if (value == null || (value instanceof Collection && ((Collection) value).isEmpty()) ||
360+
(value instanceof Object[] && ((Object[]) value).length == 0)) {
361+
return Optional.empty();
362+
}
363+
return Optional.of(value);
364+
}
365+
}
366+
338367
}

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

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@
1616

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

19-
import java.util.Collection;
2019
import java.util.List;
21-
import java.util.Optional;
2220
import javax.servlet.http.HttpServletRequest;
2321

2422
import org.springframework.core.MethodParameter;
2523
import org.springframework.http.HttpInputMessage;
2624
import org.springframework.http.converter.HttpMessageConverter;
27-
import org.springframework.lang.UsesJava8;
2825
import org.springframework.validation.BindingResult;
2926
import org.springframework.web.bind.MethodArgumentNotValidException;
3027
import org.springframework.web.bind.WebDataBinder;
@@ -157,11 +154,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
157154
throw new MissingServletRequestPartException(name);
158155
}
159156
}
160-
if (parameter.isOptional()) {
161-
arg = OptionalResolver.resolveValue(arg);
162-
}
163-
164-
return arg;
157+
return adaptArgumentIfNecessary(arg, parameter);
165158
}
166159

167160
private String getPartName(MethodParameter methodParam, RequestPart requestPart) {
@@ -177,20 +170,4 @@ private String getPartName(MethodParameter methodParam, RequestPart requestPart)
177170
return partName;
178171
}
179172

180-
181-
/**
182-
* Inner class to avoid hard-coded dependency on Java 8 Optional type...
183-
*/
184-
@UsesJava8
185-
private static class OptionalResolver {
186-
187-
public static Object resolveValue(Object value) {
188-
if (value == null || (value instanceof Collection && ((Collection) value).isEmpty()) ||
189-
(value instanceof Object[] && ((Object[]) value).length == 0)) {
190-
return Optional.empty();
191-
}
192-
return Optional.of(value);
193-
}
194-
}
195-
196173
}

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Type;
2121
import java.util.List;
22-
2322
import javax.servlet.http.HttpServletRequest;
2423

2524
import org.springframework.core.Conventions;
@@ -55,6 +54,7 @@
5554
*
5655
* @author Arjen Poutsma
5756
* @author Rossen Stoyanchev
57+
* @author Juergen Hoeller
5858
* @since 3.1
5959
*/
6060
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@@ -124,7 +124,8 @@ public boolean supportsReturnType(MethodParameter returnType) {
124124
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
125125
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
126126

127-
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
127+
parameter = parameter.nestedIfOptional();
128+
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
128129
String name = Conventions.getVariableNameForParameter(parameter);
129130

130131
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
@@ -136,28 +137,28 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
136137
}
137138
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
138139

139-
return arg;
140+
return adaptArgumentIfNecessary(arg, parameter);
140141
}
141142

142143
@Override
143-
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,
144+
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
144145
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
145146

146147
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
147148
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
148149

149-
Object arg = readWithMessageConverters(inputMessage, methodParam, paramType);
150+
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
150151
if (arg == null) {
151-
if (checkRequired(methodParam)) {
152+
if (checkRequired(parameter)) {
152153
throw new HttpMessageNotReadableException("Required request body is missing: " +
153-
methodParam.getMethod().toGenericString());
154+
parameter.getMethod().toGenericString());
154155
}
155156
}
156157
return arg;
157158
}
158159

159-
protected boolean checkRequired(MethodParameter methodParam) {
160-
return methodParam.getParameterAnnotation(RequestBody.class).required();
160+
protected boolean checkRequired(MethodParameter parameter) {
161+
return (parameter.getParameterAnnotation(RequestBody.class).required() && !parameter.isOptional());
161162
}
162163

163164
@Override

0 commit comments

Comments
 (0)