Merge branch '5.3.x' into main
This commit is contained in:
commit
b4e6014a14
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -31,13 +31,17 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.Part;
|
import jakarta.servlet.http.Part;
|
||||||
import org.junit.jupiter.api.Test;
|
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.MockMultipartFile;
|
||||||
import org.springframework.mock.web.MockPart;
|
import org.springframework.mock.web.MockPart;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
@ -55,19 +59,24 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal
|
||||||
/**
|
/**
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
* @author Jaebin Joo
|
||||||
*/
|
*/
|
||||||
public class MultipartControllerTests {
|
public class MultipartControllerTests {
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
public void multipartRequestWithSingleFile() throws Exception {
|
@ValueSource(strings = {"/multipartfile", "/part"})
|
||||||
|
public void multipartRequestWithSingleFileOrPart(String url) throws Exception {
|
||||||
byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8);
|
byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8);
|
||||||
MockMultipartFile filePart = new MockMultipartFile("file", "orig", null, fileContent);
|
|
||||||
|
|
||||||
byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8);
|
byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8);
|
||||||
MockMultipartFile jsonPart = new MockMultipartFile("json", "json", "application/json", json);
|
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()
|
standaloneSetup(new MultipartController()).build()
|
||||||
.perform(multipart("/multipartfile").file(filePart).file(jsonPart))
|
.perform(requestBuilder.file(jsonPart))
|
||||||
.andExpect(status().isFound())
|
.andExpect(status().isFound())
|
||||||
.andExpect(model().attribute("fileContent", fileContent))
|
.andExpect(model().attribute("fileContent", fileContent))
|
||||||
.andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah")));
|
.andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah")));
|
||||||
|
@ -224,19 +233,14 @@ public class MultipartControllerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void multipartRequestWithServletParts() throws Exception {
|
public void multipartRequestWithDataBindingToFile() throws Exception {
|
||||||
byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8);
|
byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8);
|
||||||
MockPart filePart = new MockPart("file", "orig", fileContent);
|
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()
|
standaloneSetup(new MultipartController()).build()
|
||||||
.perform(multipart("/multipartfile").part(filePart).part(jsonPart))
|
.perform(multipart("/multipartfilebinding").part(filePart))
|
||||||
.andExpect(status().isFound())
|
.andExpect(status().isFound())
|
||||||
.andExpect(model().attribute("fileContent", fileContent))
|
.andExpect(model().attribute("fileContent", fileContent));
|
||||||
.andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test // SPR-13317
|
@Test // SPR-13317
|
||||||
|
@ -342,10 +346,13 @@ public class MultipartControllerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/part", method = RequestMethod.POST)
|
@RequestMapping(value = "/part", method = RequestMethod.POST)
|
||||||
public String processPart(@RequestParam Part part,
|
public String processPart(@RequestPart Part part,
|
||||||
@RequestPart Map<String, String> json, Model model) throws IOException {
|
@RequestPart Map<String, String> json, Model model) throws IOException {
|
||||||
|
|
||||||
model.addAttribute("fileContent", part.getInputStream());
|
if (part != null) {
|
||||||
|
byte[] content = StreamUtils.copyToByteArray(part.getInputStream());
|
||||||
|
model.addAttribute("fileContent", content);
|
||||||
|
}
|
||||||
model.addAttribute("jsonContent", json);
|
model.addAttribute("jsonContent", json);
|
||||||
|
|
||||||
return "redirect:/index";
|
return "redirect:/index";
|
||||||
|
@ -356,6 +363,32 @@ public class MultipartControllerTests {
|
||||||
model.addAttribute("json", json);
|
model.addAttribute("json", json);
|
||||||
return "redirect:/index";
|
return "redirect:/index";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/multipartfilebinding", 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MultipartFileBean {
|
||||||
|
|
||||||
|
private MultipartFile file;
|
||||||
|
|
||||||
|
public MultipartFile getFile() {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFile(MultipartFile file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -104,11 +104,13 @@ public class ServletRequestDataBinder extends WebDataBinder {
|
||||||
* HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property,
|
* HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property,
|
||||||
* invoking a "setUploadedFile" setter method.
|
* invoking a "setUploadedFile" setter method.
|
||||||
* <p>The type of the target property for a multipart file can be MultipartFile,
|
* <p>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;
|
* byte[], or String. Servlet Part binding is also supported when the
|
||||||
* all metadata like original file name, content type, etc are lost in those cases.
|
* request has not been parsed to MultipartRequest via MultipartResolver.
|
||||||
* @param request the request with parameters to bind (can be multipart)
|
* @param request the request with parameters to bind (can be multipart)
|
||||||
* @see org.springframework.web.multipart.MultipartHttpServletRequest
|
* @see org.springframework.web.multipart.MultipartHttpServletRequest
|
||||||
|
* @see org.springframework.web.multipart.MultipartRequest
|
||||||
* @see org.springframework.web.multipart.MultipartFile
|
* @see org.springframework.web.multipart.MultipartFile
|
||||||
|
* @see jakarta.servlet.http.Part
|
||||||
* @see #bind(org.springframework.beans.PropertyValues)
|
* @see #bind(org.springframework.beans.PropertyValues)
|
||||||
*/
|
*/
|
||||||
public void bind(ServletRequest request) {
|
public void bind(ServletRequest request) {
|
||||||
|
|
|
@ -107,9 +107,9 @@ public class WebRequestDataBinder extends WebDataBinder {
|
||||||
* <p>Multipart files are bound via their parameter name, just like normal
|
* <p>Multipart files are bound via their parameter name, just like normal
|
||||||
* HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property,
|
* HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property,
|
||||||
* invoking a "setUploadedFile" setter method.
|
* invoking a "setUploadedFile" setter method.
|
||||||
* <p>The type of the target property for a multipart file can be Part, MultipartFile,
|
* <p>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;
|
* byte[], or String. Servlet Part binding is also supported when the
|
||||||
* all metadata like original file name, content type, etc are lost in those cases.
|
* request has not been parsed to MultipartRequest via MultipartResolver.
|
||||||
* @param request the request with parameters to bind (can be multipart)
|
* @param request the request with parameters to bind (can be multipart)
|
||||||
* @see org.springframework.web.multipart.MultipartRequest
|
* @see org.springframework.web.multipart.MultipartRequest
|
||||||
* @see org.springframework.web.multipart.MultipartFile
|
* @see org.springframework.web.multipart.MultipartFile
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
package org.springframework.web.reactive.result.method.annotation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -213,21 +214,30 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application
|
||||||
|
|
||||||
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);
|
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);
|
||||||
if (invocable != null) {
|
if (invocable != null) {
|
||||||
|
ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable);
|
logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable);
|
||||||
}
|
}
|
||||||
bindingContext.getModel().asMap().clear();
|
bindingContext.getModel().asMap().clear();
|
||||||
Throwable cause = exception.getCause();
|
|
||||||
if (cause != null) {
|
// Expose causes as provided arguments as well
|
||||||
return invocable.invoke(exchange, bindingContext, exception, cause, handlerMethod);
|
Throwable exToExpose = exception;
|
||||||
}
|
while (exToExpose != null) {
|
||||||
else {
|
exceptions.add(exToExpose);
|
||||||
return invocable.invoke(exchange, bindingContext, exception, handlerMethod);
|
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) {
|
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);
|
logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -82,9 +82,10 @@ public class ControllerAdviceTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception {
|
public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception {
|
||||||
AssertionError cause = new AssertionError("argh");
|
AssertionError rootCause = new AssertionError("argh");
|
||||||
FatalBeanException exception = new FatalBeanException("wrapped", cause);
|
FatalBeanException cause = new FatalBeanException("wrapped", rootCause);
|
||||||
testException(exception, cause.toString());
|
Exception exception = new Exception(cause);
|
||||||
|
testException(exception, rootCause.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testException(Throwable exception, String expected) throws Exception {
|
private void testException(Throwable exception, String expected) throws Exception {
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -139,7 +139,7 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
|
||||||
|
|
||||||
@GetMapping("/thrown-exception-with-cause-to-handle")
|
@GetMapping("/thrown-exception-with-cause-to-handle")
|
||||||
public Publisher<String> handleAndThrowExceptionWithCauseToHandle() {
|
public Publisher<String> handleAndThrowExceptionWithCauseToHandle() {
|
||||||
throw new RuntimeException("State", new IOException("IO"));
|
throw new RuntimeException("State1", new RuntimeException("State2", new IOException("IO")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/mono-error")
|
@GetMapping(path = "/mono-error")
|
||||||
|
|
Loading…
Reference in New Issue