diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index 42a0c9c0b9..d04c739c5d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -54,6 +54,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Michael Simons * @author Madhura Bhave + * @author HaiTao Zhang */ @ManagementContextConfiguration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java index c62a302b9a..0288b4b05b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java @@ -16,18 +16,29 @@ package org.springframework.boot.autoconfigure.web.embedded; -import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; import io.undertow.UndertowOptions; import org.xnio.Option; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.Undertow; +import org.springframework.boot.autoconfigure.web.ServerProperties.Undertow.Accesslog; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.embedded.undertow.ConfigurableUndertowWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.util.unit.DataSize; /** @@ -40,6 +51,7 @@ import org.springframework.util.unit.DataSize; * @author Phillip Webb * @author Arstiom Yudovin * @author Rafiullah Hamedy + * @author HaiTao Zhang * @since 2.0.0 */ public class UndertowWebServerFactoryCustomizer @@ -61,77 +73,56 @@ public class UndertowWebServerFactoryCustomizer @Override public void customize(ConfigurableUndertowWebServerFactory factory) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + FactoryOptions options = new FactoryOptions(factory); ServerProperties properties = this.serverProperties; - ServerProperties.Undertow undertowProperties = properties.getUndertow(); - ServerProperties.Undertow.Options undertowOptions = undertowProperties.getOptions(); - ServerProperties.Undertow.Accesslog accesslogProperties = undertowProperties.getAccesslog(); - PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(undertowProperties::getBufferSize).whenNonNull().asInt(DataSize::toBytes) - .to(factory::setBufferSize); - propertyMapper.from(undertowProperties::getIoThreads).to(factory::setIoThreads); - propertyMapper.from(undertowProperties::getWorkerThreads).to(factory::setWorkerThreads); - propertyMapper.from(undertowProperties::getDirectBuffers).to(factory::setUseDirectBuffers); - propertyMapper.from(accesslogProperties::isEnabled).to(factory::setAccessLogEnabled); - propertyMapper.from(accesslogProperties::getDir).to(factory::setAccessLogDirectory); - propertyMapper.from(accesslogProperties::getPattern).to(factory::setAccessLogPattern); - propertyMapper.from(accesslogProperties::getPrefix).to(factory::setAccessLogPrefix); - propertyMapper.from(accesslogProperties::getSuffix).to(factory::setAccessLogSuffix); - propertyMapper.from(accesslogProperties::isRotate).to(factory::setAccessLogRotate); - propertyMapper.from(this::getOrDeduceUseForwardHeaders).to(factory::setUseForwardHeaders); + map.from(properties::getMaxHttpHeaderSize).asInt(DataSize::toBytes).when(this::isPositive) + .to(options.server(UndertowOptions.MAX_HEADER_SIZE)); + map.from(properties::getConnectionTimeout).asInt(Duration::toMillis) + .to(options.server(UndertowOptions.NO_REQUEST_TIMEOUT)); + mapUndertowProperties(factory, options); + mapAccessLogProperties(factory); + map.from(this::getOrDeduceUseForwardHeaders).to(factory::setUseForwardHeaders); + } - propertyMapper.from(properties::getMaxHttpHeaderSize).whenNonNull().asInt(DataSize::toBytes) - .when(this::isPositive).to((maxHttpHeaderSize) -> customizeServerOption(factory, - UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize)); + private void mapUndertowProperties(ConfigurableUndertowWebServerFactory factory, FactoryOptions options) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Undertow properties = this.serverProperties.getUndertow(); + map.from(properties::getBufferSize).whenNonNull().asInt(DataSize::toBytes).to(factory::setBufferSize); + map.from(properties::getIoThreads).to(factory::setIoThreads); + map.from(properties::getWorkerThreads).to(factory::setWorkerThreads); + map.from(properties::getDirectBuffers).to(factory::setUseDirectBuffers); + map.from(properties::isEagerFilterInit).to((x) -> setEagerFilterInit(factory, x)); + map.from(properties::getMaxHttpPostSize).as(DataSize::toBytes).when(this::isPositive) + .to(options.server(UndertowOptions.MAX_ENTITY_SIZE)); + map.from(properties::getMaxParameters).to(options.server(UndertowOptions.MAX_PARAMETERS)); + map.from(properties::getMaxHeaders).to(options.server(UndertowOptions.MAX_HEADERS)); + map.from(properties::getMaxCookies).to(options.server(UndertowOptions.MAX_COOKIES)); + map.from(properties::isAllowEncodedSlash).to(options.server(UndertowOptions.ALLOW_ENCODED_SLASH)); + map.from(properties::isDecodeUrl).to(options.server(UndertowOptions.DECODE_URL)); + map.from(properties::getUrlCharset).as(Charset::name).to(options.server(UndertowOptions.URL_CHARSET)); + map.from(properties::isAlwaysSetKeepAlive).to(options.server(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)); + map.from(properties.getOptions()::getServer).to(options.forEach(options::server)); + map.from(properties.getOptions()::getSocket).to(options.forEach(options::socket)); + } - propertyMapper.from(undertowProperties::getMaxHttpPostSize).as(DataSize::toBytes).when(this::isPositive).to( - (maxHttpPostSize) -> customizeServerOption(factory, UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize)); - - propertyMapper.from(properties::getConnectionTimeout).to((connectionTimeout) -> customizeServerOption(factory, - UndertowOptions.NO_REQUEST_TIMEOUT, (int) connectionTimeout.toMillis())); - - propertyMapper.from(undertowProperties::getMaxParameters) - .to((maxParameters) -> customizeServerOption(factory, UndertowOptions.MAX_PARAMETERS, maxParameters)); - - propertyMapper.from(undertowProperties::getMaxHeaders) - .to((maxHeaders) -> customizeServerOption(factory, UndertowOptions.MAX_HEADERS, maxHeaders)); - - propertyMapper.from(undertowProperties::getMaxCookies) - .to((maxCookies) -> customizeServerOption(factory, UndertowOptions.MAX_COOKIES, maxCookies)); - - propertyMapper.from(undertowProperties::isAllowEncodedSlash) - .to((allowEncodedSlash) -> customizeServerOption(factory, UndertowOptions.ALLOW_ENCODED_SLASH, - allowEncodedSlash)); - - propertyMapper.from(undertowProperties::isDecodeUrl) - .to((isDecodeUrl) -> customizeServerOption(factory, UndertowOptions.DECODE_URL, isDecodeUrl)); - - propertyMapper.from(undertowProperties::getUrlCharset) - .to((urlCharset) -> customizeServerOption(factory, UndertowOptions.URL_CHARSET, urlCharset.name())); - - propertyMapper.from(undertowProperties::isAlwaysSetKeepAlive) - .to((alwaysSetKeepAlive) -> customizeServerOption(factory, UndertowOptions.ALWAYS_SET_KEEP_ALIVE, - alwaysSetKeepAlive)); - - propertyMapper.from(undertowOptions::getServer) - .to((server) -> server.forEach((key, value) -> setCustomOption(factory, key, value, "server"))); - - propertyMapper.from(undertowOptions::getSocket) - .to((socket) -> socket.forEach((key, value) -> setCustomOption(factory, key, value, "socket"))); - - factory.addDeploymentInfoCustomizers( - (deploymentInfo) -> deploymentInfo.setEagerFilterInit(undertowProperties.isEagerFilterInit())); + private void setEagerFilterInit(ConfigurableUndertowWebServerFactory factory, Boolean eagerFilterInit) { + factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo.setEagerFilterInit(eagerFilterInit)); } private boolean isPositive(Number value) { return value.longValue() > 0; } - private void customizeServerOption(ConfigurableUndertowWebServerFactory factory, Option option, T value) { - factory.addBuilderCustomizers((builder) -> builder.setServerOption(option, value)); - } - - private void customizeSocketOption(ConfigurableUndertowWebServerFactory factory, Option option, T value) { - factory.addBuilderCustomizers((builder) -> builder.setSocketOption(option, value)); + private void mapAccessLogProperties(ConfigurableUndertowWebServerFactory factory) { + Accesslog properties = this.serverProperties.getUndertow().getAccesslog(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::isEnabled).to(factory::setAccessLogEnabled); + map.from(properties::getDir).to(factory::setAccessLogDirectory); + map.from(properties::getPattern).to(factory::setAccessLogPattern); + map.from(properties::getPrefix).to(factory::setAccessLogPrefix); + map.from(properties::getSuffix).to(factory::setAccessLogSuffix); + map.from(properties::isRotate).to(factory::setAccessLogRotate); } private boolean getOrDeduceUseForwardHeaders() { @@ -142,31 +133,63 @@ public class UndertowWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - @SuppressWarnings("unchecked") - private void setCustomOption(ConfigurableUndertowWebServerFactory factory, String key, String value, - String type) { - Field[] fields = UndertowOptions.class.getDeclaredFields(); - for (Field field : fields) { - if (getCanonicalName(field.getName()).equals(getCanonicalName(key))) { - Option option = (Option) Option.fromString( - UndertowOptions.class.getName() + '.' + field.getName(), getClass().getClassLoader()); - T parsed = option.parseValue(value, getClass().getClassLoader()); - if (type.equals("server")) { - customizeServerOption(factory, option, parsed); - } - else if (type.equals("socket")) { - customizeSocketOption(factory, option, parsed); - } - return; - } - } - } + /** + * {@link ConfigurableUndertowWebServerFactory} wrapper that makes it easier to apply + * {@link UndertowOptions}. + */ + private static class FactoryOptions { + + private static final Map> NAME_LOOKUP; + static { + Map> lookup = new HashMap<>(); + ReflectionUtils.doWithLocalFields(UndertowOptions.class, (field) -> { + int modifiers = field.getModifiers(); + if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) + && Option.class.isAssignableFrom(field.getType())) { + try { + Option option = (Option) field.get(null); + lookup.put(getCanonicalName(field.getName()), option); + } + catch (IllegalAccessException ex) { + } + } + }); + NAME_LOOKUP = Collections.unmodifiableMap(lookup); + } + + private final ConfigurableUndertowWebServerFactory factory; + + FactoryOptions(ConfigurableUndertowWebServerFactory factory) { + this.factory = factory; + } + + Consumer server(Option option) { + return (value) -> this.factory.addBuilderCustomizers((builder) -> builder.setServerOption(option, value)); + } + + Consumer socket(Option option) { + return (value) -> this.factory.addBuilderCustomizers((builder) -> builder.setSocketOption(option, value)); + } + + @SuppressWarnings("unchecked") + Consumer> forEach(Function, Consumer> function) { + return (map) -> { + map.forEach((key, value) -> { + Option option = (Option) NAME_LOOKUP.get(getCanonicalName(key)); + Assert.state(option != null, "Unable to find '" + key + "' in UndertowOptions"); + T parsed = option.parseValue(value, getClass().getClassLoader()); + function.apply(option).accept(parsed); + }); + }; + } + + private static String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().filter(Character::isLetterOrDigit).map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } - private String getCanonicalName(String key) { - StringBuilder canonicalName = new StringBuilder(key.length()); - key.chars().map((c) -> (char) c).filter(Character::isLetterOrDigit).map(Character::toLowerCase) - .forEach((c) -> canonicalName.append((char) c)); - return canonicalName.toString(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java index b07a00684d..4ecd23bc92 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java @@ -51,6 +51,7 @@ import static org.mockito.Mockito.verify; * @author Phillip Webb * @author Artsiom Yudovin * @author Rafiullah Hamedy + * @author HaiTao Zhang */ class UndertowWebServerFactoryCustomizerTests { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java index 2521afe234..070d84d002 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java @@ -87,9 +87,9 @@ final class LenientStringToEnumConverterFactory implements ConverterFactory candidates = new LinkedHashMap(); for (T candidate : (Set) EnumSet.allOf(this.enumType)) { - candidates.put(getLettersAndDigits(candidate.name()), candidate); + candidates.put(getCanonicalName(candidate.name()), candidate); } - String name = getLettersAndDigits(source); + String name = getCanonicalName(source); T result = candidates.get(name); if (result != null) { return result; @@ -103,7 +103,7 @@ final class LenientStringToEnumConverterFactory implements ConverterFactory canonicalName.append((char) c)); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 2fd8f68c5e..9d08033456 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -55,6 +55,7 @@ import org.springframework.util.StringUtils; * {@link ReactiveWebServerFactory} that can be used to create a {@link TomcatWebServer}. * * @author Brian Clozel + * @author HaiTao Zhang * @since 2.0.0 */ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java index 684e3f72c1..38dd0ff0b3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java @@ -56,6 +56,7 @@ import static org.mockito.Mockito.verify; * * @author Brian Clozel * @author Madhura Bhave + * @author HaiTao Zhang */ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests {