Fork me on GitHub

Javalin 5 to 6 migration guide

This page attempts to cover all the things you need to know in order to migrate from Javalin 5 to Javalin 6. If you find any errors, or if something is missing, please edit this page on GitHub.

The AccessManager interface has been removed

This is quite a big internal change, and migrating should be performed with some care. It’s not a difficult migration, but it’s important to understand what’s going on.

In Javalin 5, the AccessManager interface wrapped endpoint-handlers in a lambda, and allowed you to choose whether to call the wrapped endpoint-handlers. This meant that the AccessManager was not called for static files or before/after handlers, it was only called for endpoint handlers. Let’s look at an example of an AccessManager in Javalin 5:

config.accessManager((handler, ctx, routeRoles) -> {
    var userRole = getUserRole(ctx); // some user defined function that returns a user role
    if (routeRoles.contains(userRole)) { // routeRoles are provided through the AccessManager interface
        handler.handle(ctx); // if handler.handle(ctx) is not called, the endpoint handler is not called
    }
});
config.accessManager { handler, ctx, routeRoles ->
    val userRole = getUserRole(ctx) // some user defined function that returns a user role
    if (routeRoles.contains(userRole)) { // routeRoles are provided through the AccessManager interface
        handler.handle(ctx) // if handler.handle(ctx) is not called, the endpoint handler is not called
    }
}

Now, let’s look at a similar example in Javalin 6:

app.beforeMatched(ctx -> {
    var userRole = getUserRole(ctx); // some user defined function that returns a user role
    if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface
        throw new UnauthorizedResponse(); // request will have to be explicitly stopped by throwing an exception
    }
});
app.beforeMatched { ctx ->
    val userRole = getUserRole(ctx) // some user defined function that returns a user role
    if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface
        throw UnauthorizedResponse() // request will have to be explicitly stopped by throwing an exception
    }
}

While this looks similar, there is an important difference. As mentioned, in Javalin 5, the AccessManager was only called for endpoint handlers, this means that the Javalin 5 example would not be called for static files or before/after handlers, while the Javalin 6 example would be called for all requests. If you want to migrate to a beforeMatched in Javalin 6 that has the same behavior as the AccessManager in Javalin 5, you should perform a check for the presence of route roles in the beforeMatched handler:

app.beforeMatched(ctx -> {
    if (ctx.routeRoles().isEmpty()) { // route roles can only be attached to endpoint handlers
        return; // if there are no route roles, we don't need to check anything
    }
    var userRole = getUserRole(ctx);
    if (!ctx.routeRoles().contains(userRole)) {
        throw new UnauthorizedResponse();
    }
});
app.beforeMatched { ctx ->
    if (ctx.routeRoles().isEmpty()) { // route roles can only be attached to endpoint handlers
        return // if there are no route roles, we don't need to check anything
    }
    val userRole = getUserRole(ctx)
    if (!ctx.routeRoles().contains(userRole)) {
        throw UnauthorizedResponse()
    }
}

If you are not using UnauthorizedResponse or any other HttpResponseException you shall stop processing further handlers using ctx.skipRemainingHandlers() as a last step in the beforeMatched.

Virtual threads are now opt-in

In Javalin 5, virtual threads were enabled by default. This was because virtual threads themselves were opt-in, and we wanted to make it as easy as possible to try them out. Now that virtual threads are becoming part of the official JDKs, we have decided to make them opt-in. You can enable virtual threads by setting config.useVirtualThreads = true. This will enable virtual threads for all Javalin threads, including the Jetty request threads.

Untyped “app attributes” are now typed “app data”

In Javalin 5, you could attach and access untyped attributes to the Javalin instance, like this:

// register a custom attribute
var app = Javalin.create()
app.attribute("my-key", myValue);
// access a custom attribute
var myValue = (MyValue) ctx.appAttribute("my-key");
// call a custom method on a custom attribute
((MyValue) ctx.appAttribute("my-key")).myMethod();
// register a custom attribute
val app = Javalin.create()
app.attribute("my-key", myValue)
// access a custom attribute
val myValue = ctx.appAttribute("my-key") as MyValue
// call a custom method on a custom attribute
(ctx.appAttribute("my-key") as MyValue).myMethod()

