Repository: tipsy/javalin
Pull request #517 (2019-03-24 10:03:10)

[core] Make JavalinServlet standalone

tipsy: @mvysny (and maybe @ernestas2k?) I would appreciate it if you could review this change.

Unfortunately it's a bit messy, but what it does is:

* Moves JavalinServlet's dependencies into itself
* Creates a JavalinServer for the server config, (removes JettyServerUtil)
* Creates a JavalinWsServlet for the websocket servlet
* Removes path-insensitive routing
* Removes the concept of context-paths from the servlet - the server/servlet-container itself should handle this
* Misc smaller changes


mvysny: Looks really good, I like that the hacks were largely removed. If I understand correctly, now nothing depends on `JavalinServer` and this class can therefore be moved to a separate module along with Jetty.
tipsy: There are still a few imports:

```
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
```

If we do the split, then we need to create a `BaseJavalin` and `Javalin` as discussed before. If we don't split, we could create a `JavalinServletBuilder`: 

```
class JavalinServletBuilder(val javalin: Javalin) {

    fun accessManager(accessManager: AccessManager): JavalinServletBuilder {
        javalin.accessManager(accessManager)
        return this;
    }

    fun <T : Exception> exception(exceptionClass: Class<T>, exceptionHandler: ExceptionHandler<in T>): JavalinServletBuilder {
        javalin.exception(exceptionClass, exceptionHandler as ExceptionHandler<Exception>)
        return this
    }

    fun error(statusCode: Int, errorHandler: ErrorHandler): JavalinServletBuilder {
        javalin.error(statusCode, errorHandler)
        return this
    }

    fun addHandler(handlerType: HandlerType, path: String, handler: Handler, roles: Set<Role>): JavalinServletBuilder {
        this.javalin.addHandler(handlerType, path, handler, roles)
        return this
    }

    fun addHandler(httpMethod: HandlerType, path: String, handler: Handler) = addHandler(httpMethod, path, handler, HashSet())
    
    fun get(path: String, handler: Handler): JavalinServletBuilder = addHandler(HandlerType.GET, path, handler)
    fun post(path: String, handler: Handler) =addHandler(HandlerType.POST, path, handler)
    fun put(path: String, handler: Handler) = addHandler(HandlerType.PUT, path, handler)
    fun patch(path: String, handler: Handler) = addHandler(HandlerType.PATCH, path, handler)
    fun delete(path: String, handler: Handler) = addHandler(HandlerType.DELETE, path, handler)

}
```

Patch:
diff --git a/src/main/java/io/javalin/Context.kt b/src/main/java/io/javalin/Context.kt
index fbe69331..bb47a54a 100644
--- a/src/main/java/io/javalin/Context.kt
+++ b/src/main/java/io/javalin/Context.kt
@@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletResponse
  *
  * @see <a href="https://javalin.io/documentation#context">Context in docs</a>
  */
