Если пишешь @Slf4j над классом, вызываешь log.info(...) и не задумываешься, а как это работает, то пост для тебя. Разберём, что стоит за аннотацией, и как не напороться на неочевидные грабли.
🔹 Что делает аннотация
@Slf4j аннотация Lombok'а. При компиляции он через annotation processor генерирует в классе поле:
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(YourClass.class);
Всё, никакой магии в рантайме. Просто кодогенерация на этапе javac. Если открыть .class — поле будет там как родное.
Нюанс: если и родитель, и наследник аннотированы @Slf4j, каждый получит свой собственный логгер, привязанный к своему классу.
🔹 SLF4J — это фасад, а не реализация
Ключевая идея, которую стоит держать в голове: SLF4J сам ничего не логирует. Это API-контракт. Реальная работа на binding'е:
→ Logback — нативный бэкенд, писался теми же людьми
→ Log4j2 — через
log4j-slf4j2-impl→ java.util.logging — через
slf4j-jdk14Binding определяется в classpath в compile/runtime. SLF4J при старте ищет класс org.slf4j.spi.SLF4JServiceProvider (начиная с 2.x) или StaticLoggerBinder (в 1.x). Нашёл — работает. Не нашёл — NOP logger, и ты молча теряешь все логи. В 2.x хотя бы warning кинет.
🔹 Два binding'а в classpath
Классическая проблема транзитивных зависимостей. Если в classpath окажутся и logback-classic, и log4j-slf4j2-impl — SLF4J выберет один (фактически — какой первый найдёт classloader). Поведение недетерминировано. Лечится в Maven через <exclusions> или в Gradle через exclude group.
🔹 Что под капотом вызова
log.info("User {} logged in from {}", username, ip);Здесь нет конкатенации строк. SLF4J использует параметризованные сообщения — плейсхолдеры {} заменяются лениво, только если уровень логирования активен. Это важно: вызов log.debug(...) при выключенном DEBUG не тратит ресурсы на toString() аргументов.
Но есть подвох. Если ты передаёшь выражение, которое само по себе дорого вычислять:
log.debug("Heavy result: {}", calculateSomethingExpensive());Метод всё равно вызовется — аргументы вычисляются до передачи в метод (JLS, eager evaluation). Вариант обхода:
if (log.isDebugEnabled()) {
log.debug("Heavy result: {}", calculateSomethingExpensive());
}Или, если на Log4j2 API напрямую — там есть Supplier-based ленивые аргументы. В SLF4J 2.x появился fluent API:
log.atDebug()
.setMessage("Heavy result: {}")
.addArgument(() -> calculateSomethingExpensive())
.log();
🔹 Логирование исключений
Правильный способ — throwable последним аргументом без плейсхолдера:
log.error("Payment failed for order {}", orderId, exception);Два {} — нет. Два аргумента — да. SLF4J сам определит, что последний аргумент — Throwable, и выведет стектрейс. Если засунуть exception в {}, получишь только toString() без стектрейса. Это одна из самых частых ошибок.
🔹 Мелочи, на которые стоит обратить внимание
▪️ @Slf4j vs @Log4j2
Lombok умеет генерировать логгеры для разных фасадов/реализаций. @Slf4j — для SLF4J API, @Log4j2 — напрямую для Log4j2 API (со всеми его фичами типа Supplier-аргументов и Markers). Выбирай осознанно.
▪️ Имя логгера
По умолчанию это FQCN класса. Можно переопределить: @Slf4j(topic = "audit"). Удобно для выделения audit/security логов в отдельный appender.
▪️ Serializable классы
Logger не сериализуем. Если класс implements Serializable, после десериализации log будет null. Lombok добавляет transient, но при ручном объявлении — легко забыть.
▪️ Тесты
Если в тестовом classpath нет binding'а, логи молча пропадут. Кинь slf4j-simple в testImplementation / test scope, чтобы видеть что происходит.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava