Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Settings, logging and async handlers

The application is the same on a developer's laptop and on a server, but the settings differ: the port, the database address, the path on the site. They are not written into the code; they are passed as environment variables — then the same jar file runs anywhere. In this lesson we gather the settings in one place, turn on the request log, look at useful Javalin options and at async handlers.

Settings from the environment

In the packaging lesson main already read PORT. Let us gather all settings in a record:

// src/main/java/com/example/blog/Settings.java
package com.example.blog;
 
import java.util.Map;
 
public record Settings(int port, String dbUrl, String contextPath) {
 
    public static Settings fromEnv() {
        Map<String, String> env = System.getenv();
        return new Settings(
                Integer.parseInt(env.getOrDefault("PORT", "8080")),
                env.getOrDefault("DB_URL", "jdbc:h2:./blog"),
                env.getOrDefault("CONTEXT_PATH", "/")
        );
    }
}
// src/main/java/com/example/blog/Main.java
public static void main(String[] args) {
    Settings settings = Settings.fromEnv();
    var repo = new PostRepository(Database.create(settings.dbUrl()));
    createApp(repo, settings).start(settings.port());
}
 
public static Javalin createApp(PostRepository repo) {
    return createApp(repo, Settings.fromEnv());   // for the tests
}
 
public static Javalin createApp(PostRepository repo, Settings settings) {
    // ... as before, plus in the config block:
    // config.router.contextPath = settings.contextPath();
}

Without variables everything works as before. If a value is broken, for example PORT=abc, the program fails right at startup rather than in the middle of its work:

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	...
	at com.example.blog.Settings.fromEnv(Settings.java:11)

The request log

In the before and after lesson we measured the time by hand. Javalin has a ready request logger for this — it is called after every response and receives the handling time in milliseconds. Replace the before/after pair with it:

// src/main/java/com/example/blog/Main.java
config.requestLogger.http((ctx, ms) ->
        log.info("{} {} -> {} ({} ms)", ctx.method(), ctx.path(), ctx.statusCode(), Math.round(ms)));
[JettyServerThreadPool-59] INFO com.example.blog.Main - GET /posts -> 200 (579 ms)
[JettyServerThreadPool-59] INFO com.example.blog.Main - GET /posts/99 -> 404 (27 ms)
[JettyServerThreadPool-59] INFO com.example.blog.Main - POST /posts -> 201 (48 ms)

The square brackets hold the name of the Jetty thread that handled the request.

Useful options

OptionWhat it does
config.router.contextPath = "/api"The whole application lives under /api: /api/posts and so on
config.startup.showJavalinBanner = falseRemoves the big Javalin logo drawing from the log
config.jetty.threadPool = new QueuedThreadPool(50)Your own Jetty thread pool, here at most 50 threads
config.concurrency.useVirtualThreads = trueHandle requests in Java 21+ virtual threads
config.http.maxRequestSizeThe largest request body size in bytes

QueuedThreadPool lives in the org.eclipse.jetty.util.thread package. With CONTEXT_PATH=/api the log shows Listening on http://localhost:8080/api, the request /api/posts returns the list, and /posts returns a 404 page from Jetty.

Async handlers

A handler runs in a Jetty thread. If the response depends on a long operation, you can hand it to another thread with CompletableFuture and ctx.future:

// src/main/java/com/example/blog/Main.java
config.routes.get("/stats", ctx -> ctx.future(() ->
        CompletableFuture.supplyAsync(repo::findAll)
                .thenAccept(all -> ctx.json(Map.of("posts", all.size())))));

ctx.future takes a supplier of a future result and keeps the request open until it completes. The request curl http://localhost:8080/stats returns {"posts":2}, and the log shows that another thread finished the response: [ForkJoinPool.commonPool-worker-1] ... GET /stats -> 200. With virtual threads you need this less often: ordinary blocking code does not hold expensive OS threads anyway.

Common mistake

If you start a CompletableFuture without ctx.future, Javalin does not know a response is still coming and sends an empty one right away:

// src/main/java/com/example/blog/Main.java
config.routes.get("/stats", ctx -> {
    CompletableFuture.supplyAsync(repo::findAll)
            .thenAccept(all -> ctx.json(Map.of("posts", all.size())));
});
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 0

There is no error in the log. Always pass async work to ctx.future(() -> ...).

Thanks for reading!

Donate to the site — we work hard for you.