-open class Context(private val servletRequest: HttpServletRequest, private val servletResponse: HttpServletResponse, private val javalin: Javalin) {
+open class Context(private val servletRequest: HttpServletRequest, private val servletResponse: HttpServletResponse, private val appAttributes: Map<Class<*>, Any>) {
 
     // @formatter:off
     @get:JvmSynthetic @set:JvmSynthetic internal var inExceptionHandler = false
@@ -46,7 +46,7 @@ open class Context(private val servletRequest: HttpServletRequest, private val s
     private var resultFuture: CompletableFuture<*>? = null
 
     /** Gets an attribute from the Javalin instance serving the request */
-    fun <T> appAttribute(clazz: Class<T>): T = javalin.attribute(clazz) as T
+    fun <T> appAttribute(clazz: Class<T>): T = appAttributes[clazz] as T
 
     /**
      * Gets cookie store value for specified key.
diff --git a/src/main/java/io/javalin/EmbeddedJavalin.kt b/src/main/java/io/javalin/EmbeddedJavalin.kt
deleted file mode 100644
index c9b5e7f9..00000000
--- a/src/main/java/io/javalin/EmbeddedJavalin.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package io.javalin
-
-import io.javalin.core.JavalinServlet
-import io.javalin.core.util.JettyServerUtil
-import io.javalin.staticfiles.Location
-import io.javalin.websocket.WsHandler
-import org.eclipse.jetty.server.Server
-import org.eclipse.jetty.server.session.SessionHandler
-import java.util.function.Consumer
-import java.util.function.Supplier
-
-/**
- * Use this class instead of [Javalin] to embed Javalin into servlet containers such as Tomcat. Instantiating this class
- * allows you to exclude all Jetty dependencies and tie Javalin to a servlet as follows:
- *
- * ```
- * @WebServlet(urlPatterns = ["/rest/*"], name = "MyServlet", asyncSupported = false)  // */
- * class MyServlet : HttpServlet() {
- *     val javalin = EmbeddedJavalin()
- *         .get("/rest") { ctx -> ctx.result("Hello!") }
- *         .createServlet()
- *
- *     override fun service(req: HttpServletRequest, resp: HttpServletResponse) {
- *         javalin.service(req, resp)
- *     }
- * }
- * ```
- *
- * Note that static files and uploads will not work without Jetty.
- */
-class EmbeddedJavalin : Javalin(null, null) {
-
-    init {
-        JettyServerUtil.noJettyStarted = false // embeddable doesn't use Jetty
-    }
-
-    override fun createServlet() = JavalinServlet(
-            javalin = this,
-            matcher = pathMatcher,
-            exceptionMapper = exceptionMapper,
-            errorMapper = errorMapper,
-            debugLogging = debugLogging,
-            requestLogger = requestLogger,
-            dynamicGzipEnabled = dynamicGzipEnabled,
-            autogeneratedEtagsEnabled = autogeneratedEtagsEnabled,
-            defaultContentType = defaultContentType,
-            maxRequestCacheBodySize = maxRequestCacheBodySize,
-            prefer405over404 = prefer405over404,
-            singlePageHandler = singlePageHandler,
-            resourceHandler = null // no jetty here
-    )
-
-    override fun enableMicrometer() = notAvailable("enableMicrometer()")
-    override fun enableStaticFiles(path: String, location: Location) = notAvailable("enableStaticFiles()")
-    override fun enableStaticFiles(classpathPath: String) = notAvailable("enableStaticFiles()")
-    override fun enableWebJars() = notAvailable("enableWebJars()")
-    override fun port() = notAvailable("port()")
-    override fun port(port: Int) = notAvailable("port(port)")
-    override fun server(server: Supplier<Server>) = notAvailable("server()")
-    override fun sessionHandler(sessionHandler: Supplier<SessionHandler>) = notAvailable("sessionHandler()")
-    override fun start() = notAvailable("start()")
-    override fun stop() = notAvailable("stop()")
-    override fun ws(path: String, ws: Consumer<WsHandler>) = notAvailable("WebSockets functionality")
-    override fun wsLogger(ws: Consumer<WsHandler>) = notAvailable("WebSockets functionality")
-    private fun notAvailable(action: String): Nothing = throw RuntimeException("$action is not available in standalone mode")
-}
diff --git a/src/main/java/io/javalin/Javalin.java b/src/main/java/io/javalin/Javalin.java
index de0587c2..4ad7e397 100644
--- a/src/main/java/io/javalin/Javalin.java
+++ b/src/main/java/io/javalin/Javalin.java
@@ -9,33 +9,27 @@ package io.javalin;
 
 import io.javalin.apibuilder.ApiBuilder;
 import io.javalin.apibuilder.EndpointGroup;
-import io.javalin.core.ErrorMapper;
 import io.javalin.core.EventManager;
-import io.javalin.core.ExceptionMapper;
-import io.javalin.core.HandlerEntry;
 import io.javalin.core.HandlerType;
+import io.javalin.core.JavalinServer;
 import io.javalin.core.JavalinServlet;
-import io.javalin.core.PathMatcher;
+import io.javalin.core.JettyUtil;
 import io.javalin.core.util.CorsBeforeHandler;
 import io.javalin.core.util.CorsOptionsHandler;
-import io.javalin.core.util.JettyServerUtil;
 import io.javalin.core.util.LogUtil;
 import io.javalin.core.util.RouteOverviewRenderer;
-import io.javalin.core.util.SinglePageHandler;
 import io.javalin.core.util.Util;
-import io.javalin.metrics.JavalinMicrometer;
 import io.javalin.security.AccessManager;
 import io.javalin.security.CoreRoles;
 import io.javalin.security.Role;
-import io.javalin.security.SecurityUtil;
 import io.javalin.serversentevent.SseClient;
 import io.javalin.serversentevent.SseHandler;
 import io.javalin.staticfiles.JettyResourceHandler;
 import io.javalin.staticfiles.Location;
 import io.javalin.staticfiles.StaticFileConfig;
+import io.javalin.websocket.JavalinWsServlet;
 import io.javalin.websocket.WsEntry;
 import io.javalin.websocket.WsHandler;
-import io.javalin.websocket.WsPathMatcher;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -56,42 +50,18 @@ public class Javalin {
 
     private static Logger log = LoggerFactory.getLogger(Javalin.class);
 
-    protected Server jettyServer;
-    protected SessionHandler jettySessionHandler;
-    protected Set<StaticFileConfig> staticFileConfig = new HashSet<>();
-    protected boolean ignoreTrailingSlashes = true;
-
-    protected int port = 7000;
-    protected String contextPath = "/";
-    protected String defaultContentType = "text/plain";
-    protected long maxRequestCacheBodySize = 4096;
-    protected boolean debugLogging = false;
-    protected boolean dynamicGzipEnabled = true;
-    protected boolean autogeneratedEtagsEnabled = false;
     protected boolean showStartupBanner = true;
-    protected boolean prefer405over404 = false;
-    protected boolean caseSensitiveUrls = false;
-    protected boolean started = false;
-    protected AccessManager accessManager = SecurityUtil::noopAccessManager;
-    protected RequestLogger requestLogger = null;
-
-    protected SinglePageHandler singlePageHandler = new SinglePageHandler();
-    protected PathMatcher pathMatcher = new PathMatcher();
-    protected WsPathMatcher wsPathMatcher = new WsPathMatcher();
-    protected ExceptionMapper exceptionMapper = new ExceptionMapper();
-    protected ErrorMapper errorMapper = new ErrorMapper();
+
     protected EventManager eventManager = new EventManager();
+    protected Map<Class<?>, Object> appAttributes = new HashMap<>();
     protected List<HandlerMetaInfo> handlerMetaInfo = new ArrayList<>();
-    protected Map<Class, Object> appAttributes = new HashMap<>();
-    protected Consumer<WebSocketServletFactory> wsFactoryConfig = WebSocketServletFactory::getPolicy;
 
-    protected Javalin(Server jettyServer, SessionHandler jettySessionHandler) {
-        this.jettyServer = jettyServer;
-        this.jettySessionHandler = jettySessionHandler;
-    }
+    private JavalinServer server = new JavalinServer();
+    private JavalinServlet servlet = new JavalinServlet(appAttributes);
+    private JavalinWsServlet wsServlet = new JavalinWsServlet();
 
     protected Javalin() {
-        this(JettyServerUtil.defaultServer(), JettyServerUtil.defaultSessionHandler());
+        Util.logWarningIfNotStartedAfterOneSecond(this.server);
     }
 
     /**
@@ -103,7 +73,6 @@ public class Javalin {
      * @see Javalin#start(int)
      */
     public static Javalin create() {
-        JettyServerUtil.INSTANCE.printHelpfulMessageIfNoServerHasBeenStartedAfterOneSecond();
         return new Javalin();
     }
 
@@ -127,62 +96,30 @@ public class Javalin {
      */
     public Javalin start() {
         long startupTimer = System.currentTimeMillis();
-        if (started) {
+        if (server.getStarted()) {
             throw new IllegalStateException("Cannot call start() again on a started server.");
         }
-        if (showStartupBanner) {
-            log.info(Util.INSTANCE.javalinBanner());
-        }
-        Util.INSTANCE.printHelpfulMessageIfLoggerIsMissing();
+        Util.logJavalinBanner(showStartupBanner);
+        Util.printHelpfulMessageIfLoggerIsMissing();
         eventManager.fireEvent(JavalinEvent.SERVER_STARTING);
         try {
             log.info("Starting Javalin ...");
-            port = JettyServerUtil.initialize(
-                jettyServer,
-                jettySessionHandler,
-                port,
-                contextPath,
-                createServlet(),
-                wsPathMatcher,
-                wsFactoryConfig
-            );
+            server.start(servlet, wsServlet);
             log.info("Javalin started in " + (System.currentTimeMillis() - startupTimer) + "ms \\o/");
-            started = true;
-            JettyServerUtil.INSTANCE.setNoJettyStarted(false);
             eventManager.fireEvent(JavalinEvent.SERVER_STARTED);
         } catch (Exception e) {
             log.error("Failed to start Javalin");
             eventManager.fireEvent(JavalinEvent.SERVER_START_FAILED);
             if (e.getMessage() != null && e.getMessage().contains("Failed to bind to")) {
-                throw new RuntimeException("Port already in use. Make sure no other process is using port " + port + " and try again.", e);
+                throw new RuntimeException("Port already in use. Make sure no other process is using port " + server.getJettyPort() + " and try again.", e);
             } else if (e.getMessage() != null && e.getMessage().contains("Permission denied")) {
                 throw new RuntimeException("Port 1-1023 require elevated privileges (process must be started by admin).", e);
             }
             throw new RuntimeException(e);
         }
-        JettyServerUtil.reEnableJettyLogger();
         return this;
     }
 
-    @NotNull
-    public JavalinServlet createServlet() {
-        return new JavalinServlet(
-            this,
-            pathMatcher,
-            exceptionMapper,
-            errorMapper,
-            debugLogging,
-            requestLogger,
-            dynamicGzipEnabled,
-            autogeneratedEtagsEnabled,
-            defaultContentType,
-            maxRequestCacheBodySize,
-            prefer405over404,
-            singlePageHandler,
-            new JettyResourceHandler(staticFileConfig, jettyServer, ignoreTrailingSlashes)
-        );
-    }
-
     /**
      * Synchronously stops the application instance.
      *
@@ -192,7 +129,7 @@ public class Javalin {
         eventManager.fireEvent(JavalinEvent.SERVER_STOPPING);
         log.info("Stopping Javalin ...");
         try {
-            jettyServer.stop();
+            server.getJettyServer().stop();
         } catch (Exception e) {
             log.error("Javalin failed to stop gracefully", e);
         }
@@ -207,17 +144,7 @@ public class Javalin {
      */
     public Javalin prefer405over404() {
         ensureActionIsPerformedBeforeServerStart("Telling Javalin to return 405 instead of 404 when applicable");
-        prefer405over404 = true;
-        return this;
-    }
-
-    /**
-     * Configure the instance to not use lower-case paths for path matching and parsing.
-     * The method must be called before {@link Javalin#start()}.
-     */
-    public Javalin enableCaseSensitiveUrls() {
-        ensureActionIsPerformedBeforeServerStart("Enabling case sensitive urls");
-        caseSensitiveUrls = true;
+        servlet.setPrefer405over404(true);
         return this;
     }
 
@@ -237,8 +164,7 @@ public class Javalin {
      */
     public Javalin dontIgnoreTrailingSlashes() {
         ensureActionIsPerformedBeforeServerStart("Telling Javalin to not ignore slashes");
-        pathMatcher.setIgnoreTrailingSlashes(false);
-        ignoreTrailingSlashes = false;
+        servlet.getMatcher().setIgnoreTrailingSlashes(false);
         return this;
     }
 
@@ -250,7 +176,7 @@ public class Javalin {
      */
     public Javalin server(@NotNull Supplier<Server> server) {
         ensureActionIsPerformedBeforeServerStart("Setting a custom server");
-        jettyServer = server.get();
+        this.server.setJettyServer(server.get());
         return this;
     }
 
@@ -260,7 +186,7 @@ public class Javalin {
      */
     public Javalin sessionHandler(@NotNull Supplier<SessionHandler> sessionHandler) {
         ensureActionIsPerformedBeforeServerStart("Setting a custom session handler");
-        jettySessionHandler = JettyServerUtil.INSTANCE.getSessionHandler(sessionHandler);
+        server.setJettySessionHandler(JettyUtil.getSessionHandler(sessionHandler));
         return this;
     }
 
@@ -270,7 +196,7 @@ public class Javalin {
      */
     public Javalin wsFactoryConfig(@NotNull Consumer<WebSocketServletFactory> wsFactoryConfig) {
         ensureActionIsPerformedBeforeServerStart("Setting a custom WebSocket factory config");
-        this.wsFactoryConfig = wsFactoryConfig;
+        wsServlet.setWsFactoryConfig(wsFactoryConfig);
         return this;
     }
 
@@ -294,7 +220,10 @@ public class Javalin {
      */
     public Javalin enableStaticFiles(@NotNull String path, @NotNull Location location) {
         ensureActionIsPerformedBeforeServerStart("Enabling static files");
-        staticFileConfig.add(new StaticFileConfig(path, location));
+        if (servlet.getResourceHandler() == null) {
+            servlet.setResourceHandler(new JettyResourceHandler());
+        }
+        servlet.getResourceHandler().addStaticFileConfig(new StaticFileConfig(path, location));
         return this;
     }
 
@@ -322,7 +251,7 @@ public class Javalin {
      */
     public Javalin enableSinglePageMode(@NotNull String path, @NotNull String filePath, @NotNull Location location) {
         ensureActionIsPerformedBeforeServerStart("Enabling single page mode");
-        singlePageHandler.add(path, filePath, location);
+        servlet.getSinglePageHandler().add(path, filePath, location);
         return this;
     }
 
@@ -332,7 +261,7 @@ public class Javalin {
      */
     public Javalin contextPath(@NotNull String contextPath) {
         ensureActionIsPerformedBeforeServerStart("Setting the context path");
-        this.contextPath = Util.INSTANCE.normalizeContextPath(contextPath);
+        server.setContextPath(Util.normalizeContextPath(contextPath));
         return this;
     }
 
@@ -341,7 +270,7 @@ public class Javalin {
      * Mostly useful if you start the instance with port(0) (random port)
      */
     public int port() {
-        return port;
+        return server.getJettyPort();
     }
 
     /**
@@ -350,7 +279,7 @@ public class Javalin {
      */
     public Javalin port(int port) {
         ensureActionIsPerformedBeforeServerStart("Setting the port");
-        this.port = port;
+        server.setJettyPort(port);
         return this;
     }
 
@@ -358,31 +287,21 @@ public class Javalin {
      * Configure instance to log debug information for each request.
      * The method must be called before {@link Javalin#start()}.
      */
-    public Javalin enableDebugLogging() {
+    public Javalin enableDevLogging() {
         ensureActionIsPerformedBeforeServerStart("Enabling debug-logging");
-        this.debugLogging = true;
-        wsLogger(LogUtil::wsDebugLogger);
+        requestLogger(LogUtil::requestDevLogger);
+        wsLogger(LogUtil::wsDevLogger);
         return this;
     }
 
     /**
      * Configure instance use specified request-logger
      * The method must be called before {@link Javalin#start()}.
-     * Will override the default logger of {@link Javalin#enableDebugLogging()}.
+     * Will override the default logger of {@link Javalin#enableDevLogging()}.
      */
     public Javalin requestLogger(@NotNull RequestLogger requestLogger) {
         ensureActionIsPerformedBeforeServerStart("Setting a custom request logger");
-        this.requestLogger = requestLogger;
-        return this;
-    }
-
-    /**
-     * Configure instance to enable application metrics via {@link JavalinMicrometer}
-     * The method must be called before {@link Javalin#start()}.
-     * Make sure you add the required dependencies (micrometer core + your preferred registry)
-     */
-    public Javalin enableMicrometer() {
-        JavalinMicrometer.init(this.jettyServer);
+        this.servlet.setRequestLogger(requestLogger);
         return this;
     }
 
@@ -413,7 +332,7 @@ public class Javalin {
      */
     public Javalin disableDynamicGzip() {
         ensureActionIsPerformedBeforeServerStart("Disabling dynamic GZIP");
-        this.dynamicGzipEnabled = false;
+        servlet.setDynamicGzipEnabled(false);
         return this;
     }
 
@@ -425,7 +344,7 @@ public class Javalin {
      */
     public Javalin enableAutogeneratedEtags() {
         ensureActionIsPerformedBeforeServerStart("Enabling autogenerated etags");
-        this.autogeneratedEtagsEnabled = true;
+        servlet.setAutogeneratedEtagsEnabled(true);
         return this;
     }
 
@@ -455,7 +374,7 @@ public class Javalin {
      */
     public Javalin defaultContentType(@NotNull String contentType) {
         ensureActionIsPerformedBeforeServerStart("Changing default content type");
-        this.defaultContentType = contentType;
+        servlet.setDefaultContentType(contentType);
         return this;
     }
 
@@ -466,7 +385,7 @@ public class Javalin {
      */
     public Javalin maxBodySizeForRequestCache(long bodySizeInBytes) {
         ensureActionIsPerformedBeforeServerStart("Changing request cache body size");
-        this.maxRequestCacheBodySize = bodySizeInBytes;
+        servlet.setMaxRequestCacheBodySize(bodySizeInBytes);
         return this;
     }
 
@@ -503,7 +422,7 @@ public class Javalin {
     }
 
     private void ensureActionIsPerformedBeforeServerStart(@NotNull String action) {
-        if (started) {
+        if (server.getStarted()) {
             throw new IllegalStateException(action + " must be done before starting the server.");
         }
     }
@@ -517,7 +436,7 @@ public class Javalin {
      */
     public Javalin accessManager(@NotNull AccessManager accessManager) {
         ensureActionIsPerformedBeforeServerStart("Setting an AccessManager");
-        this.accessManager = accessManager;
+        this.servlet.setAccessManager(accessManager);
         return this;
     }
 
@@ -528,7 +447,7 @@ public class Javalin {
      * @see <a href="https://javalin.io/documentation#exception-mapping">Exception mapping in docs</a>
      */
     public <T extends Exception> Javalin exception(@NotNull Class<T> exceptionClass, @NotNull ExceptionHandler<? super T> exceptionHandler) {
-        exceptionMapper.getExceptionMap().put(exceptionClass, (ExceptionHandler<Exception>) exceptionHandler);
+        servlet.getExceptionMapper().getExceptionMap().put(exceptionClass, (ExceptionHandler<Exception>) exceptionHandler);
         return this;
     }
 
@@ -547,13 +466,13 @@ public class Javalin {
     /**
      * Configures a web socket handler to be called after every web socket event
      * The method must be called before {@link Javalin#start()}.
-     * Will override the default logger of {@link Javalin#enableDebugLogging()}.
+     * Will override the default logger of {@link Javalin#enableDevLogging()}.
      */
     public Javalin wsLogger(@NotNull Consumer<WsHandler> ws) {
         ensureActionIsPerformedBeforeServerStart("Adding a WebSocket logger");
         WsHandler wsLogger = new WsHandler();
         ws.accept(wsLogger);
-        wsPathMatcher.setWsLogger(wsLogger);
+        wsServlet.getWsPathMatcher().setWsLogger(wsLogger);
         return this;
     }
 
@@ -564,7 +483,7 @@ public class Javalin {
      * @see <a href="https://javalin.io/documentation#error-mapping">Error mapping in docs</a>
      */
     public Javalin error(int statusCode, @NotNull ErrorHandler errorHandler) {
-        errorMapper.getErrorHandlerMap().put(statusCode, errorHandler);
+        servlet.getErrorMapper().getErrorHandlerMap().put(statusCode, errorHandler);
         return this;
     }
 
@@ -601,11 +520,8 @@ public class Javalin {
      * @see <a href="https://javalin.io/documentation#handlers">Handlers in docs</a>
      */
     public Javalin addHandler(@NotNull HandlerType handlerType, @NotNull String path, @NotNull Handler handler, @NotNull Set<Role> roles) {
-        boolean shouldWrap = handlerType.isHttpMethod() && !roles.contains(CoreRoles.NO_WRAP); // don't wrap CORS options
-        String prefixedPath = Util.prefixContextPath(contextPath, path);
-        Handler protectedHandler = shouldWrap ? ctx -> accessManager.manage(handler, ctx, roles) : handler;
-        pathMatcher.add(new HandlerEntry(handlerType, prefixedPath, protectedHandler, handler, caseSensitiveUrls));
-        handlerMetaInfo.add(new HandlerMetaInfo(handlerType, prefixedPath, handler, roles));
+        servlet.addHandler(handlerType, path, handler, roles);
+        handlerMetaInfo.add(new HandlerMetaInfo(handlerType, Util.prefixContextPath(server.getContextPath(), path), handler, roles));
         return this;
     }
 
@@ -856,11 +772,10 @@ public class Javalin {
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
     public Javalin ws(@NotNull String path, @NotNull Consumer<WsHandler> ws) {
-        String prefixedPath = Util.prefixContextPath(contextPath, path);
         WsHandler configuredWebSocket = new WsHandler();
         ws.accept(configuredWebSocket);
-        wsPathMatcher.add(new WsEntry(prefixedPath, configuredWebSocket, caseSensitiveUrls));
-        handlerMetaInfo.add(new HandlerMetaInfo(HandlerType.WEBSOCKET, prefixedPath, ws, new HashSet<>()));
+        wsServlet.getWsPathMatcher().add(new WsEntry(path, configuredWebSocket));
+        handlerMetaInfo.add(new HandlerMetaInfo(HandlerType.WEBSOCKET, Util.prefixContextPath(server.getContextPath(), path), ws, new HashSet<>()));
         return this;
     }
 
diff --git a/src/main/java/io/javalin/core/util/JettyServerUtil.kt b/src/main/java/io/javalin/core/JavalinServer.kt
similarity index 69%
rename from src/main/java/io/javalin/core/util/JettyServerUtil.kt
rename to src/main/java/io/javalin/core/JavalinServer.kt
index 2f763ac6..8fc1f449 100644
--- a/src/main/java/io/javalin/core/util/JettyServerUtil.kt
+++ b/src/main/java/io/javalin/core/JavalinServer.kt
@@ -4,11 +4,11 @@
  * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
  */
 
-package io.javalin.core.util
+package io.javalin.core
 
 import io.javalin.Javalin
-import io.javalin.core.JavalinServlet
-import io.javalin.websocket.WsPathMatcher
+import io.javalin.core.util.Header
+import io.javalin.websocket.JavalinWsServlet
 import org.eclipse.jetty.server.*
 import org.eclipse.jetty.server.handler.HandlerCollection
 import org.eclipse.jetty.server.handler.HandlerList
@@ -18,49 +18,32 @@ import org.eclipse.jetty.server.session.SessionHandler
 import org.eclipse.jetty.servlet.ServletContextHandler
 import org.eclipse.jetty.servlet.ServletHolder
 import org.eclipse.jetty.util.thread.QueuedThreadPool
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator
-import org.eclipse.jetty.websocket.servlet.WebSocketServlet
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory
 import org.slf4j.LoggerFactory
 import java.io.ByteArrayInputStream
 import java.net.BindException
-import java.util.function.Consumer
 import java.util.function.Supplier
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
-object JettyServerUtil {
+class JavalinServer {
 
-    private val jettyDefaultLogger = org.eclipse.jetty.util.log.Log.getLog()
     private val log = LoggerFactory.getLogger(Javalin::class.java) // let's pretend
 
-    @JvmStatic
-    fun reEnableJettyLogger() = org.eclipse.jetty.util.log.Log.setLog(jettyDefaultLogger)
+    private val jettyDefaultLogger = org.eclipse.jetty.util.log.Log.getLog()
 
-    @JvmStatic
-    fun defaultServer(): Server {
-        org.eclipse.jetty.util.log.Log.setLog(io.javalin.core.util.NoopLogger()) // disable logger before server creation
-        return Server(QueuedThreadPool(250, 8, 60_000)).apply {
-            server.addBean(LowResourceMonitor(this))
-            server.insertHandler(StatisticsHandler())
-        }
-    }
+    lateinit var jettyServer: Server
+    lateinit var jettySessionHandler: SessionHandler
 
-    @JvmStatic
-    fun defaultSessionHandler() = SessionHandler().apply { httpOnly = true }
+    var jettyPort = 7000
+    var contextPath = "/"
+    var started = false
 
-    @JvmStatic
     @Throws(BindException::class)
-    fun initialize(
-            server: Server,
-            sessionHandler: SessionHandler,
-            port: Int,
-            contextPath: String,
-            javalinServlet: JavalinServlet,
-            wsPathMatcher: WsPathMatcher,
-            wsFactoryConfig: Consumer<WebSocketServletFactory>
-    ): Int {
+    fun start(javalinServlet: JavalinServlet, javalinWsServlet: JavalinWsServlet) {
 
+        disableJettyLogger()
+        if (!::jettyServer.isInitialized) jettyServer = defaultServer()
+        if (!::jettySessionHandler.isInitialized) jettySessionHandler = defaultSessionHandler()
         val nullParent = null // javalin handlers are orphans
 
         val httpHandler = object : ServletContextHandler(nullParent, contextPath, SESSIONS) {
@@ -77,19 +60,11 @@ object JettyServerUtil {
                 jettyRequest.isHandled = true
             }
         }.apply {
-            this.sessionHandler = sessionHandler
+            this.sessionHandler = jettySessionHandler
         }
 
         val webSocketHandler = ServletContextHandler(nullParent, contextPath).apply {
-            addServlet(ServletHolder(object : WebSocketServlet() {
-                override fun configure(factory: WebSocketServletFactory) {
-                    wsFactoryConfig.accept(factory)
-                    factory.creator = WebSocketCreator { req, res ->
-                        wsPathMatcher.findEntry(req) ?: res.sendError(404, "WebSocket handler not found")
-                        wsPathMatcher // this is a long-lived object handling multiple connections
-                    }
-                }
-            }), "/*")
+            addServlet(ServletHolder(javalinWsServlet), "/*")
         }
 
         val notFoundHandler = object : SessionHandler() {
@@ -102,24 +77,36 @@ object JettyServerUtil {
             }
         }
 
-        server.apply {
+        jettyServer.apply {
             handler = attachJavalinHandlers(server.handler, HandlerList(httpHandler, webSocketHandler, notFoundHandler))
             connectors = connectors.takeIf { it.isNotEmpty() } ?: arrayOf(ServerConnector(server).apply {
-                this.port = port
+                this.port = jettyPort
             })
         }.start()
 
-        server.connectors.filterIsInstance<ServerConnector>().forEach {
+        jettyServer.connectors.filterIsInstance<ServerConnector>().forEach {
             log.info("Listening on ${it.protocol}://${it.host ?: "localhost"}:${it.localPort}$contextPath")
         }
 
-        server.connectors.filter { it !is ServerConnector }.forEach {
+        jettyServer.connectors.filter { it !is ServerConnector }.forEach {
             log.info("Binding to: $it")
         }
 
-        return (server.connectors[0] as? ServerConnector)?.localPort ?: -1
+        reEnableJettyLogger()
+        started = true
+        jettyPort = (jettyServer.connectors[0] as? ServerConnector)?.localPort ?: -1
+    }
+
+    private fun disableJettyLogger() = org.eclipse.jetty.util.log.Log.setLog(NoopLogger()) // disable logger before server creation
+    private fun reEnableJettyLogger() = org.eclipse.jetty.util.log.Log.setLog(jettyDefaultLogger)
+
+    private fun defaultServer() = Server(QueuedThreadPool(250, 8, 60_000)).apply {
+        server.addBean(LowResourceMonitor(this))
+        server.insertHandler(StatisticsHandler())
     }
 
+    private fun defaultSessionHandler() = SessionHandler().apply { httpOnly = true }
+
     private val ServerConnector.protocol get() = if (this.protocols.contains("ssl")) "https" else "http"
 
     private fun attachJavalinHandlers(userHandler: Handler?, javalinHandlers: HandlerList) = when (userHandler) {
@@ -140,21 +127,13 @@ object JettyServerUtil {
     }
 
     private fun HttpServletRequest.isWebSocket(): Boolean = this.getHeader(Header.SEC_WEBSOCKET_KEY) != null
+}
 
-    var noJettyStarted = true
-    fun printHelpfulMessageIfNoServerHasBeenStartedAfterOneSecond() {
-        // per instance checks are not considered necessary
-        // this helper is not intended for people with more than one instance
-        Thread {
-            Thread.sleep(1000)
-            if (noJettyStarted) {
-                log.info("It looks like you created a Javalin instance, but you never started it.")
-                log.info("Try: Javalin app = Javalin.create().start();")
-                log.info("For more help, visit https://javalin.io/documentation#starting-and-stopping")
-            }
-        }.start()
-    }
+object JettyUtil {
 
+    private val log = LoggerFactory.getLogger(Javalin::class.java) // let's pretend
+
+    @JvmStatic
     fun getSessionHandler(sessionHandlerSupplier: Supplier<SessionHandler>): SessionHandler {
         val sessionHandler = sessionHandlerSupplier.get()
         try {
@@ -165,6 +144,7 @@ object JettyServerUtil {
         }
         return sessionHandler
     }
+
 }
 
 class NoopLogger : org.eclipse.jetty.util.log.Logger {
@@ -184,3 +164,4 @@ class NoopLogger : org.eclipse.jetty.util.log.Logger {
     override fun debug(thrown: Throwable) {}
     override fun debug(msg: String, thrown: Throwable) {}
 }
+
diff --git a/src/main/java/io/javalin/core/JavalinServlet.kt b/src/main/java/io/javalin/core/JavalinServlet.kt
index 699ec3fe..213de230 100644
--- a/src/main/java/io/javalin/core/JavalinServlet.kt
+++ b/src/main/java/io/javalin/core/JavalinServlet.kt
@@ -8,33 +8,37 @@ package io.javalin.core
 
 import io.javalin.*
 import io.javalin.core.util.*
+import io.javalin.security.AccessManager
+import io.javalin.security.CoreRoles
+import io.javalin.security.Role
+import io.javalin.security.SecurityUtil
 import io.javalin.staticfiles.ResourceHandler
 import java.io.InputStream
 import java.util.zip.GZIPOutputStream
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
-class JavalinServlet(
-        val javalin: Javalin,
-        val matcher: PathMatcher,
-        val exceptionMapper: ExceptionMapper,
-        val errorMapper: ErrorMapper,
-        val debugLogging: Boolean,
-        val requestLogger: RequestLogger?,
-        val dynamicGzipEnabled: Boolean,
-        val autogeneratedEtagsEnabled: Boolean,
-        val defaultContentType: String,
-        val maxRequestCacheBodySize: Long,
-        val prefer405over404: Boolean,
-        val singlePageHandler: SinglePageHandler,
-        val resourceHandler: ResourceHandler?) {
+class JavalinServlet(private val appAttributes: Map<Class<*>, Any>) {
+
+    val matcher = PathMatcher()
+    val exceptionMapper = ExceptionMapper()
+    val errorMapper = ErrorMapper()
+    val singlePageHandler = SinglePageHandler()
+    var resourceHandler: ResourceHandler? = null
+    var accessManager = AccessManager { handler, ctx, permittedRoles -> SecurityUtil.noopAccessManager(handler, ctx, permittedRoles) }
+    var requestLogger: RequestLogger? = null
+    var dynamicGzipEnabled = true
+    var autogeneratedEtagsEnabled = false
+    var defaultContentType = "text/plain"
+    var maxRequestCacheBodySize = 4096L
+    var prefer405over404 = false
 
     fun service(servletRequest: HttpServletRequest, res: HttpServletResponse) {
 
         val req = CachedRequestWrapper(servletRequest, maxRequestCacheBodySize) // cached for reading multiple times
         val type = HandlerType.fromServletRequest(req)
-        val requestUri = req.requestURI
-        val ctx = Context(req, res, javalin)
+        val requestUri = req.requestURI.removePrefix(req.contextPath)
+        val ctx = Context(req, res, appAttributes)
 
         fun tryWithExceptionMapper(func: () -> Unit) = exceptionMapper.catchException(ctx, func)
 
@@ -50,7 +54,7 @@ class JavalinServlet(
                 return@tryWithExceptionMapper // return 200, there is a get handler
             }
             if (type == HandlerType.HEAD || type == HandlerType.GET) { // let Jetty check for static resources
-                if (resourceHandler?.handle(req, res) == true) return@tryWithExceptionMapper
+                if (resourceHandler?.handle(req, res, matcher.ignoreTrailingSlashes) == true) return@tryWithExceptionMapper
                 if (singlePageHandler.handle(ctx)) return@tryWithExceptionMapper
             }
             val availableHandlerTypes = MethodNotAllowedUtil.findAvailableHttpHandlerTypes(matcher, requestUri)
@@ -93,15 +97,7 @@ class JavalinServlet(
             resultStream.close()
         }
 
-        fun logRequest() {
-            if (requestLogger != null) {
-                requestLogger.handle(ctx, LogUtil.executionTimeMs(ctx))
-            } else if (debugLogging == true) {
-                LogUtil.logRequestAndResponse(ctx, matcher)
-            }
-        }
-
-        LogUtil.startTimer(ctx) // start request lifecycle
+        LogUtil.setup(ctx, matcher) // start request lifecycle
         ctx.header(Header.SERVER, "Javalin")
         ctx.contentType(defaultContentType)
         tryBeforeAndEndpointHandlers()
@@ -109,7 +105,7 @@ class JavalinServlet(
             tryErrorHandlers()
             tryAfterHandlers()
             writeResult(res)
-            logRequest()
+            requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx))
             return // sync lifecycle complete
         } else { // finish request asynchronously
             val asyncContext = req.startAsync()
@@ -126,7 +122,7 @@ class JavalinServlet(
                 tryErrorHandlers()
                 tryAfterHandlers()
                 writeResult(asyncContext.response as HttpServletResponse)
-                logRequest()
+                requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx))
                 asyncContext.complete() // async lifecycle complete
             }
         }
@@ -137,4 +133,10 @@ class JavalinServlet(
     private fun gzipShouldBeDone(ctx: Context) = dynamicGzipEnabled
             && ctx.resultStream()?.available() ?: 0 > 1500 // mtu is apparently ~1500 bytes
             && (ctx.header(Header.ACCEPT_ENCODING) ?: "").contains("gzip", ignoreCase = true)
+
+    fun addHandler(handlerType: HandlerType, path: String, handler: Handler, roles: Set<Role>) {
+        val shouldWrap = handlerType.isHttpMethod() && !roles.contains(CoreRoles.NO_WRAP)
+        val protectedHandler = if (shouldWrap) Handler { ctx -> accessManager.manage(handler, ctx, roles) } else handler
+        matcher.add(HandlerEntry(handlerType, path, protectedHandler, handler))
+    }
 }
diff --git a/src/main/java/io/javalin/core/PathMatcher.kt b/src/main/java/io/javalin/core/PathMatcher.kt
index 8e0df94f..d7937b5b 100644
--- a/src/main/java/io/javalin/core/PathMatcher.kt
+++ b/src/main/java/io/javalin/core/PathMatcher.kt
@@ -10,8 +10,8 @@ import io.javalin.Handler
 import io.javalin.core.util.ContextUtil.urlDecode
 import java.util.*
 
-data class HandlerEntry(val type: HandlerType, val path: String, val handler: Handler, val rawHandler: Handler, val caseSensitiveUrls: Boolean) {
-    private val pathParser = PathParser(path, caseSensitiveUrls)
+data class HandlerEntry(val type: HandlerType, val path: String, val handler: Handler, val rawHandler: Handler) {
+    private val pathParser = PathParser(path)
     fun matches(requestUri: String) = pathParser.matches(requestUri)
     fun extractPathParams(requestUri: String) = pathParser.extractPathParams(requestUri)
     fun extractSplats(requestUri: String) = pathParser.extractSplats(requestUri)
@@ -19,7 +19,6 @@ data class HandlerEntry(val type: HandlerType, val path: String, val handler: Ha
 
 class PathParser(
         path: String,
-        private val caseSensitive: Boolean,
         private val pathParamNames: List<String> = path.split("/")
                 .filter { it.startsWith(":") }
                 .map { it.replace(":", "") },
@@ -32,7 +31,7 @@ class PathParser(
                 .replace("/$".toRegex(), "/?") // Replace trailing slash to optional one
                 .run { if (!endsWith("/?")) this + "/?" else this } // Add slash if doesn't have one
                 .run { "^" + this + "$" } // Let the matcher know that it is the whole path
-                .toCasedRegex(caseSensitive),
+                .toRegex(),
         private val splatRegex: Regex = matchRegex.pattern.replace(".*?", "(.*?)").toRegex(RegexOption.IGNORE_CASE),
         private val pathParamRegex: Regex = matchRegex.pattern.replace("[^/]+?", "([^/]+?)").toRegex(RegexOption.IGNORE_CASE)) {
 
@@ -49,8 +48,6 @@ class PathParser(
 
 }
 
-private fun String.toCasedRegex(caseSensitive: Boolean) = this.let { if (caseSensitive) it.toRegex() else it.toRegex(RegexOption.IGNORE_CASE) }
-
 class PathMatcher(var ignoreTrailingSlashes: Boolean = true) {
 
     private val handlerEntries = HandlerType.values().associateTo(EnumMap<HandlerType, ArrayList<HandlerEntry>>(HandlerType::class.java)) {
@@ -58,9 +55,6 @@ class PathMatcher(var ignoreTrailingSlashes: Boolean = true) {
     }
 
     fun add(entry: HandlerEntry) {
-        if (!entry.caseSensitiveUrls && entry.path != entry.path.toLowerCase()) {
-            throw IllegalArgumentException("By default URLs must be lowercase. Change casing or call `app.enableCaseSensitiveUrls()` to allow mixed casing.")
-        }
         if (entry.type.isHttpMethod() && handlerEntries[entry.type]!!.find { it.type == entry.type && it.path == entry.path } != null) {
             throw IllegalArgumentException("Handler with type='${entry.type}' and path='${entry.path}' already exists.")
         }
diff --git a/src/main/java/io/javalin/core/util/ContextUtil.kt b/src/main/java/io/javalin/core/util/ContextUtil.kt
index 60f3f8ae..ba8f4e59 100644
--- a/src/main/java/io/javalin/core/util/ContextUtil.kt
+++ b/src/main/java/io/javalin/core/util/ContextUtil.kt
@@ -64,8 +64,8 @@ object ContextUtil {
             pathParamMap: Map<String, String> = mapOf(),
             splatList: List<String> = listOf(),
             handlerType: HandlerType = HandlerType.INVALID,
-            javalin: Javalin = object : Javalin() {} // dummy javalin
-    ) = Context(request, response, javalin).apply {
+            appAttributes: Map<Class<*>, Any> = mapOf()
+    ) = Context(request, response, appAttributes).apply {
         this.matchedPath = matchedPath
         this.pathParamMap = pathParamMap
         this.splatList = splatList
diff --git a/src/main/java/io/javalin/core/util/LogUtil.kt b/src/main/java/io/javalin/core/util/LogUtil.kt
index c6ffaf28..8647d271 100644
--- a/src/main/java/io/javalin/core/util/LogUtil.kt
+++ b/src/main/java/io/javalin/core/util/LogUtil.kt
@@ -9,8 +9,8 @@ package io.javalin.core.util
 import io.javalin.Context
 import io.javalin.core.HandlerType
 import io.javalin.core.PathMatcher
-import io.javalin.websocket.WsHandler
 import io.javalin.websocket.WsContext
+import io.javalin.websocket.WsHandler
 import org.slf4j.LoggerFactory
 import java.util.*
 
@@ -18,13 +18,14 @@ object LogUtil {
 
     private val log = LoggerFactory.getLogger(LogUtil::class.java)
 
-    fun logRequestAndResponse(ctx: Context, matcher: PathMatcher) = try {
+    @JvmStatic
+    fun requestDevLogger(ctx: Context, time: Float) = try {
         val type = HandlerType.fromServletRequest(ctx.req)
         val requestUri = ctx.req.requestURI
-        val executionTimeMs = Formatter(Locale.US).format("%.2f", executionTimeMs(ctx))
         val gzipped = ctx.res.getHeader(Header.CONTENT_ENCODING) == "gzip"
         val staticFile = ctx.req.getAttribute("handled-as-static-file") == true
         with(ctx) {
+            val matcher = ctx.attribute<PathMatcher>("javalin-request-log-matcher")!!
             val allMatching = (matcher.findEntries(HandlerType.BEFORE, requestUri) + matcher.findEntries(type, requestUri) + matcher.findEntries(HandlerType.AFTER, requestUri)).map { it.type.name + "=" + it.path }
             val resBody = resultStream()?.apply { reset() }?.bufferedReader()?.use { it.readText() } ?: ""
             val resHeaders = res.headerNames.asSequence().map { it to res.getHeader(it) }.toMap()
@@ -37,7 +38,7 @@ object LogUtil {
                         |    QueryString: ${queryString()}
                         |    QueryParams: ${queryParamMap().mapValues { (_, v) -> v.toString() }}
                         |    FormParams: ${formParamMap().mapValues { (_, v) -> v.toString() }}
-                        |Response: [${status()}], execution took $executionTimeMs ms
+                        |Response: [${status()}], execution took ${Formatter(Locale.US).format("%.2f", time)} ms
                         |    Headers: $resHeaders
                         |    ${resBody(resBody, gzipped, staticFile)}
                         |----------------------------------------------------------------------------------""".trimMargin())
@@ -53,12 +54,15 @@ object LogUtil {
         else -> "No body was set"
     }
 
-    fun startTimer(ctx: Context) = ctx.attribute("javalin-request-log-start-time", System.nanoTime())
+    fun setup(ctx: Context, matcher: PathMatcher) {
+        ctx.attribute("javalin-request-log-matcher", matcher)
+        ctx.attribute("javalin-request-log-start-time", System.nanoTime())
+    }
 
     fun executionTimeMs(ctx: Context) = (System.nanoTime() - ctx.attribute<Long>("javalin-request-log-start-time")!!) / 1000000f
 
     @JvmStatic
-    fun wsDebugLogger(ws: WsHandler) {
+    fun wsDevLogger(ws: WsHandler) {
         ws.onConnect { ctx -> ctx.logEvent("onConnect") }
         ws.onMessage { ctx -> ctx.logEvent("onMessage", "Message (next line):\n${ctx.message()}") }
         ws.onBinaryMessage { ctx -> ctx.logEvent("onBinaryMessage", "Offset: ${ctx.offset}, Length: ${ctx.length}\nMessage (next line):\n${ctx.data}") }
diff --git a/src/main/java/io/javalin/core/util/Util.kt b/src/main/java/io/javalin/core/util/Util.kt
index ecc2d38d..fa8b7a91 100644
--- a/src/main/java/io/javalin/core/util/Util.kt
+++ b/src/main/java/io/javalin/core/util/Util.kt
@@ -7,6 +7,8 @@
 package io.javalin.core.util
 
 import io.javalin.InternalServerErrorResponse
+import io.javalin.Javalin
+import io.javalin.core.JavalinServer
 import org.slf4j.LoggerFactory
 import java.io.ByteArrayOutputStream
 import java.io.File
@@ -18,8 +20,9 @@ import java.util.zip.CheckedInputStream
 
 object Util {
 
-    private val log = LoggerFactory.getLogger(Util::class.java)
+    private val log = LoggerFactory.getLogger(Javalin::class.java)
 
+    @JvmStatic
     fun normalizeContextPath(contextPath: String) = ("/$contextPath").replace("/{2,}".toRegex(), "/").removeSuffix("/")
 
     @JvmStatic
@@ -61,6 +64,7 @@ object Util {
 
     fun pathToList(pathString: String): List<String> = pathString.split("/").filter { it.isNotEmpty() }
 
+    @JvmStatic
     fun printHelpfulMessageIfLoggerIsMissing() {
         if (!classExists(OptionalDependency.SLF4JSIMPLE.testClass)) {
             System.err.println("""
@@ -71,8 +75,9 @@ object Util {
         }
     }
 
-    fun javalinBanner(): String {
-        return "\n" + """
+    @JvmStatic
+    fun logJavalinBanner(showBanner: Boolean) {
+        if (showBanner) log.info("\n" + """
           |           __                      __ _
           |          / /____ _ _   __ ____ _ / /(_)____
           |     __  / // __ `/| | / // __ `// // // __ \
@@ -80,7 +85,7 @@ object Util {
           |    \____/ \__,_/  |___/ \__,_//_//_//_/ /_/
           |
           |        https://javalin.io/documentation
-          |""".trimMargin()
+          |""".trimMargin())
     }
 
     fun getChecksumAndReset(inputStream: InputStream): String {
@@ -108,4 +113,15 @@ object Util {
         return false
     }
 
+
+    @JvmStatic
+    fun logWarningIfNotStartedAfterOneSecond(server: JavalinServer) = Thread {
+        Thread.sleep(1000)
+        if (!server.started) {
+            log.info("It looks like you created a Javalin instance, but you never started it.")
+            log.info("Try: Javalin app = Javalin.create().start();")
+            log.info("For more help, visit https://javalin.io/documentation#starting-and-stopping")
+        }
+    }.start()
+
 }
diff --git a/src/main/java/io/javalin/staticfiles/JettyResourceHandler.kt b/src/main/java/io/javalin/staticfiles/JettyResourceHandler.kt
index b7b2cff2..91f082ff 100644
--- a/src/main/java/io/javalin/staticfiles/JettyResourceHandler.kt
+++ b/src/main/java/io/javalin/staticfiles/JettyResourceHandler.kt
@@ -8,7 +8,6 @@ package io.javalin.staticfiles
 
 import io.javalin.core.util.Header
 import org.eclipse.jetty.server.Request
-import org.eclipse.jetty.server.Server
 import org.eclipse.jetty.server.handler.ResourceHandler
 import org.eclipse.jetty.server.handler.gzip.GzipHandler
 import org.eclipse.jetty.util.resource.Resource
@@ -17,27 +16,23 @@ import java.io.File
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
-data class StaticFileConfig(val path: String, val location: Location)
-enum class Location { CLASSPATH, EXTERNAL; }
-
-class JettyResourceHandler(
-        staticFileConfig: Set<StaticFileConfig>,
-        jettyServer: Server,
-        private val ignoreTrailingSlashes: Boolean) : io.javalin.staticfiles.ResourceHandler {
+class JettyResourceHandler : io.javalin.staticfiles.ResourceHandler {
 
     private val log = LoggerFactory.getLogger("io.javalin.Javalin")
 
-    private val handlers = staticFileConfig.map { config ->
-        GzipHandler().apply {
-            server = jettyServer // the handler is standalone, this assignment just prevents a log.warn
+    val handlers = mutableListOf<GzipHandler>()
+
+    override fun addStaticFileConfig(config: StaticFileConfig) {
+        handlers.add(GzipHandler().apply {
             handler = if (config.path == "/webjars") WebjarHandler() else ResourceHandler().apply {
                 resourceBase = getResourcePath(config)
                 isDirAllowed = false
                 isEtags = true
                 log.info("Static file handler added with path=${config.path} and location=${config.location}. Absolute path: '${getResourcePath(config)}'.")
             }
-        }
-    }.onEach { it.start() }
+            start()
+        })
+    }
 
     inner class WebjarHandler : ResourceHandler() {
         override fun getResource(path: String) = Resource.newClassPathResource("META-INF/resources$path") ?: super.getResource(path)
@@ -58,14 +53,14 @@ class JettyResourceHandler(
         return staticFileConfig.path
     }
 
-    override fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse): Boolean {
+    override fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse, ignoreTrailingSlashes: Boolean): Boolean {
         val target = httpRequest.getAttribute("jetty-target") as String
         val baseRequest = httpRequest.getAttribute("jetty-request") as Request
         for (gzipHandler in handlers) {
             try {
                 val resourceHandler = (gzipHandler.handler as ResourceHandler)
                 val resource = resourceHandler.getResource(target)
-                if (resource.isFile() || resource.isDirectoryWithWelcomeFile(resourceHandler, target)) {
+                if (resource.isFile() || resource.isDirectoryWithWelcomeFile(resourceHandler, target, ignoreTrailingSlashes)) {
                     val maxAge = if (target.startsWith("/immutable/") || resourceHandler is WebjarHandler) 31622400 else 0
                     httpResponse.setHeader(Header.CACHE_CONTROL, "max-age=$maxAge")
                     gzipHandler.handle(target, baseRequest, httpRequest, httpResponse)
@@ -81,9 +76,9 @@ class JettyResourceHandler(
 
     private fun Resource?.isFile() = this != null && this.exists() && !this.isDirectory
 
-    private fun Resource?.isDirectoryWithWelcomeFile(handler: ResourceHandler, target: String) =
-            this != null && this.isDirectory && handler.getResource(welcomeFilePath(target))?.exists() == true
+    private fun Resource?.isDirectoryWithWelcomeFile(handler: ResourceHandler, target: String, ignoreTrailingSlashes: Boolean) =
+            this != null && this.isDirectory && handler.getResource(welcomeFilePath(target, ignoreTrailingSlashes))?.exists() == true
 
-    private fun welcomeFilePath(target: String) = if (!target.endsWith("/") && ignoreTrailingSlashes) "$target/index.html" else "${target}index.html"
+    private fun welcomeFilePath(target: String, ignoreTrailingSlashes: Boolean) = if (!target.endsWith("/") && ignoreTrailingSlashes) "$target/index.html" else "${target}index.html"
 
 }
diff --git a/src/main/java/io/javalin/staticfiles/ResourceHandler.kt b/src/main/java/io/javalin/staticfiles/ResourceHandler.kt
index 25bde65a..11ec7710 100644
--- a/src/main/java/io/javalin/staticfiles/ResourceHandler.kt
+++ b/src/main/java/io/javalin/staticfiles/ResourceHandler.kt
@@ -3,6 +3,10 @@ package io.javalin.staticfiles
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
+enum class Location { CLASSPATH, EXTERNAL; }
+data class StaticFileConfig(val path: String, val location: Location)
+
 interface ResourceHandler {
-    fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse): Boolean
+    fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse, ignoreTrailingSlashes: Boolean): Boolean
+    fun addStaticFileConfig(config: StaticFileConfig)
 }
diff --git a/src/main/java/io/javalin/websocket/JavalinWsServlet.kt b/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
new file mode 100644
index 00000000..2bac14e4
--- /dev/null
+++ b/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
@@ -0,0 +1,27 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+
+package io.javalin.websocket
+
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator
+import org.eclipse.jetty.websocket.servlet.WebSocketServlet
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory
+import java.util.function.Consumer
+
+class JavalinWsServlet : WebSocketServlet() {
+
+    var wsFactoryConfig: Consumer<WebSocketServletFactory>? = null
+    var wsPathMatcher = WsPathMatcher()
+
+    override fun configure(factory: WebSocketServletFactory) {
+        wsFactoryConfig?.accept(factory)
+        factory.creator = WebSocketCreator { req, res ->
+            wsPathMatcher.findEntry(req) ?: res.sendError(404, "WebSocket handler not found")
+            wsPathMatcher // this is a long-lived object handling multiple connections
+        }
+    }
+
+}
diff --git a/src/main/java/io/javalin/websocket/WsPathMatcher.kt b/src/main/java/io/javalin/websocket/WsPathMatcher.kt
index 24b7c460..a9020a6e 100644
--- a/src/main/java/io/javalin/websocket/WsPathMatcher.kt
+++ b/src/main/java/io/javalin/websocket/WsPathMatcher.kt
@@ -10,11 +10,12 @@ import io.javalin.core.PathParser
 import org.eclipse.jetty.websocket.api.Session
 import org.eclipse.jetty.websocket.api.UpgradeRequest
 import org.eclipse.jetty.websocket.api.annotations.*
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest
 import java.util.*
 import java.util.concurrent.ConcurrentHashMap
 
-data class WsEntry(val path: String, val handler: WsHandler, val caseSensitiveUrls: Boolean) {
-    private val pathParser = PathParser(path, caseSensitiveUrls)
+data class WsEntry(val path: String, val handler: WsHandler) {
+    private val pathParser = PathParser(path)
     fun matches(requestUri: String) = pathParser.matches(requestUri)
     fun extractPathParams(requestUri: String) = pathParser.extractPathParams(requestUri)
 }
@@ -32,9 +33,6 @@ class WsPathMatcher {
     private val sessionPathParams = ConcurrentHashMap<Session, Map<String, String>>()
 
     fun add(wsEntry: WsEntry) {
-        if (!wsEntry.caseSensitiveUrls && wsEntry.path != wsEntry.path.toLowerCase()) {
-            throw IllegalArgumentException("By default URLs must be lowercase. Change casing or call `app.enableCaseSensitiveUrls()` to allow mixed casing.")
-        }
         wsEntries.add(wsEntry)
     }
 
@@ -87,11 +85,11 @@ class WsPathMatcher {
 
     private fun findEntry(session: Session) = findEntry(session.upgradeRequest)?.also { cacheSession(session, it) }
 
-    fun findEntry(req: UpgradeRequest) = wsEntries.find { it.matches(req.requestURI.path) }
+    fun findEntry(req: UpgradeRequest) = wsEntries.find { it.matches(req.uriNoContextPath()) }
 
     private fun cacheSession(session: Session, wsEntry: WsEntry) {
         sessionIds.putIfAbsent(session, UUID.randomUUID().toString())
-        sessionPathParams.putIfAbsent(session, wsEntry.extractPathParams(session.upgradeRequest.requestURI.path))
+        sessionPathParams.putIfAbsent(session, wsEntry.extractPathParams(session.upgradeRequest.uriNoContextPath()))
     }
 
     private fun destroy(session: Session) {
@@ -99,4 +97,6 @@ class WsPathMatcher {
         sessionPathParams.remove(session)
     }
 
+    private fun UpgradeRequest.uriNoContextPath() = this.requestURI.path.removePrefix((this as ServletUpgradeRequest).httpServletRequest.contextPath)
+
 }


Test patch:
diff --git a/src/test/java/io/javalin/TestCustomJetty.kt b/src/test/java/io/javalin/TestCustomJetty.kt
index 71127a97..7182eb92 100644
--- a/src/test/java/io/javalin/TestCustomJetty.kt
+++ b/src/test/java/io/javalin/TestCustomJetty.kt
@@ -7,6 +7,7 @@
 
 package io.javalin
 
+import com.mashape.unirest.http.Unirest
 import io.javalin.util.TestServlet
 import io.javalin.util.TestUtil
 import org.assertj.core.api.Assertions.assertThat
@@ -26,6 +27,12 @@ import javax.servlet.http.HttpServletResponse
 
 class TestCustomJetty {
 
+    @Test
+    fun `setting port works`() {
+        Javalin.create().start(1234).get("/") { it.result("PORT WORKS") }
+        assertThat(Unirest.get("http://localhost:1234/").asString().body).isEqualTo("PORT WORKS")
+    }
+
     @Test
     fun `embedded server can have custom jetty Handler`() {
         val statisticsHandler = StatisticsHandler()
diff --git a/src/test/java/io/javalin/TestLogging.kt b/src/test/java/io/javalin/TestLogging.kt
index 746a5160..cadfbb11 100644
--- a/src/test/java/io/javalin/TestLogging.kt
+++ b/src/test/java/io/javalin/TestLogging.kt
@@ -20,7 +20,7 @@ class TestLogging {
     fun `default logging (no logging) works`() = runTest(Javalin.create())
 
     @Test
-    fun `debug logging works`() = runTest(Javalin.create().enableDebugLogging())
+    fun `debug logging works`() = runTest(Javalin.create().enableDevLogging())
 
     @Test
     fun `custom logging works`() = runTest(Javalin.create().requestLogger { _, executionTimeMs ->
diff --git a/src/test/java/io/javalin/TestRouting.kt b/src/test/java/io/javalin/TestRouting.kt
index 96611630..e06555cf 100644
--- a/src/test/java/io/javalin/TestRouting.kt
+++ b/src/test/java/io/javalin/TestRouting.kt
@@ -21,8 +21,6 @@ class TestRouting {
     private val okHttp = OkHttpClient().newBuilder().build()
     fun OkHttpClient.getBody(path: String) = this.newCall(Request.Builder().url(path).get().build()).execute().body()!!.string()
 
-    val caseSensitiveJavalin = Javalin.create().enableCaseSensitiveUrls()
-
     @Test
     fun `general integration test`() = TestUtil.test { app, http ->
         app.get("/") { ctx -> ctx.result("/") }
@@ -44,21 +42,8 @@ class TestRouting {
         assertThat(http.getBody("/s/s/s/s")).isEqualTo("/s/s/s/s")
     }
 
-    @Test(expected = IllegalArgumentException::class)
-    fun `urls must be lowercase by default`() = TestUtil.test { app, _ ->
-        app.get("/My-Url") { ctx -> ctx.result("OK") }
-    }
-
-    @Test
-    fun `urls are case insensitive by default`() = TestUtil.test { app, http ->
-        app.get("/my-url") { ctx -> ctx.result("OK") }
-        assertThat(http.getBody("/my-url")).isEqualTo("OK")
-        assertThat(http.getBody("/My-UrL")).isEqualTo("OK")
-        assertThat(http.getBody("/MY-URL")).isEqualTo("OK")
-    }
-
     @Test
-    fun `case sensitive urls work`() = TestUtil.test(caseSensitiveJavalin) { app, http ->
+    fun `case sensitive urls work`() = TestUtil.test { app, http ->
         app.get("/My-Url") { ctx -> ctx.result("OK") }
         assertThat(http.getBody("/MY-URL")).isEqualTo("Not found")
         assertThat(http.getBody("/My-Url")).isEqualTo("OK")
@@ -67,11 +52,11 @@ class TestRouting {
     @Test
     fun `extracting path-param and splat works`() = TestUtil.test { app, http ->
         app.get("/path/:path-param/*") { ctx -> ctx.result("/" + ctx.pathParam("path-param") + "/" + ctx.splat(0)) }
-        assertThat(http.getBody("/PATH/P/S")).isEqualTo("/P/S")
+        assertThat(http.getBody("/path/P/S")).isEqualTo("/P/S")
     }
 
     @Test
-    fun `extracting path-param and splat works case sensitive`() = TestUtil.test(caseSensitiveJavalin) { app, http ->
+    fun `extracting path-param and splat works case sensitive`() = TestUtil.test { app, http ->
         app.get("/:path-param/Path/*") { ctx -> ctx.result(ctx.pathParam("path-param") + ctx.splat(0)!!) }
         assertThat(http.getBody("/path-param/Path/Splat")).isEqualTo("path-paramSplat")
         assertThat(http.getBody("/path-param/path/Splat")).isEqualTo("Not found")
@@ -95,7 +80,7 @@ class TestRouting {
     }
 
     @Test
-    fun `path-params work case-sensitive`() = TestUtil.test(caseSensitiveJavalin) { app, http ->
+    fun `path-params work case-sensitive`() = TestUtil.test { app, http ->
         app.get("/:userId") { ctx -> ctx.result(ctx.pathParam("userId")) }
         assertThat(http.getBody("/path-param")).isEqualTo("path-param")
         app.get("/:a/:A") { ctx -> ctx.result("${ctx.pathParam("a")}-${ctx.pathParam("A")}") }
diff --git a/src/test/java/io/javalin/TestStaticFiles.kt b/src/test/java/io/javalin/TestStaticFiles.kt
index d4ca620b..329827f5 100644
--- a/src/test/java/io/javalin/TestStaticFiles.kt
+++ b/src/test/java/io/javalin/TestStaticFiles.kt
@@ -23,7 +23,7 @@ class TestStaticFiles {
             .enableStaticFiles("/public/immutable")
             .enableStaticFiles("/public/protected")
             .enableStaticFiles("/public/subdir")
-    private val debugLoggingApp = Javalin.create().enableStaticFiles("/public").enableDebugLogging()
+    private val debugLoggingApp = Javalin.create().enableStaticFiles("/public").enableDevLogging()
 
     @Test
     fun `serving HTML from classpath works`() = TestUtil.test(defaultStaticResourceApp) { _, http ->
diff --git a/src/test/java/io/javalin/TestWebSocket.kt b/src/test/java/io/javalin/TestWebSocket.kt
index 7abc362e..e286f73e 100644
--- a/src/test/java/io/javalin/TestWebSocket.kt
+++ b/src/test/java/io/javalin/TestWebSocket.kt
@@ -30,7 +30,6 @@ import java.util.concurrent.atomic.AtomicInteger
 class TestWebSocket {
 
     private val contextPathJavalin = Javalin.create().contextPath("/websocket")
-    private val caseSensitiveJavalin = Javalin.create().enableCaseSensitiveUrls()
     private val javalinWithWsLogger = Javalin.create().wsLogger { ws ->
         ws.onConnect { ctx -> log.add(ctx.pathParam("param") + " connected") }
         ws.onClose { ctx -> log.add(ctx.pathParam("param") + " disconnected") }
@@ -206,7 +205,7 @@ class TestWebSocket {
 
     @Test
     fun `headers and host are available in session`() = TestUtil.test { app, _ ->
-        app.ws("websocket") { ws ->
+        app.ws("/websocket") { ws ->
             ws.onConnect { ctx -> log.add("Header: " + ctx.header("Test")!!) }
             ws.onClose { ctx -> log.add("Closed connection from: " + ctx.host()!!) }
         }
@@ -235,18 +234,8 @@ class TestWebSocket {
         assertThat(queryParams).contains("1", "2")
     }
 
-    @Test(expected = IllegalArgumentException::class)
-    fun `paths must be lowercase by default`() = TestUtil.test { app, _ ->
-        app.ws("/pAtH/:param") { ws -> ws.onConnect { ctx -> log.add(ctx.pathParam("param")) } }
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun `path-params must be lowercase by default`() = TestUtil.test { app, _ ->
-        app.ws("/path/:Param") { ws -> ws.onConnect { ctx -> log.add(ctx.pathParam("param")) } }
-    }
-
     @Test
-    fun `routing and path-params case sensitive works`() = TestUtil.test(caseSensitiveJavalin) { app, _ ->
+    fun `routing and path-params case sensitive works`() = TestUtil.test { app, _ ->
         app.ws("/pAtH/:param") { ws -> ws.onConnect { ctx -> log.add(ctx.pathParam("param")) } }
         app.ws("/other-path/:param") { ws -> ws.onConnect { ctx -> log.add(ctx.pathParam("param")) } }
         connectAndDisconnect(TestClient(URI.create("ws://localhost:" + app.port() + "/PaTh/my-param")))
@@ -269,7 +258,7 @@ class TestWebSocket {
     }
 
     @Test
-    fun `debug logging works for web sockets`() = TestUtil.test(Javalin.create().enableDebugLogging()) { app, _ ->
+    fun `debug logging works for web sockets`() = TestUtil.test(Javalin.create().enableDevLogging()) { app, _ ->
         app.ws("/path/:param") {}
         connectAndDisconnect(TestClient(URI.create("ws://localhost:" + app.port() + "/path/0")))
         connectAndDisconnect(TestClient(URI.create("ws://localhost:" + app.port() + "/path/1?test=banana&hi=1&hi=2")))
diff --git a/src/test/java/io/javalin/examples/HelloWorldWebSockets.java b/src/test/java/io/javalin/examples/HelloWorldWebSockets.java
index a451edd4..65a2175e 100644
--- a/src/test/java/io/javalin/examples/HelloWorldWebSockets.java
+++ b/src/test/java/io/javalin/examples/HelloWorldWebSockets.java
@@ -12,7 +12,7 @@ import io.javalin.Javalin;
 // see HelloWorldSecure for how to set that up
 public class HelloWorldWebSockets {
     public static void main(String[] args) {
-        Javalin app = Javalin.create().enableDebugLogging().port(7070);
+        Javalin app = Javalin.create().enableDevLogging().port(7070);
         app.ws("/websocket", ws -> {
             ws.onConnect(ctx -> {
                 System.out.println("Connected");
diff --git a/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt b/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
index 56b43aa8..0781920c 100644
--- a/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
+++ b/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
@@ -102,7 +102,7 @@ class RouteMatcherPerformanceTest {
         )
 
         val oldEntries = routes.map { OldHandlerEntry(HandlerType.AFTER, it, Handler { }) }
-        val newEntries = routes.map { NewHandlerEntry(HandlerType.AFTER, it, Handler { }, Handler { }, false) }
+        val newEntries = routes.map { NewHandlerEntry(HandlerType.AFTER, it, Handler { }, Handler { }) }
 
         val testEntries = listOf(
                 "/test/1234/some/path/here",
diff --git a/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt b/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
index 631f4bb1..19615e60 100644
--- a/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
+++ b/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
@@ -14,7 +14,7 @@ import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 
 fun main(args: Array<String>) {
-    val app = Javalin.create().enableDebugLogging().start(7070)
+    val app = Javalin.create().enableDevLogging().start(7070)
 
     app.routes {
         get("/result") { ctx ->
diff --git a/src/test/kotlin/io/javalin/examples/HelloWorldWebSockets.kt b/src/test/kotlin/io/javalin/examples/HelloWorldWebSockets.kt
index ee7def69..77f4564c 100644
--- a/src/test/kotlin/io/javalin/examples/HelloWorldWebSockets.kt
+++ b/src/test/kotlin/io/javalin/examples/HelloWorldWebSockets.kt
@@ -12,7 +12,7 @@ import io.javalin.Javalin
 // see HelloWorldSecure for how to set that up
 fun main(args: Array<String>) {
     Javalin.create().apply {
-        enableDebugLogging()
+        enableDevLogging()
         ws("/websocket") { ws ->
             ws.onConnect { ctx ->
                 println("Connection established")