dropwizard/dropwizard

View on GitHub
dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java

Summary

Maintainability
D
2 days
Test Coverage
package io.dropwizard.server;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.jetty9.InstrumentedHandler;
import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool;
import com.codahale.metrics.servlets.AdminServlet;
import com.codahale.metrics.servlets.HealthCheckServlet;
import com.codahale.metrics.servlets.MetricsServlet;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dropwizard.jersey.filter.AllowedMethodsFilter;
import io.dropwizard.jersey.jackson.JacksonFeature;
import io.dropwizard.jersey.setup.JerseyEnvironment;
import io.dropwizard.jersey.validation.HibernateValidationBinder;
import io.dropwizard.jetty.GzipHandlerFactory;
import io.dropwizard.jetty.MutableServletContextHandler;
import io.dropwizard.jetty.ServerPushFilterFactory;
import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
import io.dropwizard.request.logging.LogbackAccessRequestLogFactory;
import io.dropwizard.request.logging.RequestLogFactory;
import io.dropwizard.servlets.ThreadNameFilter;
import io.dropwizard.setup.AdminEnvironment;
import io.dropwizard.setup.ExceptionMapperBinder;
import io.dropwizard.util.Duration;
import io.dropwizard.validation.MinDuration;
import io.dropwizard.validation.ValidationMethod;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.setuid.RLimit;
import org.eclipse.jetty.setuid.SetUIDListener;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;

/**
 * A base class for {@link ServerFactory} implementations.
 * <p/>
 * <b>Configuration Parameters:</b>
 * <table>
 *     <tr>
 *         <td>Name</td>
 *         <td>Default</td>
 *         <td>Description</td>
 *     </tr>
 *     <tr>
 *         <td>{@code requestLog}</td>
 *         <td></td>
 *         <td>The {@link RequestLogFactory request log} configuration.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code gzip}</td>
 *         <td></td>
 *         <td>The {@link GzipHandlerFactory GZIP} configuration.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code serverPush}</td>
 *         <td></td>
 *         <td>The {@link ServerPushFilterFactory} configuration.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxThreads}</td>
 *         <td>1024</td>
 *         <td>The maximum number of threads to use for requests.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code minThreads}</td>
 *         <td>8</td>
 *         <td>The minimum number of threads to use for requests.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxQueuedRequests}</td>
 *         <td>1024</td>
 *         <td>The maximum number of requests to queue before blocking the acceptors.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code idleThreadTimeout}</td>
 *         <td>1 minute</td>
 *         <td>The amount of time a worker thread can be idle before being stopped.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code nofileSoftLimit}</td>
 *         <td>(none)</td>
 *         <td>
 *             The number of open file descriptors before a soft error is issued. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code nofileHardLimit}</td>
 *         <td>(none)</td>
 *         <td>
 *             The number of open file descriptors before a hard error is issued. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code gid}</td>
 *         <td>(none)</td>
 *         <td>
 *             The group ID to switch to once the connectors have started. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code uid}</td>
 *         <td>(none)</td>
 *         <td>
 *             The user ID to switch to once the connectors have started. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code user}</td>
 *         <td>(none)</td>
 *         <td>
 *             The username to switch to once the connectors have started. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code group}</td>
 *         <td>(none)</td>
 *         <td>
 *             The group to switch to once the connectors have started. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code umask}</td>
 *         <td>(none)</td>
 *         <td>
 *             The umask to switch to once the connectors have started. <b>Requires Jetty's
 *             {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code startsAsRoot}</td>
 *         <td>(none)</td>
 *         <td>
 *             Whether or not the Dropwizard application is started as a root user. <b>Requires
 *             Jetty's {@code libsetuid.so} on {@code java.library.path}.</b>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code registerDefaultExceptionMappers}</td>
 *         <td>true</td>
 *         <td>
 *            Whether or not the default Jersey ExceptionMappers should be registered.
 *            Set this to false if you want to register your own.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code shutdownGracePeriod}</td>
 *         <td>30 seconds</td>
 *         <td>
 *             The maximum time to wait for Jetty, and all Managed instances, to cleanly shutdown
 *             before forcibly terminating them.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code allowedMethods}</td>
 *         <td>GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH</td>
 *         <td>
 *             The set of allowed HTTP methods. Others will be rejected with a
 *             405 Method Not Allowed response.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code rootPath}</td>
 *         <td>/*</td>
 *         <td>
 *           The URL pattern relative to {@code applicationContextPath} from which the JAX-RS resources will be served.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code enableThreadNameFilter}</td>
 *         <td>true</td>
 *         <td>
 *           Whether or not to apply the {@code ThreadNameFilter} that adjusts thread names to include the request
 *           method and request URI.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code dumpAfterStart}</td>
 *         <td>true</td>
 *         <td>
 *           Whether or not to dump jetty diagnostics after start.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code dumpBeforeStop}</td>
 *         <td>true</td>
 *         <td>
 *           Whether or not to dump jetty diagnostics before stop.
 *         </td>
 *     </tr>
 * </table>
 *
 * @see DefaultServerFactory
 * @see SimpleServerFactory
 */