In Javalin 6, the appAttribute methods have been renamed to appData, and the data is now typed through the Key<T> class. Data is now registered through the JavalinConfig, as opposed to the Javalin instance itself:

// register a custom attribute
static var myKey = new Key<MyValue>("my-key");
var app = Javalin.create(config -> {
    config.appData(myKey, myValue);
});
// access a custom attribute
var myValue = ctx.appData(myKey); // var will be inferred to MyValue
// call a custom method on a custom attribute
ctx.appData(myKey).myMethod();
// register a custom attribute
val myKey = Key<MyValue>("my-key")
val app = Javalin.create { config ->
    config.appData(myKey, myValue)
}
// access a custom attribute
val myValue = ctx.appData(myKey) // val will be inferred to MyValue
// call a custom method on a custom attribute
ctx.appData(myKey).myMethod()

You don’t have to store your keys in a static variable (although it’s recommended), so the shortest migration path would be to just replace appAttribute with appData and wrap your strings in Key<T> (both when declaring the attribute and when accessing it).

The Javalin#routes() method has been moved

In Javalin 5, you could attach routes by calling Javalin#routes(...), and then defining the routes inside the lambda. Since a lot of people did this after starting the server, we decided to move this to the config.

In Javalin 5:

var app = Javalin.create().start();
app.routes(() -> {
    get("/hello", ctx -> ctx.result("Hello World"));
});
val app = Javalin.create().start()
app.routes {
    get("/hello") { ctx -> ctx.result("Hello World") }
}

In Javalin 6:

var app = Javalin.createAndStart(config -> {
    config.router.apiBuilder(() -> {
        get("/hello", ctx -> ctx.result("Hello World"));
    });
});
val app = Javalin.createAndStart { config ->
    config.router.apiBuilder {
        get("/hello") { ctx -> ctx.result("Hello World") }
    }
}

Jetty config has been reworked

In Javalin5, you configured Jetty like this:

Javalin.create(config -> {
    config.jetty.server(serverSupplier); // set the Jetty Server for Javalin to run on
    config.jetty.sessionHandler(sessionHandlerSupplier); // set the SessionHandler that Jetty will use for sessions
    config.jetty.contextHandlerConfig(contextHandlerConsumer); // configure the ServletContextHandler Jetty runs on
    config.jetty.wsFactoryConfig(jettyWebSocketServletFactoryConsumer); // configure the JettyWebSocketServletFactory
    config.jetty.httpConfigurationConfig(httpConfigurationConsumer); // configure the HttpConfiguration of Jetty
});
Javalin.create { config ->
    config.jetty.server(serverSupplier) // set the Jetty Server for Javalin to run on
    config.jetty.sessionHandler(sessionHandlerSupplier) // set the SessionHandler that Jetty will use for sessions
    config.jetty.contextHandlerConfig(contextHandlerConsumer) // configure the ServletContextHandler Jetty runs on
    config.jetty.wsFactoryConfig(jettyWebSocketServletFactoryConsumer) // configure the JettyWebSocketServletFactory
    config.jetty.httpConfigurationConfig(httpConfigurationConsumer) // configure the HttpConfiguration of Jetty
}

This has been reworked a bit. We wanted to get rid of the supplier methods, and rather focus on giving users the option to modify the existing Jetty objects. In particular swapping out the Jetty Server could cause issues, both with Javalin internals and with Javalin plugins. The new Jetty config in Javalin 6 looks like this:

