Как организовать логирование вызовов делегатов
Что проверяют
В Java нет делегатов в смысле C#, поэтому сначала смотрят, как вы поняли вопрос: объект, которому класс передаёт работу (паттерн делегирования), или функциональный интерфейс вроде Function и Runnable. Дальше важно, умеете ли вы добавить сквозную функциональность, не размазывая логи по бизнес-коду. Следом часто спрашивают, почему аспект молчит при вызове метода изнутри того же класса.
Как отвечать
- декоратор:
LoggingPaymentService implements PaymentServiceоборачивает реальную реализацию и пишет вход, выход, длительность и исключение. Прозрачно, но на каждый интерфейс нужен свой класс; - динамический прокси:
Proxy.newProxyInstanceсInvocationHandlerлогирует любой интерфейс одной реализацией. Подходит только для интерфейсов, для классов нужна генерация байткода, например ByteBuddy; - Spring AOP:
@Aspectс@Around("@annotation(com.example.Logged)")или по пакету. Самый частый ответ в проектах на Spring. Аспект работает через прокси, поэтому вызовthis.method()внутри бина мимо него проходит, аprivate-методы не перехватываются; - лямбды: для функциональных интерфейсов хватит обёртки
static <T, R> Function<T, R> logged(String name, Function<T, R> f), которая логирует до и послеf.apply.
Логируйте через SLF4J с параметрами (log.debug("call {} args={}", name, args)), без конкатенации строк. Для тяжёлых аргументов проверяйте уровень или используйте fluent API SLF4J 2 (log.atDebug().addArgument(...)). Идентификатор запроса кладите в MDC, чтобы вызовы одного запроса собирались вместе. Персональные данные и секреты в аргументах маскируйте. Если длительность нужна как метрика, а не строка в логе, лучше подойдут @Timed или Observation API из Micrometer.
Где спотыкаются
Логируют каждый вызов на INFO с полным toString() аргументов. Логи раздуваются, а в них утекают пароли и токены.
Не знают про самовызов в Spring AOP и удивляются, что аннотированный метод ничего не пишет.
Глотают исключение в обёртке: логируют и не пробрасывают дальше, меняя поведение программы. Декоратор должен залогировать исключение и выбросить его снова.