public abstract class AbstractServerFactory implements ServerFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(ServerFactory.class);

    @Valid
    @Nullable
    private RequestLogFactory<?> requestLog;

    @Valid
    @NotNull
    private GzipHandlerFactory gzip = new GzipHandlerFactory();

    @Valid
    @NotNull
    private ServerPushFilterFactory serverPush = new ServerPushFilterFactory();

    @Min(4)
    private int maxThreads = 1024;

    @Min(1)
    private int minThreads = 8;

    private int maxQueuedRequests = 1024;

    @MinDuration(1)
    private Duration idleThreadTimeout = Duration.minutes(1);

    @Min(1)
    @Nullable
    private Integer nofileSoftLimit;

    @Min(1)
    @Nullable
    private Integer nofileHardLimit;

    @Nullable
    private Integer gid;

    @Nullable
    private Integer uid;

    @Nullable
    private String user;

    @Nullable
    private String group;

    @Nullable
    private String umask;

    @Nullable
    private Boolean startsAsRoot;

    private Boolean registerDefaultExceptionMappers = Boolean.TRUE;

    private Boolean detailedJsonProcessingExceptionMapper = Boolean.FALSE;

    private Duration shutdownGracePeriod = Duration.seconds(30);

    @NotNull
    private Set<String> allowedMethods = AllowedMethodsFilter.DEFAULT_ALLOWED_METHODS;

    private Optional<String> jerseyRootPath = Optional.empty();

    private boolean enableThreadNameFilter = true;

    private boolean dumpAfterStart = false;

    private boolean dumpBeforeStop = false;

    @JsonIgnore
    @ValidationMethod(message = "must have a smaller minThreads than maxThreads")
    public boolean isThreadPoolSizedCorrectly() {
        return minThreads <= maxThreads;
    }

    @JsonProperty("requestLog")
    public synchronized RequestLogFactory<?> getRequestLogFactory() {
        if (requestLog == null) {
            // Lazy init to avoid a hard dependency to logback
            requestLog = new LogbackAccessRequestLogFactory();
        }
        return requestLog;
    }

    @JsonProperty("requestLog")
    public synchronized void setRequestLogFactory(RequestLogFactory<?> requestLog) {
        this.requestLog = requestLog;
    }

    @JsonProperty("gzip")
    public GzipHandlerFactory getGzipFilterFactory() {
        return gzip;
    }

    @JsonProperty("gzip")
    public void setGzipFilterFactory(GzipHandlerFactory gzip) {
        this.gzip = gzip;
    }

    @JsonProperty("serverPush")
    public ServerPushFilterFactory getServerPush() {
        return serverPush;
    }

    @JsonProperty("serverPush")
    public void setServerPush(ServerPushFilterFactory serverPush) {
        this.serverPush = serverPush;
    }

    @JsonProperty
    public int getMaxThreads() {
        return maxThreads;
    }

    @JsonProperty
    public void setMaxThreads(int count) {
        this.maxThreads = count;
    }

    @JsonProperty
    public int getMinThreads() {
        return minThreads;
    }

    @JsonProperty
    public void setMinThreads(int count) {
        this.minThreads = count;
    }

    @JsonProperty
    public int getMaxQueuedRequests() {
        return maxQueuedRequests;
    }

    @JsonProperty
    public void setMaxQueuedRequests(int maxQueuedRequests) {
        this.maxQueuedRequests = maxQueuedRequests;
    }

    @JsonProperty
    public Duration getIdleThreadTimeout() {
        return idleThreadTimeout;
    }

    @JsonProperty
    public void setIdleThreadTimeout(Duration idleThreadTimeout) {
        this.idleThreadTimeout = idleThreadTimeout;
    }

    @JsonProperty
    @Nullable
    public Integer getNofileSoftLimit() {
        return nofileSoftLimit;
    }

    @JsonProperty
    public void setNofileSoftLimit(Integer nofileSoftLimit) {
        this.nofileSoftLimit = nofileSoftLimit;
    }

    @JsonProperty
    @Nullable
    public Integer getNofileHardLimit() {
        return nofileHardLimit;
    }

    @JsonProperty
    public void setNofileHardLimit(Integer nofileHardLimit) {
        this.nofileHardLimit = nofileHardLimit;
    }

    @JsonProperty
    @Nullable
    public Integer getGid() {
        return gid;
    }

    @JsonProperty
    public void setGid(Integer gid) {
        this.gid = gid;
    }

    @JsonProperty
    @Nullable
    public Integer getUid() {
        return uid;
    }

    @JsonProperty
    public void setUid(Integer uid) {
        this.uid = uid;
    }

    @JsonProperty
    @Nullable
    public String getUser() {
        return user;
    }

    @JsonProperty
    public void setUser(String user) {
        this.user = user;
    }

    @JsonProperty
    @Nullable
    public String getGroup() {
        return group;
    }

    @JsonProperty
    public void setGroup(String group) {
        this.group = group;
    }

    @JsonProperty
    @Nullable
    public String getUmask() {
        return umask;
    }

    @JsonProperty
    public void setUmask(String umask) {
        this.umask = umask;
    }

    @JsonProperty
    @Nullable
    public Boolean getStartsAsRoot() {
        return startsAsRoot;
    }

    @JsonProperty
    public void setStartsAsRoot(Boolean startsAsRoot) {
        this.startsAsRoot = startsAsRoot;
    }

    public Boolean getRegisterDefaultExceptionMappers() {
        return registerDefaultExceptionMappers;
    }

    @JsonProperty
    public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionMappers) {
        this.registerDefaultExceptionMappers = registerDefaultExceptionMappers;
    }

    public Boolean getDetailedJsonProcessingExceptionMapper() {
        return detailedJsonProcessingExceptionMapper;
    }

    @JsonProperty
    public void setDetailedJsonProcessingExceptionMapper(Boolean detailedJsonProcessingExceptionMapper) {
        this.detailedJsonProcessingExceptionMapper = detailedJsonProcessingExceptionMapper;
    }

    @JsonProperty
    public Duration getShutdownGracePeriod() {
        return shutdownGracePeriod;
    }

    @JsonProperty
    public void setShutdownGracePeriod(Duration shutdownGracePeriod) {
        this.shutdownGracePeriod = shutdownGracePeriod;
    }

    @JsonProperty
    public Set<String> getAllowedMethods() {
        return allowedMethods;
    }

    @JsonProperty
    public void setAllowedMethods(Set<String> allowedMethods) {
        this.allowedMethods = allowedMethods;
    }

    @JsonProperty("rootPath")
    public Optional<String> getJerseyRootPath() {
        return jerseyRootPath;
    }

    @JsonProperty("rootPath")
    public void setJerseyRootPath(String jerseyRootPath) {
        this.jerseyRootPath = Optional.ofNullable(jerseyRootPath);
    }

    @JsonProperty
    public boolean getEnableThreadNameFilter() {
        return enableThreadNameFilter;
    }

    @JsonProperty
    public void setEnableThreadNameFilter(boolean enableThreadNameFilter) {
        this.enableThreadNameFilter = enableThreadNameFilter;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public boolean getDumpAfterStart() {
        return dumpAfterStart;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public void setDumpAfterStart(boolean dumpAfterStart) {
        this.dumpAfterStart = dumpAfterStart;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public boolean getDumpBeforeStop() {
        return dumpBeforeStop;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public void setDumpBeforeStop(boolean dumpBeforeStop) {
        this.dumpBeforeStop = dumpBeforeStop;
    }

    protected Handler createAdminServlet(Server server,
                                         MutableServletContextHandler handler,
                                         MetricRegistry metrics,
                                         HealthCheckRegistry healthChecks,
                                         AdminEnvironment admin) {
        configureSessionsAndSecurity(handler, server);
        handler.setServer(server);
        handler.getServletContext().setAttribute(MetricsServlet.METRICS_REGISTRY, metrics);
        handler.getServletContext().setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, healthChecks);
        handler.getServletContext().setAttribute(AdminServlet.HEALTHCHECK_ENABLED_PARAM_KEY, admin.isHealthCheckServletEnabled());
        handler.addServlet(AdminServlet.class, "/*");
        final String allowedMethodsParam = String.join(",", allowedMethods);
        handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
                .setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, allowedMethodsParam);
        return handler;
    }

    private void configureSessionsAndSecurity(MutableServletContextHandler handler, Server server) {
        handler.setServer(server);
        if (handler.isSecurityEnabled()) {
            handler.getSecurityHandler().setServer(server);
        }
        if (handler.isSessionsEnabled()) {
            handler.getSessionHandler().setServer(server);
        }
    }

    protected Handler createAppServlet(Server server,
                                       JerseyEnvironment jersey,
                                       ObjectMapper objectMapper,
                                       Validator validator,
                                       MutableServletContextHandler handler,
                                       @Nullable Servlet jerseyContainer,
                                       MetricRegistry metricRegistry) {
        configureSessionsAndSecurity(handler, server);
        final String allowedMethodsParam = String.join(",", allowedMethods);
        handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
                .setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, allowedMethodsParam);
        if (enableThreadNameFilter) {
            handler.addFilter(ThreadNameFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
        }
        serverPush.addFilter(handler);
        if (jerseyContainer != null) {
            jerseyRootPath.ifPresent(jersey::setUrlPattern);
            jersey.register(new JacksonFeature(objectMapper));
            jersey.register(new HibernateValidationBinder(validator));
            if (registerDefaultExceptionMappers == null || registerDefaultExceptionMappers) {
                jersey.register(new ExceptionMapperBinder(detailedJsonProcessingExceptionMapper));
            }
            handler.addServlet(new ServletHolder("jersey", jerseyContainer), jersey.getUrlPattern());
        }
        final InstrumentedHandler instrumented = new InstrumentedHandler(metricRegistry);
        instrumented.setServer(server);
        instrumented.setHandler(handler);
        return instrumented;
    }

    protected ThreadPool createThreadPool(MetricRegistry metricRegistry) {
        final BlockingQueue<Runnable> queue = new BlockingArrayQueue<>(minThreads, maxThreads, maxQueuedRequests);
        final InstrumentedQueuedThreadPool threadPool =
                new InstrumentedQueuedThreadPool(metricRegistry, maxThreads, minThreads,
                                                 (int) idleThreadTimeout.toMilliseconds(), queue);
        threadPool.setName("dw");
        return threadPool;
    }

    protected Server buildServer(LifecycleEnvironment lifecycle,
                                 ThreadPool threadPool) {
        final Server server = new Server(threadPool);
        server.addLifeCycleListener(buildSetUIDListener());
        lifecycle.attach(server);
        final ErrorHandler errorHandler = new ErrorHandler();
        errorHandler.setServer(server);
        errorHandler.setShowStacks(false);
        server.addBean(errorHandler);
        server.setStopAtShutdown(true);
        server.setStopTimeout(shutdownGracePeriod.toMilliseconds());
        server.setDumpAfterStart(dumpAfterStart);
        server.setDumpBeforeStop(dumpBeforeStop);
        return server;
    }

    protected SetUIDListener buildSetUIDListener() {
        final SetUIDListener listener = new SetUIDListener();

        if (startsAsRoot != null) {
            listener.setStartServerAsPrivileged(startsAsRoot);
        }

        if (gid != null) {
            listener.setGid(gid);
        }

        if (uid != null) {
            listener.setUid(uid);
        }

        if (user != null) {
            listener.setUsername(user);
        }

        if (group != null) {
            listener.setGroupname(group);
        }

        if (nofileHardLimit != null || nofileSoftLimit != null) {
            final RLimit rlimit = new RLimit();
            if (nofileHardLimit != null) {
                rlimit.setHard(nofileHardLimit);
            }

            if (nofileSoftLimit != null) {
                rlimit.setSoft(nofileSoftLimit);
            }

            listener.setRLimitNoFiles(rlimit);
        }

        if (umask != null) {
            listener.setUmaskOctal(umask);
        }

        return listener;
    }

    protected Handler addRequestLog(Server server, Handler handler, String name) {
        if (getRequestLogFactory().isEnabled()) {
            final RequestLogHandler requestLogHandler = new RequestLogHandler();
            requestLogHandler.setRequestLog(getRequestLogFactory().build(name));
            // server should own the request log's lifecycle since it's already started,
            // the handler might not become managed in case of an error which would leave
            // the request log stranded
            server.addBean(requestLogHandler.getRequestLog(), true);
            requestLogHandler.setHandler(handler);
            return requestLogHandler;
        }
        return handler;
    }

    protected Handler addStatsHandler(Handler handler) {
        // Graceful shutdown is implemented via the statistics handler,
        // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
        final StatisticsHandler statisticsHandler = new StatisticsHandler();
        statisticsHandler.setHandler(handler);
        return statisticsHandler;
    }

    protected Handler buildGzipHandler(Handler handler) {
        return gzip.isEnabled() ? gzip.build(handler) : handler;
    }

    @SuppressWarnings("Slf4jFormatShouldBeConst")
    protected void printBanner(String name) {
        String msg = "Starting " + name;
        final URL resource = Thread.currentThread().getContextClassLoader().getResource("banner.txt");
        if (resource != null) {
            try (final InputStream resourceStream = resource.openStream();
                 final InputStreamReader inputStreamReader = new InputStreamReader(resourceStream);
                 final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
                final String banner = bufferedReader
                        .lines()
                        .collect(Collectors.joining(System.lineSeparator()));
                msg = String.format("Starting %s%n%s", name, banner);
            } catch (IllegalArgumentException | IOException ignored) {
            }
        }
        LOGGER.info(msg);
    }
}