mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:30:28 +00:00
Polishing in MultipartHttpMessageConverter
This commit is contained in:
+1
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
* @since 7.1
|
||||
*/
|
||||
abstract class DefaultParts {
|
||||
|
||||
|
||||
+43
-29
@@ -158,6 +158,7 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
|
||||
private int maxParts = -1;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new converter instance with the given converter instances for reading and
|
||||
* writing parts.
|
||||
@@ -185,6 +186,7 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
new ResourceHttpMessageConverter()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the list of {@link MediaType} objects supported by this converter.
|
||||
* @see #addSupportedMediaTypes(MediaType...)
|
||||
@@ -219,7 +221,6 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
return Collections.unmodifiableList(this.supportedMediaTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured converters for MIME parts.
|
||||
*/
|
||||
@@ -274,7 +275,6 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
this.multipartCharset = charset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the maximum amount of memory that is allowed per headers section of each part.
|
||||
* <p>By default, this is set to 10K.
|
||||
@@ -317,17 +317,15 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
this.maxParts = maxParts;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
|
||||
if (!supportsMediaType(mediaType)) {
|
||||
return false;
|
||||
}
|
||||
if (!MultiValueMap.class.isAssignableFrom(elementType.toClass()) ||
|
||||
(!elementType.hasUnresolvableGenerics() &&
|
||||
!Part.class.isAssignableFrom(elementType.getGeneric(1).toClass()))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return (MultiValueMap.class.isAssignableFrom(elementType.toClass()) &&
|
||||
(elementType.hasUnresolvableGenerics() ||
|
||||
Part.class.isAssignableFrom(elementType.getGeneric(1).toClass())));
|
||||
}
|
||||
|
||||
private boolean supportsMediaType(@Nullable MediaType mediaType) {
|
||||
@@ -343,7 +341,9 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, Part> read(ResolvableType type, HttpInputMessage message, @Nullable Map<String, Object> hints) throws IOException, HttpMessageNotReadableException {
|
||||
public MultiValueMap<String, Part> read(
|
||||
ResolvableType type, HttpInputMessage message, @Nullable Map<String, Object> hints)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
Charset headersCharset = MultipartUtils.charset(message.getHeaders());
|
||||
byte[] boundary = boundary(message, headersCharset);
|
||||
@@ -351,12 +351,15 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
throw new HttpMessageNotReadableException("No multipart boundary found in Content-Type: \"" +
|
||||
message.getHeaders().getContentType() + "\"", message);
|
||||
}
|
||||
PartGenerator partListener = new PartGenerator(this.maxInMemorySize, this.maxDiskUsagePerPart, this.maxParts, getTempDirectory());
|
||||
new MultipartParser(this.maxHeadersSize, 2 * 1024).parse(message.getBody(), boundary,
|
||||
headersCharset, partListener);
|
||||
return partListener.getParts();
|
||||
}
|
||||
|
||||
PartGenerator partGenerator = new PartGenerator(
|
||||
this.maxInMemorySize, this.maxDiskUsagePerPart, this.maxParts, getTempDirectory());
|
||||
|
||||
MultipartParser parser = new MultipartParser(this.maxHeadersSize, 2 * 1024);
|
||||
parser.parse(message.getBody(), boundary, headersCharset, partGenerator);
|
||||
|
||||
return partGenerator.getParts();
|
||||
}
|
||||
|
||||
private static byte @Nullable [] boundary(HttpInputMessage message, Charset headersCharset) {
|
||||
MediaType contentType = message.getHeaders().getContentType();
|
||||
@@ -398,7 +401,11 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void write(MultiValueMap<String, ?> map, ResolvableType type, @Nullable MediaType contentType, HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints) throws IOException, HttpMessageNotWritableException {
|
||||
public void write(
|
||||
MultiValueMap<String, ?> map, ResolvableType type, @Nullable MediaType contentType,
|
||||
HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
|
||||
MultiValueMap<String, Object> parts = (MultiValueMap<String, Object>) map;
|
||||
|
||||
// If the supplied content type is null, fall back to multipart/form-data.
|
||||
@@ -446,19 +453,25 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"unchecked", "ConstantValue"})
|
||||
@SuppressWarnings("ConstantValue")
|
||||
private <T> boolean checkPartsRepeatable(MultiValueMap<String, Object> map, MediaType contentType) {
|
||||
return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> {
|
||||
HttpHeaders headers = null;
|
||||
Object body = part;
|
||||
if (part instanceof HttpEntity<?> entity) {
|
||||
headers = entity.getHeaders();
|
||||
body = entity.getBody();
|
||||
Assert.state(body != null, "Empty body for part '" + e.getKey() + "': " + part);
|
||||
}
|
||||
HttpMessageConverter<T> converter = (HttpMessageConverter<T>) findConverterFor(e.getKey(), headers, body);
|
||||
return converter != null && converter.canWriteRepeatedly((T) body, contentType);
|
||||
}));
|
||||
return map.entrySet().stream().allMatch(entry ->
|
||||
entry.getValue().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.allMatch(part -> isPartRepeatable(entry.getKey(), part, contentType)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> boolean isPartRepeatable(String name, Object part, MediaType contentType) {
|
||||
HttpHeaders headers = null;
|
||||
Object body = part;
|
||||
if (part instanceof HttpEntity<?> entity) {
|
||||
headers = entity.getHeaders();
|
||||
body = entity.getBody();
|
||||
Assert.state(body != null, "Empty body for part '" + name + "': " + part);
|
||||
}
|
||||
HttpMessageConverter<T> converter = (HttpMessageConverter<T>) findConverterFor(name, headers, body);
|
||||
return (converter != null && converter.canWriteRepeatedly((T) body, contentType));
|
||||
}
|
||||
|
||||
private @Nullable HttpMessageConverter<?> findConverterFor(
|
||||
@@ -483,7 +496,9 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
return (this.multipartCharset != null);
|
||||
}
|
||||
|
||||
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
|
||||
private void writeParts(
|
||||
OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
|
||||
|
||||
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
for (Object part : entry.getValue()) {
|
||||
@@ -549,7 +564,6 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter<
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
|
||||
os.write('-');
|
||||
os.write('-');
|
||||
|
||||
+4
@@ -43,11 +43,13 @@ import org.springframework.http.converter.HttpMessageConversionException;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 7.1
|
||||
*/
|
||||
final class MultipartParser {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(MultipartParser.class);
|
||||
|
||||
|
||||
private final int maxHeadersSize;
|
||||
|
||||
private final int bufferSize;
|
||||
@@ -89,6 +91,7 @@ final class MultipartParser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class InternalParser {
|
||||
|
||||
private final byte[] boundary;
|
||||
@@ -139,6 +142,7 @@ final class MultipartParser {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents the internal state of the {@link MultipartParser}.
|
||||
* The flow for well-formed multipart messages is shown below:
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ import org.springframework.http.MediaType;
|
||||
* Various static utility methods for dealing with multipart parsing.
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
* @since 7.1
|
||||
*/
|
||||
abstract class MultipartUtils {
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ public interface Part {
|
||||
* Delete the underlying storage for this part.
|
||||
*/
|
||||
default void delete() throws IOException {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
-22
@@ -47,11 +47,13 @@ import org.springframework.util.MultiValueMap;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 7.1
|
||||
*/
|
||||
final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PartGenerator.class);
|
||||
|
||||
|
||||
private final MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
private final int maxInMemorySize;
|
||||
@@ -266,23 +268,26 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
@Override
|
||||
public void onBody(DataBuffer dataBuffer, boolean last) {
|
||||
this.byteCount += dataBuffer.readableByteCount();
|
||||
if (PartGenerator.this.maxInMemorySize == -1 ||
|
||||
this.byteCount <= PartGenerator.this.maxInMemorySize) {
|
||||
this.content.add(dataBuffer);
|
||||
if (last) {
|
||||
emitMemoryPart();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isMaxInMemorySizeExceeded()) {
|
||||
switchToFile(dataBuffer, last);
|
||||
return;
|
||||
}
|
||||
this.content.add(dataBuffer);
|
||||
if (last) {
|
||||
emitMemoryPart();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMaxInMemorySizeExceeded() {
|
||||
return (PartGenerator.this.maxInMemorySize != -1 &&
|
||||
this.byteCount > PartGenerator.this.maxInMemorySize);
|
||||
}
|
||||
|
||||
private void switchToFile(DataBuffer current, boolean last) {
|
||||
FileState newState = new FileState(this.headers, PartGenerator.this.fileStorageDirectory);
|
||||
this.content.forEach(newState::writeBuffer);
|
||||
newState.onBody(current, last);
|
||||
PartGenerator.this.state = newState;
|
||||
FileState fileState = new FileState(this.headers, PartGenerator.this.fileStorageDirectory);
|
||||
this.content.forEach(fileState::writeBuffer);
|
||||
fileState.onBody(current, last);
|
||||
PartGenerator.this.state = fileState;
|
||||
}
|
||||
|
||||
private void emitMemoryPart() {
|
||||
@@ -331,23 +336,27 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
@Override
|
||||
public void onBody(DataBuffer dataBuffer, boolean last) {
|
||||
this.byteCount += dataBuffer.readableByteCount();
|
||||
if (PartGenerator.this.maxDiskUsagePerPart == -1 || this.byteCount <= PartGenerator.this.maxDiskUsagePerPart) {
|
||||
writeBuffer(dataBuffer);
|
||||
if (last) {
|
||||
Part part = DefaultParts.part(this.headers, this.file);
|
||||
PartGenerator.this.addPart(part);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isMaxDiskUsagePerPartExceeded()) {
|
||||
try {
|
||||
this.outputStream.close();
|
||||
}
|
||||
catch (IOException exc) {
|
||||
// ignored
|
||||
}
|
||||
throw new HttpMessageConversionException("Part exceeded the disk usage limit of " +
|
||||
PartGenerator.this.maxDiskUsagePerPart + " bytes");
|
||||
throw new HttpMessageConversionException(
|
||||
"Part exceeded the disk usage limit of " +
|
||||
PartGenerator.this.maxDiskUsagePerPart + " bytes");
|
||||
}
|
||||
writeBuffer(dataBuffer);
|
||||
if (last) {
|
||||
Part part = DefaultParts.part(this.headers, this.file);
|
||||
PartGenerator.this.addPart(part);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMaxDiskUsagePerPartExceeded() {
|
||||
return (PartGenerator.this.maxDiskUsagePerPart != -1 &&
|
||||
this.byteCount > PartGenerator.this.maxDiskUsagePerPart);
|
||||
}
|
||||
|
||||
private Path createFile(Path directory) {
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Provides an HttpMessageConverter for Multipart support.
|
||||
* Provides support for reading and writing multipart support,
|
||||
* through an {@link org.springframework.http.converter.HttpMessageConverter}.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.http.converter.multipart;
|
||||
|
||||
Reference in New Issue
Block a user