Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Настройки, логирование и асинхронность

На ноутбуке разработчика и на сервере приложение одно и то же, а настройки разные: порт, адрес базы данных, путь на сайте. Их не вписывают в код, а передают переменными окружения — тогда один и тот же 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(() -> ...).

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.