Javalin.create(config -> {
    config.jetty.defaultHost = "localhost"; // set the default host for Jetty
    config.jetty.defaultPort = 1234; // set the default port for Jetty
    config.jetty.threadPool = new ThreadPool(); // set the thread pool for Jetty
    config.jetty.multipartConfig = new MultipartConfig(); // set the multipart config for Jetty
    config.jetty.modifyJettyWebSocketServletFactory(factory -> {}); // modify the JettyWebSocketServletFactory
    config.jetty.modifyServer(server -> {}); // modify the Jetty Server
    config.jetty.modifyServletContextHandler(handler -> {}); // modify the ServletContextHandler (you can set a SessionHandler here)
    config.jetty.modifyHttpConfiguration(httpConfig -> {}); // modify the HttpConfiguration
    config.jetty.addConnector((server, httpConfig) -> new ServerConnector(server)); // add a connector to the Jetty Server
});
Javalin.create { config ->
    config.jetty.defaultHost = "localhost" // set the default host for Jetty
    config.jetty.defaultPort = 1234 // set the default port for Jetty
    config.jetty.threadPool = ThreadPool() // set the thread pool for Jetty
    config.jetty.multipartConfig = MultipartConfig() // set the multipart config for Jetty
    config.jetty.modifyJettyWebSocketServletFactory { factory -> } // modify the JettyWebSocketServletFactory
    config.jetty.modifyServer { server -> } // modify the Jetty Server
    config.jetty.modifyServletContextHandler { handler -> } // modify the ServletContextHandler (you can set a SessionHandler here)
    config.jetty.modifyHttpConfiguration { httpConfig -> } // modify the HttpConfiguration
    config.jetty.addConnector { server, httpConfig -> ServerConnector(server) } // add a connector to the Jetty Server
}

If you really need to set the Jetty Server, you can do so by accessing it through Javalin’s private config: config.pvt.jetty.server.

The plugin API has been reworked

In Javalin 5, plugins were made up of two interfaces, Plugin and PluginLifecycleInit.

interface Plugin {
    void apply(@NotNull Javalin app);
}

interface PluginLifecycleInit {
    void init(@NotNull Javalin app);
}

This API resulted in a lot of different looking plugins. There was no standardized way of doing configuration, and since both interfaces had access to the Javalin instance, it was unclear when to do what.

In Javalin 6 we’ve reworked the plugin API to be more opinionated. This will make things a bit harder for plugin developers, but it should make things a lot easier for end-users.

Plugins are represented by an abstract class Plugin that requires a config consumer and a default config in the constructor:

abstract class Plugin<CONFIG>(userConfig: Consumer<CONFIG>? = null, defaultConfig: CONFIG? = null) {
    open fun onInitialize(config: JavalinConfig) {} // optional hook for initializing the plugin
    open fun onStart(config: JavalinConfig) {} // optional hook for starting the plugin
    open fun repeatable(): Boolean = false // whether the plugin can be registered multiple times
    open fun priority(): PluginPriority = PluginPriority.NORMAL // the registration priority of the plugin [LOW, NORMAL, HIGH]
    open fun name(): String = this.javaClass.simpleName // the name of the plugin
    protected val pluginConfig // available to extending classes
}

Below you can find an example of a plugin without configuration, and a plugin with configuration.

Plugin with no configuration:

public class NoConfigPlugin extends Plugin<Void> {
    // optionally override any of the methods in the Plugin class
    // if you try to access pluginConfig, you will get an exception
}
open class NoConfigPlugin : Plugin<Void>() {
    // optionally override any of the methods in the Plugin class
    // if you try to access pluginConfig, you will get an exception
}

Plugin with configuration:

public class PluginWithConfig extends Plugin<PluginWithConfig.Config> {
    public PluginWithConfig(Consumer<Config> userConfig) {
        super(userConfig, new Config()); // user config and a default config are passed to the super constructor
    }
    // override any methods you want here
    static class Config { // could be stored in a separate file if you want
        String someField = "Default value";
    }
    var userValue = pluginConfig.someField // pluginConfig holds the config supplied by the user, applied to the default config
}
class PluginWithConfig(userConfig: Consumer<PluginConfig>) : Plugin<PluginConfig>(userConfig, PluginConfig()) {
    // user config and a default config are passed to the super constructor       ^^^^^^^^^^  ^^^^^^^^^^^^^^

    // override any methods you want here
    val userValue = pluginConfig.someField // pluginConfig holds the config supplied by the user, applied to the default config
}

class PluginConfig {
    @JvmField var someField: String = "Default value"
}

