From b30f4d7bb770be1d13171ffd4a394635f8dbecdd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 25 Apr 2022 16:01:02 +0100 Subject: [PATCH 1/3] Exposes all root causes to ExceptionHandler methods Closes gh-28155 --- .../RequestMappingHandlerAdapter.java | 26 +++++++++++++------ .../annotation/ControllerAdviceTests.java | 9 ++++--- ...pingExceptionHandlingIntegrationTests.java | 4 +-- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 094c8a7287..7f970f670b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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,6 +16,7 @@ package org.springframework.web.reactive.result.method.annotation; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -213,21 +214,30 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod); if (invocable != null) { + ArrayList exceptions = new ArrayList<>(); try { if (logger.isDebugEnabled()) { logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable); } bindingContext.getModel().asMap().clear(); - Throwable cause = exception.getCause(); - if (cause != null) { - return invocable.invoke(exchange, bindingContext, exception, cause, handlerMethod); - } - else { - return invocable.invoke(exchange, bindingContext, exception, handlerMethod); + + // Expose causes as provided arguments as well + Throwable exToExpose = exception; + while (exToExpose != null) { + exceptions.add(exToExpose); + Throwable cause = exToExpose.getCause(); + exToExpose = (cause != exToExpose ? cause : null); } + Object[] arguments = new Object[exceptions.size() + 1]; + exceptions.toArray(arguments); // efficient arraycopy call in ArrayList + arguments[arguments.length - 1] = handlerMethod; + + return invocable.invoke(exchange, bindingContext, arguments); } catch (Throwable invocationEx) { - if (logger.isWarnEnabled()) { + // Any other than the original exception (or a cause) is unintended here, + // probably an accident (e.g. failed assertion or the like). + if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java index ec0e73ee02..d506d2af4b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -82,9 +82,10 @@ public class ControllerAdviceTests { @Test public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception { - AssertionError cause = new AssertionError("argh"); - FatalBeanException exception = new FatalBeanException("wrapped", cause); - testException(exception, cause.toString()); + AssertionError rootCause = new AssertionError("argh"); + FatalBeanException cause = new FatalBeanException("wrapped", rootCause); + Exception exception = new Exception(cause); + testException(exception, rootCause.toString()); } private void testException(Throwable exception, String expected) throws Exception { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java index 7acff0afd1..2dbf712d79 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -138,7 +138,7 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap @GetMapping("/thrown-exception-with-cause-to-handle") public Publisher handleAndThrowExceptionWithCauseToHandle() { - throw new RuntimeException("State", new IOException("IO")); + throw new RuntimeException("State1", new RuntimeException("State2", new IOException("IO"))); } @GetMapping(path = "/mono-error") From caaf83b8e6534b8c9a3806661022ac8185985876 Mon Sep 17 00:00:00 2001 From: binchoo <079111w@gmail.com> Date: Wed, 27 Apr 2022 12:49:05 +0900 Subject: [PATCH 2/3] Add tests for binding to a Part field See gh-27830 --- .../standalone/MultipartControllerTests.java | 109 +++++++++++++++++- .../web/bind/ServletRequestDataBinder.java | 5 +- .../bind/support/WebRequestDataBinder.java | 3 +- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index a00dd2774e..141f8072c1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -39,6 +39,9 @@ import org.springframework.mock.web.MockPart; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; import org.springframework.ui.Model; +import org.springframework.util.StreamUtils; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -56,6 +59,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal /** * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Jaebin Joo */ public class MultipartControllerTests { @@ -225,7 +229,7 @@ public class MultipartControllerTests { } @Test - public void multipartRequestWithServletParts() throws Exception { + public void multipartRequestWithParts_resolvesMultipartFileArguments() throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); MockPart filePart = new MockPart("file", "orig", fileContent); @@ -240,6 +244,50 @@ public class MultipartControllerTests { .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); } + @Test + public void multipartRequestWithParts_resolvesPartArguments() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); + MockPart jsonPart = new MockPart("json", json); + jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/part").part(filePart).part(jsonPart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); + } + + @Test + public void multipartRequestWithParts_resolvesMultipartFileProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/multipartfileproperty").part(filePart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)); + } + + @Test + public void multipartRequestWithParts_cannotResolvePartProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + Exception exception = standaloneSetup(new MultipartController()).build() + .perform(multipart("/partproperty").part(filePart)) + .andExpect(status().is4xxClientError()) + .andReturn() + .getResolvedException(); + + assertThat(exception).isNotNull(); + assertThat(exception).isInstanceOf(BindException.class); + assertThat(((BindException) exception).getFieldError("file")) + .as("MultipartRequest would not bind Part properties.").isNotNull(); + } + @Test // SPR-13317 public void multipartRequestWrapped() throws Exception { byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); @@ -343,10 +391,13 @@ public class MultipartControllerTests { } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestParam Part part, + public String processPart(@RequestPart Part file, @RequestPart Map json, Model model) throws IOException { - model.addAttribute("fileContent", part.getInputStream()); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } model.addAttribute("jsonContent", json); return "redirect:/index"; @@ -357,8 +408,60 @@ public class MultipartControllerTests { model.addAttribute("json", json); return "redirect:/index"; } + + @RequestMapping(value = "/multipartfileproperty", method = RequestMethod.POST) + public String processMultipartFileBean(MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + MultipartFile file = multipartFileBean.getFile(); + if (file != null) { + model.addAttribute("fileContent", file.getBytes()); + } + } + return "redirect:/index"; + } + + @RequestMapping(value = "/partproperty", method = RequestMethod.POST) + public String processPartBean(PartBean partBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + Part file = partBean.getFile(); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } + } + return "redirect:/index"; + } } + private static class MultipartFileBean { + + private MultipartFile file; + + public MultipartFile getFile() { + return file; + } + + public void setFile(MultipartFile file) { + this.file = file; + } + } + + private static class PartBean { + + private Part file; + + public Part getFile() { + return file; + } + + public void setFile(Part file) { + this.file = file; + } + } private static class RequestWrappingFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 864e218439..24c37ce3cb 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -104,11 +104,14 @@ public class ServletRequestDataBinder extends WebDataBinder { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * Part, byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartHttpServletRequest + * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile + * @see jakarta.servlet.http.Part * @see #bind(org.springframework.beans.PropertyValues) */ public void bind(ServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index 805667f69b..ed0d1b3051 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -108,7 +108,8 @@ public class WebRequestDataBinder extends WebDataBinder { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be Part, MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest From f0d149b33066759e21a4c39ee3ce90cb23320fc3 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 28 Apr 2022 11:26:50 +0100 Subject: [PATCH 3/3] Polishing contribution Closes gh-27830 --- .../standalone/MultipartControllerTests.java | 110 ++++-------------- .../web/bind/ServletRequestDataBinder.java | 5 +- .../bind/support/WebRequestDataBinder.java | 7 +- 3 files changed, 25 insertions(+), 97 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index 141f8072c1..67400f7dac 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -32,15 +32,16 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockPart; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.ui.Model; import org.springframework.util.StreamUtils; -import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -63,16 +64,20 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal */ public class MultipartControllerTests { - @Test - public void multipartRequestWithSingleFile() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"/multipartfile", "/part"}) + public void multipartRequestWithSingleFileOrPart(String url) throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockMultipartFile filePart = new MockMultipartFile("file", "orig", null, fileContent); byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); MockMultipartFile jsonPart = new MockMultipartFile("json", "json", "application/json", json); + MockMultipartHttpServletRequestBuilder requestBuilder = (url.endsWith("file") ? + multipart(url).file(new MockMultipartFile("file", "orig", null, fileContent)) : + multipart(url).part(new MockPart("part", "orig", fileContent))); + standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfile").file(filePart).file(jsonPart)) + .perform(requestBuilder.file(jsonPart)) .andExpect(status().isFound()) .andExpect(model().attribute("fileContent", fileContent)) .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); @@ -229,65 +234,16 @@ public class MultipartControllerTests { } @Test - public void multipartRequestWithParts_resolvesMultipartFileArguments() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); - MockPart jsonPart = new MockPart("json", json); - jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); - - standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfile").part(filePart).part(jsonPart)) - .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); - } - - @Test - public void multipartRequestWithParts_resolvesPartArguments() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); - MockPart jsonPart = new MockPart("json", json); - jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); - - standaloneSetup(new MultipartController()).build() - .perform(multipart("/part").part(filePart).part(jsonPart)) - .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); - } - - @Test - public void multipartRequestWithParts_resolvesMultipartFileProperties() throws Exception { + public void multipartRequestWithDataBindingToFile() throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); MockPart filePart = new MockPart("file", "orig", fileContent); standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfileproperty").part(filePart)) + .perform(multipart("/multipartfilebinding").part(filePart)) .andExpect(status().isFound()) .andExpect(model().attribute("fileContent", fileContent)); } - @Test - public void multipartRequestWithParts_cannotResolvePartProperties() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - Exception exception = standaloneSetup(new MultipartController()).build() - .perform(multipart("/partproperty").part(filePart)) - .andExpect(status().is4xxClientError()) - .andReturn() - .getResolvedException(); - - assertThat(exception).isNotNull(); - assertThat(exception).isInstanceOf(BindException.class); - assertThat(((BindException) exception).getFieldError("file")) - .as("MultipartRequest would not bind Part properties.").isNotNull(); - } - @Test // SPR-13317 public void multipartRequestWrapped() throws Exception { byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); @@ -391,11 +347,11 @@ public class MultipartControllerTests { } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestPart Part file, + public String processPart(@RequestPart Part part, @RequestPart Map json, Model model) throws IOException { - if (file != null) { - byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + if (part != null) { + byte[] content = StreamUtils.copyToByteArray(part.getInputStream()); model.addAttribute("fileContent", content); } model.addAttribute("jsonContent", json); @@ -409,9 +365,9 @@ public class MultipartControllerTests { return "redirect:/index"; } - @RequestMapping(value = "/multipartfileproperty", method = RequestMethod.POST) - public String processMultipartFileBean(MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) - throws IOException { + @RequestMapping(value = "/multipartfilebinding", method = RequestMethod.POST) + public String processMultipartFileBean( + MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) throws IOException { if (!bindingResult.hasErrors()) { MultipartFile file = multipartFileBean.getFile(); @@ -421,20 +377,6 @@ public class MultipartControllerTests { } return "redirect:/index"; } - - @RequestMapping(value = "/partproperty", method = RequestMethod.POST) - public String processPartBean(PartBean partBean, Model model, BindingResult bindingResult) - throws IOException { - - if (!bindingResult.hasErrors()) { - Part file = partBean.getFile(); - if (file != null) { - byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); - model.addAttribute("fileContent", content); - } - } - return "redirect:/index"; - } } private static class MultipartFileBean { @@ -450,18 +392,6 @@ public class MultipartControllerTests { } } - private static class PartBean { - - private Part file; - - public Part getFile() { - return file; - } - - public void setFile(Part file) { - this.file = file; - } - } private static class RequestWrappingFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 24c37ce3cb..1c6f0218d2 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -104,9 +104,8 @@ public class ServletRequestDataBinder extends WebDataBinder { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be MultipartFile, - * Part, byte[], or String. The Part binding is only supported when the request - * is not a MultipartRequest. The latter two receive the contents of the uploaded file; - * all metadata like original file name, content type, etc are lost in those cases. + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartHttpServletRequest * @see org.springframework.web.multipart.MultipartRequest diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index ed0d1b3051..16f6141cbd 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -107,10 +107,9 @@ public class WebRequestDataBinder extends WebDataBinder { *

Multipart files are bound via their parameter name, just like normal * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. - *

The type of the target property for a multipart file can be Part, MultipartFile, - * byte[], or String. The Part binding is only supported when the request - * is not a MultipartRequest. The latter two receive the contents of the uploaded file; - * all metadata like original file name, content type, etc are lost in those cases. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile