From 4c0b6304ac3787c206d832c15b8f110741b0cccb Mon Sep 17 00:00:00 2001 From: bono007 Date: Sun, 1 Aug 2021 23:55:04 -0500 Subject: [PATCH 1/2] Add spring.webflux.multipart configuration properties See gh-26254 --- .../ReactiveMultipartAutoConfiguration.java | 80 ++++++ .../reactive/ReactiveMultipartProperties.java | 134 ++++++++++ .../reactive/WebFluxAutoConfiguration.java | 3 +- .../main/resources/META-INF/spring.factories | 1 + ...activeMultipartAutoConfigurationTests.java | 251 ++++++++++++++++++ .../ReactiveMultipartPropertiesTests.java | 46 ++++ 6 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartPropertiesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java new file mode 100644 index 0000000000..454a7408c9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java @@ -0,0 +1,80 @@ +package org.springframework.boot.autoconfigure.web.reactive; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Paths; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.util.unit.DataSize; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for multipart support in Spring + * Webflux. + *

+ * Configures the {@link DefaultPartHttpMessageReader} via a {@link CodecCustomizer}. + * + * @author Chris Bono + * @since 2.6.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ CodecConfigurer.class, DefaultPartHttpMessageReader.class }) +@ConditionalOnWebApplication(type = Type.REACTIVE) +@EnableConfigurationProperties(ReactiveMultipartProperties.class) +class ReactiveMultipartAutoConfiguration { + + @Bean + @Order(1) + CodecCustomizer defaultPartHttpMessageReaderCustomizer(ReactiveMultipartProperties multipartProperties) { + return (configurer) -> configurer.defaultCodecs().configureDefaultCodec((codec) -> { + if (!DefaultPartHttpMessageReader.class.isInstance(codec)) { + return; + } + DefaultPartHttpMessageReader defaultPartHttpMessageReader = (DefaultPartHttpMessageReader) codec; + boolean streaming = multipartProperties.getStreaming() != null && multipartProperties.getStreaming(); + boolean unlimitedMaxMemorySize = multipartProperties.getMaxInMemorySize() != null + && multipartProperties.getMaxInMemorySize().toBytes() == -1; + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(multipartProperties::getStreaming).to(defaultPartHttpMessageReader::setStreaming); + if (!streaming) { + map.from(multipartProperties::getMaxInMemorySize).asInt(this::convertToBytes) + .to(defaultPartHttpMessageReader::setMaxInMemorySize); + } + if (!streaming && !unlimitedMaxMemorySize) { + map.from(multipartProperties::getMaxDiskUsagePerPart).as(this::convertToBytes) + .to(defaultPartHttpMessageReader::setMaxDiskUsagePerPart); + map.from(multipartProperties::getFileStorageDirectory) + .to(dir -> setFileStorageDirectory(dir, defaultPartHttpMessageReader)); + } + map.from(multipartProperties::getMaxParts).to(defaultPartHttpMessageReader::setMaxParts); + map.from(multipartProperties::getMaxHeadersSize).asInt(this::convertToBytes) + .to(defaultPartHttpMessageReader::setMaxHeadersSize); + map.from(multipartProperties::getHeadersCharset).to(defaultPartHttpMessageReader::setHeadersCharset); + }); + } + + private void setFileStorageDirectory(String fileStorageDirectory, + DefaultPartHttpMessageReader defaultPartHttpMessageReader) { + try { + defaultPartHttpMessageReader.setFileStorageDirectory(Paths.get(fileStorageDirectory)); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private Long convertToBytes(DataSize size) { + return (size != null) ? size.toBytes() : null; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java new file mode 100644 index 0000000000..36392e9b72 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.util.unit.DataSize; + +/** + * {@link ConfigurationProperties Configuration properties} for configuring multipart + * support in Spring Webflux. Used to configure the {@link DefaultPartHttpMessageReader}. + * + * @author Chris Bono + * @since 2.6.0 + */ +@ConfigurationProperties(prefix = "spring.webflux.multipart") +public class ReactiveMultipartProperties { + + /** + * Maximum amount of memory allowed per part. Set to -1 to store all contents in + * memory. Ignored when streaming is enabled. + */ + private DataSize maxInMemorySize = DataSize.ofKilobytes(256); + + /** + * Maximum amount of memory allowed per headers section of each part. Set to -1 to + * enforce no limits. + */ + private DataSize maxHeadersSize = DataSize.ofKilobytes(8); + + /** + * Maximum amount of disk space allowed per part. Default is -1 which enforces no + * limits. Ignored when streaming is enabled or 'maxInMemorySize' is set to -1. + */ + private DataSize maxDiskUsagePerPart = DataSize.ofBytes(-1); + + /** + * Maximum number of parts allowed in a given multipart request. Default is -1 which + * enforces no limits. + */ + private Integer maxParts = -1; + + /** + * Whether or not to stream directly from the parsed input buffer stream without + * storing in memory nor file. Default is non-streaming. + */ + private Boolean streaming = Boolean.FALSE; + + /** + * Directory used to store parts larger than 'maxInMemorySize'. Default is a directory + * named 'spring-multipart' created under the system temporary directory. Ignored when + * streaming is enabled or 'maxInMemorySize' is set to -1. + */ + private String fileStorageDirectory; + + /** + * Character set used to decode headers. + */ + private Charset headersCharset = StandardCharsets.UTF_8; + + public DataSize getMaxInMemorySize() { + return maxInMemorySize; + } + + public void setMaxInMemorySize(DataSize maxInMemorySize) { + this.maxInMemorySize = maxInMemorySize; + } + + public DataSize getMaxHeadersSize() { + return maxHeadersSize; + } + + public void setMaxHeadersSize(DataSize maxHeadersSize) { + this.maxHeadersSize = maxHeadersSize; + } + + public DataSize getMaxDiskUsagePerPart() { + return maxDiskUsagePerPart; + } + + public void setMaxDiskUsagePerPart(DataSize maxDiskUsagePerPart) { + this.maxDiskUsagePerPart = maxDiskUsagePerPart; + } + + public Integer getMaxParts() { + return maxParts; + } + + public void setMaxParts(Integer maxParts) { + this.maxParts = maxParts; + } + + public Boolean getStreaming() { + return streaming; + } + + public void setStreaming(Boolean streaming) { + this.streaming = streaming; + } + + public String getFileStorageDirectory() { + return fileStorageDirectory; + } + + public void setFileStorageDirectory(String fileStorageDirectory) { + this.fileStorageDirectory = fileStorageDirectory; + } + + public Charset getHeadersCharset() { + return headersCharset; + } + + public void setHeadersCharset(Charset headersCharset) { + this.headersCharset = headersCharset; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index c6f369543f..d9ce69ead6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -91,6 +91,7 @@ import org.springframework.web.server.session.WebSessionManager; * @author Phillip Webb * @author EddĂș MelĂ©ndez * @author Artsiom Yudovin + * @author Chris Bono * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -98,7 +99,7 @@ import org.springframework.web.server.session.WebSessionManager; @ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) @AutoConfigureAfter({ ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class, - ValidationAutoConfiguration.class }) + ReactiveMultipartAutoConfiguration.class, ValidationAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 33b91643fd..305690f0ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -139,6 +139,7 @@ org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java new file mode 100644 index 0000000000..5055cfe7d9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java @@ -0,0 +1,251 @@ +/* + * Copyright 2012-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.http.codec.support.DefaultServerCodecConfigurer; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReactiveMultipartAutoConfiguration}. + * + * @author Chris Bono + */ +class ReactiveMultipartAutoConfigurationTests { + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveMultipartAutoConfiguration.class)); + + private static final boolean DEFAULT_STREAMING = false; + + private static final int DEFAULT_MAX_IN_MEMORY_SIZE = 256 * 1024; + + private static final int DEFAULT_MAX_PARTS = -1; + + private static final int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024; + + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private static final long DEFAULT_MAX_DISK_USAGE_PER_PART = -1; + + private static final Path DEFAULT_FILE_STORAGE_DIRECTORY = Paths.get(System.getProperty("java.io.tmpdir"), + "spring-multipart"); + + @Test + void shouldNotProvideCustomizerForNonReactiveApp() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveMultipartAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(CodecCustomizer.class)); + } + + @Test + void shouldNotProvideCustomizerWhenDefaultPartHttpMessageReaderNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader(DefaultPartHttpMessageReader.class)) + .run((context) -> assertThat(context).doesNotHaveBean(CodecCustomizer.class)); + } + + @Test + void shouldNotProvideCustomizerWhenCodecConfigurerNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader(CodecConfigurer.class)) + .run((context) -> assertThat(context).doesNotHaveBean(CodecCustomizer.class)); + } + + @Test + void customizerSetsAppropriatePropsWhenStreamingEnabled() { + this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:true", + "spring.webflux.multipart.max-in-memory-size=1GB", "spring.webflux.multipart.max-headers-size=512MB", + "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", + "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") + .run((context) -> { + CodecCustomizer customizer = context.getBean(CodecCustomizer.class); + DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); + customizer.customize(configurer); + DefaultPartHttpMessageReader partReader = getPartReader(configurer); + + // always set + assertThat(partReader).hasFieldOrPropertyWithValue("streaming", true); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); + assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", + toIntBytes(DataSize.ofMegabytes(512))); + assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); + + // never set when streaming + assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); + assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", + DEFAULT_MAX_DISK_USAGE_PER_PART); + assertThat(partReader).extracting("fileStorageDirectory") + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); + }); + } + + @Test + void customizerSetsAppropriatePropsWhenStreamingDisabledWithUnlimitedMemorySize() { + this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", + "spring.webflux.multipart.max-in-memory-size=-1", "spring.webflux.multipart.max-headers-size=512MB", + "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", + "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") + .run((context) -> { + + CodecCustomizer customizer = context.getBean(CodecCustomizer.class); + DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); + customizer.customize(configurer); + DefaultPartHttpMessageReader partReader = getPartReader(configurer); + + // always set + assertThat(partReader).hasFieldOrPropertyWithValue("streaming", false); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); + assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", + toIntBytes(DataSize.ofMegabytes(512))); + assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); + + // set when not streaming w/ unlimited memory size + assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", -1); + + // never set when not streaming w/ unlimited memory size + assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", + DEFAULT_MAX_DISK_USAGE_PER_PART); + assertThat(partReader).extracting("fileStorageDirectory") + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); + }); + } + + @Test + void customizerSetsAppropriatePropsWhenStreamingDisabledWithLimitedMemorySize() { + this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", + "spring.webflux.multipart.max-in-memory-size=1GB", "spring.webflux.multipart.max-headers-size=512MB", + "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", + "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") + .run((context) -> { + + CodecCustomizer customizer = context.getBean(CodecCustomizer.class); + DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); + customizer.customize(configurer); + DefaultPartHttpMessageReader partReader = getPartReader(configurer); + + // always set + assertThat(partReader).hasFieldOrPropertyWithValue("streaming", false); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); + assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", + toIntBytes(DataSize.ofMegabytes(512))); + assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); + + // set when not streaming w/ limited memory size + assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", + toIntBytes(DataSize.ofGigabytes(1))); + assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", + DataSize.ofMegabytes(100).toBytes()); + assertThat(partReader).extracting("fileStorageDirectory") + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .returns(Paths.get("."), Mono::block); + }); + } + + @Test + void customizerSetsDefaultPropsWhenNoPropertiesAreSet() { + this.contextRunner.run((context) -> { + CodecCustomizer customizer = context.getBean(CodecCustomizer.class); + DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); + customizer.customize(configurer); + DefaultPartHttpMessageReader partReader = getPartReader(configurer); + assertThat(partReader).hasFieldOrPropertyWithValue("streaming", DEFAULT_STREAMING); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", DEFAULT_MAX_PARTS); + assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", DEFAULT_MAX_HEADERS_SIZE); + assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", DEFAULT_CHARSET); + assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); + assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", DEFAULT_MAX_DISK_USAGE_PER_PART); + assertThat(partReader).extracting("fileStorageDirectory") + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); + }); + } + + @Test + void customizerSetsDefaultPropsWhenDefaultsAreExplicitlySet() { + this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", + "spring.webflux.multipart.max-in-memory-size=256KB", "spring.webflux.multipart.max-headers-size=8KB", + "spring.webflux.multipart.max-disk-usage-per-part=-1", "spring.webflux.multipart.max-parts=-1", + "spring.webflux.multipart.file-storage-directory:" + DEFAULT_FILE_STORAGE_DIRECTORY, + "spring.webflux.multipart.headers-charset:" + DEFAULT_CHARSET).run((context) -> { + CodecCustomizer customizer = context.getBean(CodecCustomizer.class); + DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); + customizer.customize(configurer); + DefaultPartHttpMessageReader partReader = getPartReader(configurer); + assertThat(partReader).hasFieldOrPropertyWithValue("streaming", DEFAULT_STREAMING); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", DEFAULT_MAX_PARTS); + assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", DEFAULT_MAX_HEADERS_SIZE); + assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", DEFAULT_CHARSET); + assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); + assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", + DEFAULT_MAX_DISK_USAGE_PER_PART); + assertThat(partReader).extracting("fileStorageDirectory") + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); + }); + } + + @Test + void customizerBeanShouldHaveOrderOne() { + this.contextRunner.run((context) -> { + Method customizerMethod = ReflectionUtils.findMethod(ReactiveMultipartAutoConfiguration.class, + "defaultPartHttpMessageReaderCustomizer", ReactiveMultipartProperties.class); + Integer order = new TestAnnotationAwareOrderComparator().findOrder(customizerMethod); + assertThat(order).isEqualTo(1); + }); + } + + private DefaultPartHttpMessageReader getPartReader(DefaultServerCodecConfigurer codecConfigurer) { + return codecConfigurer.getReaders().stream().filter(DefaultPartHttpMessageReader.class::isInstance) + .map(DefaultPartHttpMessageReader.class::cast).findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find DefaultPartHttpMessageReader")); + } + + private Integer toIntBytes(DataSize size) { + return (size != null) ? (int) size.toBytes() : null; + } + + static class TestAnnotationAwareOrderComparator extends AnnotationAwareOrderComparator { + + @Override + public Integer findOrder(Object obj) { + return super.findOrder(obj); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartPropertiesTests.java new file mode 100644 index 0000000000..f9815d619f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartPropertiesTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import org.junit.jupiter.api.Test; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReactiveMultipartProperties} + * + * @author Chris Bono + */ +class ReactiveMultipartPropertiesTests { + + @Test + void defaultValuesAreConsistent() { + ReactiveMultipartProperties multipartProperties = new ReactiveMultipartProperties(); + DefaultPartHttpMessageReader defaultPartHttpMessageReader = new DefaultPartHttpMessageReader(); + assertThat(defaultPartHttpMessageReader).hasFieldOrPropertyWithValue("streaming", + multipartProperties.getStreaming()); + assertThat(defaultPartHttpMessageReader).hasFieldOrPropertyWithValue("maxInMemorySize", + (int) multipartProperties.getMaxInMemorySize().toBytes()); + assertThat(defaultPartHttpMessageReader).hasFieldOrPropertyWithValue("maxHeadersSize", + (int) multipartProperties.getMaxHeadersSize().toBytes()); + assertThat(defaultPartHttpMessageReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", + multipartProperties.getMaxDiskUsagePerPart().toBytes()); + assertThat(defaultPartHttpMessageReader).hasFieldOrPropertyWithValue("maxParts", + multipartProperties.getMaxParts()); + } + +} From e48cb12252ccca0b7a1f629edf6b457ad7c35064 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 2 Aug 2021 15:40:06 +0200 Subject: [PATCH 2/2] Polish "Add spring.webflux.multipart configuration properties" See gh-26254 --- .../ReactiveMultipartAutoConfiguration.java | 75 ++++---- .../reactive/ReactiveMultipartProperties.java | 30 +-- .../main/resources/META-INF/spring.factories | 2 +- ...activeMultipartAutoConfigurationTests.java | 176 +----------------- 4 files changed, 63 insertions(+), 220 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java index 454a7408c9..dd3bd1e874 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java @@ -1,7 +1,23 @@ +/* + * Copyright 2012-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.boot.autoconfigure.web.reactive; import java.io.IOException; -import java.io.UncheckedIOException; +import java.nio.file.Path; import java.nio.file.Paths; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -14,67 +30,54 @@ import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.config.WebFluxConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} for multipart support in Spring - * Webflux. - *

- * Configures the {@link DefaultPartHttpMessageReader} via a {@link CodecCustomizer}. + * WebFlux. * * @author Chris Bono + * @author Brian Clozel * @since 2.6.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ CodecConfigurer.class, DefaultPartHttpMessageReader.class }) +@ConditionalOnClass({ DefaultPartHttpMessageReader.class, WebFluxConfigurer.class }) @ConditionalOnWebApplication(type = Type.REACTIVE) @EnableConfigurationProperties(ReactiveMultipartProperties.class) -class ReactiveMultipartAutoConfiguration { +public class ReactiveMultipartAutoConfiguration { @Bean - @Order(1) + @Order(0) CodecCustomizer defaultPartHttpMessageReaderCustomizer(ReactiveMultipartProperties multipartProperties) { return (configurer) -> configurer.defaultCodecs().configureDefaultCodec((codec) -> { - if (!DefaultPartHttpMessageReader.class.isInstance(codec)) { - return; - } - DefaultPartHttpMessageReader defaultPartHttpMessageReader = (DefaultPartHttpMessageReader) codec; - boolean streaming = multipartProperties.getStreaming() != null && multipartProperties.getStreaming(); - boolean unlimitedMaxMemorySize = multipartProperties.getMaxInMemorySize() != null - && multipartProperties.getMaxInMemorySize().toBytes() == -1; - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(multipartProperties::getStreaming).to(defaultPartHttpMessageReader::setStreaming); - if (!streaming) { - map.from(multipartProperties::getMaxInMemorySize).asInt(this::convertToBytes) + if (codec instanceof DefaultPartHttpMessageReader) { + DefaultPartHttpMessageReader defaultPartHttpMessageReader = (DefaultPartHttpMessageReader) codec; + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(multipartProperties::getMaxInMemorySize).asInt(DataSize::toBytes) .to(defaultPartHttpMessageReader::setMaxInMemorySize); - } - if (!streaming && !unlimitedMaxMemorySize) { - map.from(multipartProperties::getMaxDiskUsagePerPart).as(this::convertToBytes) + map.from(multipartProperties::getMaxHeadersSize).asInt(DataSize::toBytes) + .to(defaultPartHttpMessageReader::setMaxHeadersSize); + map.from(multipartProperties::getMaxDiskUsagePerPart).asInt(DataSize::toBytes) .to(defaultPartHttpMessageReader::setMaxDiskUsagePerPart); - map.from(multipartProperties::getFileStorageDirectory) - .to(dir -> setFileStorageDirectory(dir, defaultPartHttpMessageReader)); + map.from(multipartProperties::getMaxParts).to(defaultPartHttpMessageReader::setMaxParts); + map.from(multipartProperties::getStreaming).to(defaultPartHttpMessageReader::setStreaming); + map.from(multipartProperties::getFileStorageDirectory).as(Paths::get) + .to((dir) -> configureFileStorageDirectory(defaultPartHttpMessageReader, dir)); + map.from(multipartProperties::getHeadersCharset).to(defaultPartHttpMessageReader::setHeadersCharset); } - map.from(multipartProperties::getMaxParts).to(defaultPartHttpMessageReader::setMaxParts); - map.from(multipartProperties::getMaxHeadersSize).asInt(this::convertToBytes) - .to(defaultPartHttpMessageReader::setMaxHeadersSize); - map.from(multipartProperties::getHeadersCharset).to(defaultPartHttpMessageReader::setHeadersCharset); }); } - private void setFileStorageDirectory(String fileStorageDirectory, - DefaultPartHttpMessageReader defaultPartHttpMessageReader) { + private void configureFileStorageDirectory(DefaultPartHttpMessageReader defaultPartHttpMessageReader, + Path fileStorageDirectory) { try { - defaultPartHttpMessageReader.setFileStorageDirectory(Paths.get(fileStorageDirectory)); + defaultPartHttpMessageReader.setFileStorageDirectory(fileStorageDirectory); } catch (IOException ex) { - throw new UncheckedIOException(ex); + throw new IllegalStateException("Failed to configure multipart file storage directory", ex); } } - private Long convertToBytes(DataSize size) { - return (size != null) ? size.toBytes() : null; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java index 36392e9b72..bf06bc926c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java @@ -34,8 +34,8 @@ import org.springframework.util.unit.DataSize; public class ReactiveMultipartProperties { /** - * Maximum amount of memory allowed per part. Set to -1 to store all contents in - * memory. Ignored when streaming is enabled. + * Maximum amount of memory allowed per part before it's written to disk. Set to -1 to + * store all contents in memory. Ignored when streaming is enabled. */ private DataSize maxInMemorySize = DataSize.ofKilobytes(256); @@ -47,7 +47,7 @@ public class ReactiveMultipartProperties { /** * Maximum amount of disk space allowed per part. Default is -1 which enforces no - * limits. Ignored when streaming is enabled or 'maxInMemorySize' is set to -1. + * limits. Ignored when streaming is enabled. */ private DataSize maxDiskUsagePerPart = DataSize.ofBytes(-1); @@ -58,15 +58,15 @@ public class ReactiveMultipartProperties { private Integer maxParts = -1; /** - * Whether or not to stream directly from the parsed input buffer stream without - * storing in memory nor file. Default is non-streaming. + * Whether to stream directly from the parsed input buffer stream without storing in + * memory nor file. Default is non-streaming. */ private Boolean streaming = Boolean.FALSE; /** - * Directory used to store parts larger than 'maxInMemorySize'. Default is a directory - * named 'spring-multipart' created under the system temporary directory. Ignored when - * streaming is enabled or 'maxInMemorySize' is set to -1. + * Directory used to store file parts larger than 'maxInMemorySize'. Default is a + * directory named 'spring-multipart' created under the system temporary directory. + * Ignored when streaming is enabled. */ private String fileStorageDirectory; @@ -76,7 +76,7 @@ public class ReactiveMultipartProperties { private Charset headersCharset = StandardCharsets.UTF_8; public DataSize getMaxInMemorySize() { - return maxInMemorySize; + return this.maxInMemorySize; } public void setMaxInMemorySize(DataSize maxInMemorySize) { @@ -84,7 +84,7 @@ public class ReactiveMultipartProperties { } public DataSize getMaxHeadersSize() { - return maxHeadersSize; + return this.maxHeadersSize; } public void setMaxHeadersSize(DataSize maxHeadersSize) { @@ -92,7 +92,7 @@ public class ReactiveMultipartProperties { } public DataSize getMaxDiskUsagePerPart() { - return maxDiskUsagePerPart; + return this.maxDiskUsagePerPart; } public void setMaxDiskUsagePerPart(DataSize maxDiskUsagePerPart) { @@ -100,7 +100,7 @@ public class ReactiveMultipartProperties { } public Integer getMaxParts() { - return maxParts; + return this.maxParts; } public void setMaxParts(Integer maxParts) { @@ -108,7 +108,7 @@ public class ReactiveMultipartProperties { } public Boolean getStreaming() { - return streaming; + return this.streaming; } public void setStreaming(Boolean streaming) { @@ -116,7 +116,7 @@ public class ReactiveMultipartProperties { } public String getFileStorageDirectory() { - return fileStorageDirectory; + return this.fileStorageDirectory; } public void setFileStorageDirectory(String fileStorageDirectory) { @@ -124,7 +124,7 @@ public class ReactiveMultipartProperties { } public Charset getHeadersCharset() { - return headersCharset; + return this.headersCharset; } public void setHeadersCharset(Charset headersCharset) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 305690f0ba..ab1d2ac664 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -138,8 +138,8 @@ org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\ org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\ -org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java index 5055cfe7d9..27c68bcf80 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java @@ -16,27 +16,21 @@ package org.springframework.boot.autoconfigure.web.reactive; -import java.lang.reflect.Method; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; -import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.codec.CodecCustomizer; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.support.DefaultServerCodecConfigurer; -import org.springframework.util.ReflectionUtils; import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.config.WebFluxConfigurer; import static org.assertj.core.api.Assertions.assertThat; @@ -50,18 +44,6 @@ class ReactiveMultipartAutoConfigurationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveMultipartAutoConfiguration.class)); - private static final boolean DEFAULT_STREAMING = false; - - private static final int DEFAULT_MAX_IN_MEMORY_SIZE = 256 * 1024; - - private static final int DEFAULT_MAX_PARTS = -1; - - private static final int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024; - - private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private static final long DEFAULT_MAX_DISK_USAGE_PER_PART = -1; - private static final Path DEFAULT_FILE_STORAGE_DIRECTORY = Paths.get(System.getProperty("java.io.tmpdir"), "spring-multipart"); @@ -73,179 +55,37 @@ class ReactiveMultipartAutoConfigurationTests { } @Test - void shouldNotProvideCustomizerWhenDefaultPartHttpMessageReaderNotAvailable() { - this.contextRunner.withClassLoader(new FilteredClassLoader(DefaultPartHttpMessageReader.class)) + void shouldNotProvideCustomizerWhenWebFluxNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader(WebFluxConfigurer.class)) .run((context) -> assertThat(context).doesNotHaveBean(CodecCustomizer.class)); } @Test - void shouldNotProvideCustomizerWhenCodecConfigurerNotAvailable() { - this.contextRunner.withClassLoader(new FilteredClassLoader(CodecConfigurer.class)) - .run((context) -> assertThat(context).doesNotHaveBean(CodecCustomizer.class)); - } - - @Test - void customizerSetsAppropriatePropsWhenStreamingEnabled() { + void shouldConfigureMultipartProperties() { this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:true", - "spring.webflux.multipart.max-in-memory-size=1GB", "spring.webflux.multipart.max-headers-size=512MB", + "spring.webflux.multipart.max-in-memory-size=1GB", "spring.webflux.multipart.max-headers-size=16KB", "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", - "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") - .run((context) -> { + "spring.webflux.multipart.headers-charset:UTF_16").run((context) -> { CodecCustomizer customizer = context.getBean(CodecCustomizer.class); DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); customizer.customize(configurer); DefaultPartHttpMessageReader partReader = getPartReader(configurer); - - // always set assertThat(partReader).hasFieldOrPropertyWithValue("streaming", true); assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", - toIntBytes(DataSize.ofMegabytes(512))); - assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); - - // never set when streaming - assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); - assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", - DEFAULT_MAX_DISK_USAGE_PER_PART); - assertThat(partReader).extracting("fileStorageDirectory") - .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) - .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); - }); - } - - @Test - void customizerSetsAppropriatePropsWhenStreamingDisabledWithUnlimitedMemorySize() { - this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", - "spring.webflux.multipart.max-in-memory-size=-1", "spring.webflux.multipart.max-headers-size=512MB", - "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", - "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") - .run((context) -> { - - CodecCustomizer customizer = context.getBean(CodecCustomizer.class); - DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); - customizer.customize(configurer); - DefaultPartHttpMessageReader partReader = getPartReader(configurer); - - // always set - assertThat(partReader).hasFieldOrPropertyWithValue("streaming", false); - assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); - assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", - toIntBytes(DataSize.ofMegabytes(512))); - assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); - - // set when not streaming w/ unlimited memory size - assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", -1); - - // never set when not streaming w/ unlimited memory size - assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", - DEFAULT_MAX_DISK_USAGE_PER_PART); - assertThat(partReader).extracting("fileStorageDirectory") - .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) - .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); - }); - } - - @Test - void customizerSetsAppropriatePropsWhenStreamingDisabledWithLimitedMemorySize() { - this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", - "spring.webflux.multipart.max-in-memory-size=1GB", "spring.webflux.multipart.max-headers-size=512MB", - "spring.webflux.multipart.max-disk-usage-per-part=100MB", "spring.webflux.multipart.max-parts=7", - "spring.webflux.multipart.file-storage-directory:.", "spring.webflux.multipart.headers-charset:UTF_16") - .run((context) -> { - - CodecCustomizer customizer = context.getBean(CodecCustomizer.class); - DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); - customizer.customize(configurer); - DefaultPartHttpMessageReader partReader = getPartReader(configurer); - - // always set - assertThat(partReader).hasFieldOrPropertyWithValue("streaming", false); - assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); - assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", - toIntBytes(DataSize.ofMegabytes(512))); + Math.toIntExact(DataSize.ofKilobytes(16).toBytes())); assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); - - // set when not streaming w/ limited memory size assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", - toIntBytes(DataSize.ofGigabytes(1))); + Math.toIntExact(DataSize.ofGigabytes(1).toBytes())); assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", DataSize.ofMegabytes(100).toBytes()); - assertThat(partReader).extracting("fileStorageDirectory") - .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) - .returns(Paths.get("."), Mono::block); }); } - @Test - void customizerSetsDefaultPropsWhenNoPropertiesAreSet() { - this.contextRunner.run((context) -> { - CodecCustomizer customizer = context.getBean(CodecCustomizer.class); - DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); - customizer.customize(configurer); - DefaultPartHttpMessageReader partReader = getPartReader(configurer); - assertThat(partReader).hasFieldOrPropertyWithValue("streaming", DEFAULT_STREAMING); - assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", DEFAULT_MAX_PARTS); - assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", DEFAULT_MAX_HEADERS_SIZE); - assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", DEFAULT_CHARSET); - assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); - assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", DEFAULT_MAX_DISK_USAGE_PER_PART); - assertThat(partReader).extracting("fileStorageDirectory") - .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) - .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); - }); - } - - @Test - void customizerSetsDefaultPropsWhenDefaultsAreExplicitlySet() { - this.contextRunner.withPropertyValues("spring.webflux.multipart.streaming:false", - "spring.webflux.multipart.max-in-memory-size=256KB", "spring.webflux.multipart.max-headers-size=8KB", - "spring.webflux.multipart.max-disk-usage-per-part=-1", "spring.webflux.multipart.max-parts=-1", - "spring.webflux.multipart.file-storage-directory:" + DEFAULT_FILE_STORAGE_DIRECTORY, - "spring.webflux.multipart.headers-charset:" + DEFAULT_CHARSET).run((context) -> { - CodecCustomizer customizer = context.getBean(CodecCustomizer.class); - DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); - customizer.customize(configurer); - DefaultPartHttpMessageReader partReader = getPartReader(configurer); - assertThat(partReader).hasFieldOrPropertyWithValue("streaming", DEFAULT_STREAMING); - assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", DEFAULT_MAX_PARTS); - assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", DEFAULT_MAX_HEADERS_SIZE); - assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", DEFAULT_CHARSET); - assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", DEFAULT_MAX_IN_MEMORY_SIZE); - assertThat(partReader).hasFieldOrPropertyWithValue("maxDiskUsagePerPart", - DEFAULT_MAX_DISK_USAGE_PER_PART); - assertThat(partReader).extracting("fileStorageDirectory") - .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) - .returns(DEFAULT_FILE_STORAGE_DIRECTORY, Mono::block); - }); - } - - @Test - void customizerBeanShouldHaveOrderOne() { - this.contextRunner.run((context) -> { - Method customizerMethod = ReflectionUtils.findMethod(ReactiveMultipartAutoConfiguration.class, - "defaultPartHttpMessageReaderCustomizer", ReactiveMultipartProperties.class); - Integer order = new TestAnnotationAwareOrderComparator().findOrder(customizerMethod); - assertThat(order).isEqualTo(1); - }); - } - private DefaultPartHttpMessageReader getPartReader(DefaultServerCodecConfigurer codecConfigurer) { return codecConfigurer.getReaders().stream().filter(DefaultPartHttpMessageReader.class::isInstance) .map(DefaultPartHttpMessageReader.class::cast).findFirst() .orElseThrow(() -> new IllegalStateException("Could not find DefaultPartHttpMessageReader")); } - private Integer toIntBytes(DataSize size) { - return (size != null) ? (int) size.toBytes() : null; - } - - static class TestAnnotationAwareOrderComparator extends AnnotationAwareOrderComparator { - - @Override - public Integer findOrder(Object obj) { - return super.findOrder(obj); - } - - } - }