New signature for Context#async

In Javalin 5, the Context#async method had the following signature:

ctx.async(
    10L, // timeoutMillis
    () -> ctx.result("Timeout"), // onTimeout
    () -> { // task
        Thread.sleep(500L);
        ctx.result("Result");
    }
))
ctx.async(
    timeout = 10L,
    onTimeout = { ctx.result("Timeout") },
    task = {
        Thread.sleep(500L)
        ctx.result("Result")
    }
)

In Javalin 6, this was changed to a consumer-based signature, similar to many other Javalin APIs:

ctx.async(config -> {
    config.timeout = 10L;
    config.onTimeout(timeoutCtx -> timeoutCtx.result("Timeout"));
}, () -> {
    Thread.sleep(500L);
    ctx.result("Result");
});
ctx.async({ config ->
    config.timeout = 10L
    config.onTimeout { timeoutCtx -> timeoutCtx.result("Timeout") }
}) {
    Thread.sleep(500L)
    ctx.result("Result")
}

Static configuration methods have been removed

In Javalin 5, there were some classes which had their own static methods for configuration:

JavalinRenderer.register(myFileRenderer);
JavalinValidation.register(Custom.class, Custom::parse);
JavalinRenderer.register(myFileRenderer)
JavalinValidation.register(Custom.class, Custom::parse)

We’ve moved all these to the config for Javalin 6:

var app = Javalin.create(config -> {
  config.fileRenderer(myFileRenderer);
  config.validation.register(Custom.class, Custom::parse);
});
val app = Javalin.create { config ->
  config.fileRenderer(myFileRenderer)
  config.validation.register(Custom.class, Custom::parse)
}

Changes to private config

In Javalin 5, you could access Javalin’s private config through app.cfg, this has been change to app.unsafeConfig() in Javalin 6, in order to make it clear that it’s not recommended to access/change the config. We have also removed app.updateConfig(), as that also gave the impression that updating the config manually was a safe action.

Most end-users of Javalin should not need to access the private config, if you have a use-case that requires it, please reach out to us on Discord or GitHub.

Changes to compression

The Context interface now has a minSizeForCompression() function, which sets a minimum size for compression. If no value is set, this is populated from the current CompressionStrategy (which is set on the JavalinConfig). This allows you to enable compression for responses of unknown size, by calling minSizeForCompression(0).

We also added a compressionDecisionMade flag to CompressedOutputStream, to avoid this decision being made multiple times for the same output stream.

Compression config has also been moved from config.compression into config.http. In Javalin 5:

Javalin.create(config -> {
    config.compression.custom(compressionStrategy);      
    config.compression.brotliAndGzip(gzipLvl, brotliLvl);
    config.compression.gzipOnly(gzipLvl);                
    config.compression.brotliOnly(brotliLvl);            
    config.compression.none();                           
});
Javalin.create { config ->
    config.compression.custom(compressionStrategy)      
    config.compression.brotliAndGzip(gzipLvl, brotliLvl)
    config.compression.gzipOnly(gzipLvl)                
    config.compression.brotliOnly(brotliLvl)            
    config.compression.none()                           
}

In Javalin 6:

Javalin.create(config -> {
    config.http.customCompression(compressionStrategy);      
    config.http.brotliAndGzipCompression(gzipLvl, brotliLvl);
    config.http.gzipOnlyCompression(gzipLvl);                
    config.http.brotliOnlyCompression(brotliLvl);            
    config.http.disableCompression();                           
});
Javalin.create { config ->
    config.http.customCompression(compressionStrategy)      
    config.http.brotliAndGzipCompression(gzipLvl, brotliLvl)
    config.http.gzipOnlyCompression(gzipLvl)                
    config.http.brotliOnlyCompression(brotliLvl)            
    config.http.disableCompression()                           
}

Miscellaneous changes

Additional changes

It’s hard to keep track of everything, but you can look at the full commit log between the last 5.x version and 6.0.

If you run into something not covered by this guide, please edit this page on GitHub!

You can also reach out to us on Discord or GitHub.

Like Javalin?
Star us 😊

×