Apply server.undertow.* config to reactive servers

This commit applies `server.undertow.*` configuration properties
to Undertow when configured as a reactive web server.

See gh-11500
This commit is contained in:
Brian Clozel
2018-01-11 11:15:23 +01:00
parent 15bc718248
commit 98ac2218db
10 changed files with 581 additions and 190 deletions

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2012-2018 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
*
* http://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.web.embedded.undertow;
import java.io.File;
import io.undertow.Undertow;
import io.undertow.servlet.api.DeploymentInfo;
/**
* Web Server Factory configuration for Undertow-specific features.
*
* @author Brian Clozel
* @since 2.0.0
* @see UndertowServletWebServerFactory
* @see UndertowReactiveWebServerFactory
*/
public interface ConfigurableUndertowWebServerFactory {
/**
* Add {@link UndertowBuilderCustomizer}s that should be used to customize the
* Undertow {@link Undertow.Builder}.
* @param customizers the customizers to add
*/
void addBuilderCustomizers(UndertowBuilderCustomizer... customizers);
/**
* Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the
* Undertow {@link DeploymentInfo}.
* @param customizers the customizers to add
*/
void addDeploymentInfoCustomizers(UndertowDeploymentInfoCustomizer... customizers);
/**
* Set the buffer size.
* @param bufferSize buffer size
*/
void setBufferSize(Integer bufferSize);
/**
* Set the number of IO Threads.
* @param ioThreads number of IO Threads
*/
void setIoThreads(Integer ioThreads);
/**
* Set the number of Worker Threads.
* @param workerThreads number of Worker Threads
*/
void setWorkerThreads(Integer workerThreads);
/**
* Set whether direct buffers should be used.
* @param useForwardHeaders whether direct buffers should be used
*/
void setUseDirectBuffers(Boolean useForwardHeaders);
/**
* Set the access log directory.
* @param accessLogDirectory access log directory
*/
void setAccessLogDirectory(File accessLogDirectory);
/**
* Set the access log pattern.
* @param accessLogPattern access log pattern
*/
void setAccessLogPattern(String accessLogPattern);
/**
* Set the access log prefix.
* @param accessLogPrefix log prefix
*/
void setAccessLogPrefix(String accessLogPrefix);
/**
* Set the access log suffix.
* @param accessLogSuffix access log suffix
*/
void setAccessLogSuffix(String accessLogSuffix);
/**
* Set whether access logs are enabled.
* @param accessLogEnabled whether access logs are enabled
*/
void setAccessLogEnabled(boolean accessLogEnabled);
/**
* Set whether access logs rotation is enabled.
* @param accessLogRotate whether access logs rotation is enabled
*/
void setAccessLogRotate(boolean accessLogRotate);
/**
* Set if x-forward-* headers should be processed.
* @param useForwardHeaders if x-forward headers should be used
*/
void setUseForwardHeaders(boolean useForwardHeaders);
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2012-2018 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
*
* http://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.web.embedded.undertow;
import java.util.ArrayList;
import java.util.List;
import io.undertow.attribute.RequestHeaderAttribute;
import io.undertow.predicate.Predicate;
import io.undertow.predicate.Predicates;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.encoding.ContentEncodingRepository;
import io.undertow.server.handlers.encoding.EncodingHandler;
import io.undertow.server.handlers.encoding.GzipEncodingProvider;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import org.springframework.boot.web.server.Compression;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Configure the HTTP compression on an Undertow {@link HttpHandler}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
final class UndertowCompressionConfigurer {
private UndertowCompressionConfigurer() {
}
/**
* Optionally wrap the given {@link HttpHandler} for HTTP compression support.
* @param compression the HTTP compression configuration
* @param httpHandler the HTTP handler to wrap
* @return the wrapped HTTP handler if compression is enabled, or the handler itself
*/
public static HttpHandler configureCompression(Compression compression, HttpHandler httpHandler) {
if (compression == null || !compression.getEnabled()) {
return httpHandler;
}
ContentEncodingRepository repository = new ContentEncodingRepository();
repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50,
Predicates.and(getCompressionPredicates(compression)));
return new EncodingHandler(repository).setNext(httpHandler);
}
private static Predicate[] getCompressionPredicates(Compression compression) {
List<Predicate> predicates = new ArrayList<>();
predicates.add(new MaxSizePredicate(compression.getMinResponseSize()));
predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes()));
if (compression.getExcludedUserAgents() != null) {
for (String agent : compression.getExcludedUserAgents()) {
RequestHeaderAttribute agentHeader = new RequestHeaderAttribute(
new HttpString(HttpHeaders.USER_AGENT));
predicates.add(Predicates.not(Predicates.regex(agentHeader, agent)));
}
}
return predicates.toArray(new Predicate[predicates.size()]);
}
private static class CompressibleMimeTypePredicate implements Predicate {
private final List<MimeType> mimeTypes;
CompressibleMimeTypePredicate(String[] mimeTypes) {
this.mimeTypes = new ArrayList<>(mimeTypes.length);
for (String mimeTypeString : mimeTypes) {
this.mimeTypes.add(MimeTypeUtils.parseMimeType(mimeTypeString));
}
}
@Override
public boolean resolve(HttpServerExchange value) {
String contentType = value.getResponseHeaders()
.getFirst(HttpHeaders.CONTENT_TYPE);
if (contentType != null) {
for (MimeType mimeType : this.mimeTypes) {
if (mimeType
.isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) {
return true;
}
}
}
return false;
}
}
/**
* Predicate that returns true if the Content-Size of a request is above a given value
* or is missing.
*/
private static class MaxSizePredicate implements Predicate {
private final Predicate maxContentSize;
MaxSizePredicate(int size) {
this.maxContentSize = Predicates.maxContentSize(size);
}
@Override
public boolean resolve(HttpServerExchange value) {
if (value.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) {
return this.maxContentSize.resolve(value);
}
return true;
}
}
}

