Настройки, логирование и асинхронность
На ноутбуке разработчика и на сервере приложение одно и то же, а настройки разные: порт, адрес базы данных, путь на сайте. Их не вписывают в код, а передают переменными окружения — тогда один и тот же jar-файл запускается где угодно. В этом уроке соберём настройки в одном месте, включим журнал запросов, посмотрим полезные опции Javalin и асинхронные обработчики.
Настройки из окружения
В уроке о сборке main уже читал PORT. Соберём все настройки в запись:
// 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()); // для тестов
}
public static Javalin createApp(PostRepository repo, Settings settings) {
// ... как раньше, плюс в блоке настроек:
// config.router.contextPath = settings.contextPath();
}Без переменных всё работает как раньше. Если значение испорчено, например PORT=abc, программа падает сразу при старте, а не в середине работы:
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)
Журнал запросов
В уроке о before и after мы замеряли время вручную. В Javalin для этого есть готовый логгер запросов — он вызывается после каждого ответа и получает время обработки в миллисекундах. Замените им пару before/after:
// src/main/java/com/example/blog/Main.java
config.requestLogger.http((ctx, ms) ->
log.info("{} {} -> {} ({} мс)", ctx.method(), ctx.path(), ctx.statusCode(), Math.round(ms)));[JettyServerThreadPool-59] INFO com.example.blog.Main - GET /posts -> 200 (626 мс)
[JettyServerThreadPool-59] INFO com.example.blog.Main - GET /posts/99 -> 404 (29 мс)
[JettyServerThreadPool-59] INFO com.example.blog.Main - POST /posts -> 201 (52 мс)
В квадратных скобках — имя потока Jetty, который обработал запрос.
Полезные опции
| Опция | Что делает |
|---|---|
config.router.contextPath = "/api" | Всё приложение доступно под /api: /api/posts и так далее |
config.startup.showJavalinBanner = false | Убирает из лога большой рисунок-логотип Javalin |
config.jetty.threadPool = new QueuedThreadPool(50) | Свой пул потоков Jetty, здесь не больше 50 потоков |
config.concurrency.useVirtualThreads = true | Обрабатывать запросы в виртуальных потоках Java 21+ |
config.http.maxRequestSize | Наибольший размер тела запроса в байтах |
QueuedThreadPool лежит в пакете org.eclipse.jetty.util.thread. С CONTEXT_PATH=/api в логе появится Listening on http://localhost:8080/api, запрос /api/posts вернёт список, а /posts — страницу 404 от Jetty.
Асинхронные обработчики
Обработчик выполняется в потоке Jetty. Если ответ зависит от долгой операции, её можно отдать другому потоку через CompletableFuture и 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 принимает поставщик будущего результата и держит запрос открытым, пока оно не завершится. Запрос curl http://localhost:8080/stats вернёт {"posts":3}, а в логе видно, что ответ закончил другой поток: [ForkJoinPool.commonPool-worker-1] ... GET /stats -> 200. С виртуальными потоками это нужно реже: обычный блокирующий код и так не держит дорогие потоки ОС.
Частая ошибка
Если запустить CompletableFuture без ctx.future, Javalin не знает, что ответ ещё будет, и сразу отправляет пустой:
// 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
Никакой ошибки в логе нет. Асинхронную работу всегда передавайте в ctx.future(() -> ...).