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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
// 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:
- Java
- Kotlin
// 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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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
- We’ve removed support for jvmbrotli, as it’s no longer maintained. Use Brotli4j instead.
- We’ve added a small API for getting the type of status code a
HttpStatus
is. For example,status.isSuccess()
will return true for all 2xx status codes. - It’s now possible to exclude the Jetty websocket dependency without breaking Javalin. This is useful if you want to save a couple of bytes, or have other dependencies that conflict with Jetty’s websocket dependency.
- Now that Loom is part of the official JDKs, it is no longer opt-in, and therefor no longer enabled by default.
You can enable it by callingconfig.useVirtualThreads = true
. - The bundled plugins are now available on
config.bundledPlugins
, instead ofconfig.plugins
. - The
add
method in the CORS-plugin is now calledaddRule
.
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!