View File

@@ -16,18 +16,29 @@
package org.springframework.boot.web.embedded.undertow;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.AccessLogReceiver;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import io.undertow.servlet.api.DeploymentInfo;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter;
import org.springframework.util.Assert;
@@ -37,7 +48,12 @@ import org.springframework.util.Assert;
* @author Brian Clozel
* @since 2.0.0
*/
public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory {
public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory
implements ConfigurableUndertowWebServerFactory {
private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>();
private List<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers = new ArrayList<>();
private Integer bufferSize;
@@ -47,7 +63,20 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
private Boolean directBuffers;
private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>();
private File accessLogDirectory;
private String accessLogPattern;
private String accessLogPrefix;
private String accessLogSuffix;
private boolean accessLogEnabled = false;
private boolean accessLogRotate = true;
private boolean useForwardHeaders;
/**
* Create a new {@link UndertowReactiveWebServerFactory} instance.
@@ -65,9 +94,9 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
}
@Override
public WebServer getWebServer(HttpHandler httpHandler) {
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
Undertow.Builder builder = createBuilder(getPort());
UndertowHttpHandlerAdapter handler = new UndertowHttpHandlerAdapter(httpHandler);
HttpHandler handler = createUndertowHandler(httpHandler);
builder.setHandler(handler);
return new UndertowWebServer(builder, getPort() >= 0);
}
@@ -98,6 +127,50 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
return builder;
}
private HttpHandler createUndertowHandler(org.springframework.http.server.reactive.HttpHandler httpHandler) {
HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
if (isAccessLogEnabled()) {
handler = createAccessLogHandler(handler);
}
return handler;
}
private AccessLogHandler createAccessLogHandler(io.undertow.server.HttpHandler handler) {
try {
createAccessLogDirectoryIfNecessary();
String prefix = (this.accessLogPrefix != null ? this.accessLogPrefix
: "access_log.");
AccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(
createWorker(), this.accessLogDirectory, prefix, this.accessLogSuffix,
this.accessLogRotate);
String formatString = (this.accessLogPattern != null) ? this.accessLogPattern
: "common";
return new AccessLogHandler(handler, accessLogReceiver, formatString,
Undertow.class.getClassLoader());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex);
}
}
private void createAccessLogDirectoryIfNecessary() {
Assert.state(this.accessLogDirectory != null, "Access log directory is not set");
if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) {
throw new IllegalStateException("Failed to create access log directory '"
+ this.accessLogDirectory + "'");
}
}
private XnioWorker createWorker() throws IOException {
Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());
return xnio.createWorker(
OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap());
}
private void customizeSsl(Undertow.Builder builder) {
new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider())
.customize(builder);
@@ -114,19 +187,94 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
return getAddress().getHostAddress();
}
/**
* Set {@link UndertowDeploymentInfoCustomizer}s that should be applied to the
* Undertow {@link DeploymentInfo}. Calling this method will replace any existing
* customizers.
* @param customizers the customizers to set
*/
public void setDeploymentInfoCustomizers(
Collection<? extends UndertowDeploymentInfoCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.deploymentInfoCustomizers = new ArrayList<>(customizers);
}
/**
* Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that
* will be applied to the Undertow {@link DeploymentInfo}.
* @return the customizers that will be applied
*/
public Collection<UndertowDeploymentInfoCustomizer> getDeploymentInfoCustomizers() {
return this.deploymentInfoCustomizers;
}
@Override
public void addDeploymentInfoCustomizers(
UndertowDeploymentInfoCustomizer... customizers) {
Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null");
this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers));
}
@Override
public void setAccessLogDirectory(File accessLogDirectory) {
this.accessLogDirectory = accessLogDirectory;
}
@Override
public void setAccessLogPattern(String accessLogPattern) {
this.accessLogPattern = accessLogPattern;
}
@Override
public void setAccessLogPrefix(String accessLogPrefix) {
this.accessLogPrefix = accessLogPrefix;
}
@Override
public void setAccessLogSuffix(String accessLogSuffix) {
this.accessLogSuffix = accessLogSuffix;
}
public boolean isAccessLogEnabled() {
return this.accessLogEnabled;
}
@Override
public void setAccessLogEnabled(boolean accessLogEnabled) {
this.accessLogEnabled = accessLogEnabled;
}
@Override
public void setAccessLogRotate(boolean accessLogRotate) {
this.accessLogRotate = accessLogRotate;
}
protected final boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}
@Override
public void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
@Override
public void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}
public void setDirectBuffers(Boolean directBuffers) {
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}

View File

@@ -28,17 +28,8 @@ import javax.servlet.ServletException;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.attribute.RequestHeaderAttribute;
import io.undertow.predicate.Predicate;
import io.undertow.predicate.Predicates;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.encoding.ContentEncodingRepository;
import io.undertow.server.handlers.encoding.EncodingHandler;
import io.undertow.server.handlers.encoding.GzipEncodingProvider;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel;
@@ -47,9 +38,6 @@ import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -216,37 +204,14 @@ public class UndertowServletWebServer implements WebServer {
}
private HttpHandler getContextHandler(HttpHandler httpHandler) {
HttpHandler contextHandler = configurationCompressionIfNecessary(httpHandler);
HttpHandler contextHandler = UndertowCompressionConfigurer
.configureCompression(this.compression, httpHandler);
if (StringUtils.isEmpty(this.contextPath)) {
return contextHandler;
}
return Handlers.path().addPrefixPath(this.contextPath, contextHandler);
}
private HttpHandler configurationCompressionIfNecessary(HttpHandler httpHandler) {
if (this.compression == null || !this.compression.getEnabled()) {
return httpHandler;
}
ContentEncodingRepository repository = new ContentEncodingRepository();
repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50,
Predicates.and(getCompressionPredicates(this.compression)));
return new EncodingHandler(repository).setNext(httpHandler);
}
private Predicate[] getCompressionPredicates(Compression compression) {
List<Predicate> predicates = new ArrayList<>();
predicates.add(new MaxSizePredicate(compression.getMinResponseSize()));
predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes()));
if (compression.getExcludedUserAgents() != null) {
for (String agent : compression.getExcludedUserAgents()) {
RequestHeaderAttribute agentHeader = new RequestHeaderAttribute(
new HttpString(HttpHeaders.USER_AGENT));
predicates.add(Predicates.not(Predicates.regex(agentHeader, agent)));
}
}
return predicates.toArray(new Predicate[predicates.size()]);
}
private String getPortsDescription() {
List<Port> ports = getActualPorts();
if (!ports.isEmpty()) {
@@ -395,54 +360,4 @@ public class UndertowServletWebServer implements WebServer {
}
private static class CompressibleMimeTypePredicate implements Predicate {
private final List<MimeType> mimeTypes;
CompressibleMimeTypePredicate(String[] mimeTypes) {
this.mimeTypes = new ArrayList<>(mimeTypes.length);
for (String mimeTypeString : mimeTypes) {
this.mimeTypes.add(MimeTypeUtils.parseMimeType(mimeTypeString));
}
}
@Override
public boolean resolve(HttpServerExchange value) {
String contentType = value.getResponseHeaders()
.getFirst(HttpHeaders.CONTENT_TYPE);
if (contentType != null) {
for (MimeType mimeType : this.mimeTypes) {
if (mimeType
.isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) {
return true;
}
}
}
return false;
}
}
/**
* Predicate that returns true if the Content-Size of a request is above a given value
* or is missing.
*/
private static class MaxSizePredicate implements Predicate {
private final Predicate maxContentSize;
MaxSizePredicate(int size) {
this.maxContentSize = Predicates.maxContentSize(size);
}
@Override
public boolean resolve(HttpServerExchange value) {
if (value.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) {
return this.maxContentSize.resolve(value);
}
return true;
}
}
}

View File

@@ -85,7 +85,7 @@ import org.springframework.util.Assert;
* @see UndertowServletWebServer
*/
public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory
implements ResourceLoaderAware {
implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware {
private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
@@ -167,11 +167,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return this.builderCustomizers;
}
/**
* Add {@link UndertowBuilderCustomizer}s that should be used to customize the
* Undertow {@link Builder}.
* @param customizers the customizers to add
*/
@Override
public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.builderCustomizers.addAll(Arrays.asList(customizers));
@@ -198,11 +194,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return this.deploymentInfoCustomizers;
}
/**
* Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the
* Undertow {@link DeploymentInfo}.
* @param customizers the customizers to add
*/
@Override
public void addDeploymentInfoCustomizers(
UndertowDeploymentInfoCustomizer... customizers) {
Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null");
@@ -449,10 +441,12 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
this.resourceLoader = resourceLoader;
}
@Override
public void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
@@ -461,14 +455,17 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
this.workerThreads = workerThreads;
}
public void setDirectBuffers(Boolean directBuffers) {
@Override
public void setUseDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}
@Override
public void setAccessLogDirectory(File accessLogDirectory) {
this.accessLogDirectory = accessLogDirectory;
}
@Override
public void setAccessLogPattern(String accessLogPattern) {
this.accessLogPattern = accessLogPattern;
}
@@ -477,14 +474,17 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return this.accessLogPrefix;
}
@Override
public void setAccessLogPrefix(String accessLogPrefix) {
this.accessLogPrefix = accessLogPrefix;
}
@Override
public void setAccessLogSuffix(String accessLogSuffix) {
this.accessLogSuffix = accessLogSuffix;
}
@Override
public void setAccessLogEnabled(boolean accessLogEnabled) {
this.accessLogEnabled = accessLogEnabled;
}
@@ -493,6 +493,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return this.accessLogEnabled;
}
@Override
public void setAccessLogRotate(boolean accessLogRotate) {
this.accessLogRotate = accessLogRotate;
}
@@ -501,11 +502,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
return this.useForwardHeaders;
}
/**
* Set if x-forward-* headers should be processed.
* @param useForwardHeaders if x-forward headers should be used
* @since 1.3.0
*/
@Override
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}