Аспектно-ориентированное программирование (AOP) — это мощный инструмент для разделения кода, который позволяет изолировать кросс-функциональные задачи, такие как логирование, обработка транзакций и безопасность, от основной бизнес-логики. В этой статье мы рассмотрим, как использовать AOP в Spring на примере реализации кастомного логирования с помощью аннотации и аспектов.
Что такое AOP?
AOP (Aspect-Oriented Programming) — это парадигма программирования, которая позволяет разделить кросс-функциональные задачи (такие как логирование, безопасность и транзакции) от основной бизнес-логики. В Spring AOP часто используется для обработки таких задач, не изменяя основной код приложения.
Попробуем разобраться в AOP на примере типовой задачи, с которой периодически сталкиваются разработчики в рамках проектов. К примеру, есть тяжеловесный эндпоинт, который вызывает большое количество методов, скажем, 1000. В ходе вызова этих методов создается большой объем логов, будь то info
, warn
, error
и т. д. Проблема в том, что логи уровня warn
и error
быстро отображаются в таких системах, как Portainer, но по прошествии небольшого промежутка времени, мы можем найти их только в условном Graylog. Но эти логи важны, а каждый раз искать их в Graylog не хочется. Решением подобной проблемы может стать сохранение важных логов в базу данных с возможностью их дальнейшего получения через эндпоинт.
Но как сохранять логи? Допустим, в 600 из 1000 методов есть логи уровня warn
и error
, и 200 из них было бы неплохо сохранить. В этой ситуации среди прочих (возможно, более простых) решений задачи можно выделить использование AOP.
Мы создадим пользовательскую аннотацию @Loggable
, которая будет использовать AOP для сбора логов, выполненных внутри методов, помеченных этой аннотацией. Все сообщения, генерируемые через log.warn()
, будут собираться в текущем потоке и сохраняться в базе данных в конце выполнения метода контроллера.
1. Создание аннотации @Loggable
Наша кастомная аннотация @Loggable
будет использоваться для пометки методов, выполнение которых мы хотим логировать.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Loggable { }
Эта аннотация будет указывать аспекту, что метод нуждается в логировании.
2. Реализация аспекта для перехвата методов с аннотацией @Loggable
Для реализации AOP в Spring нам нужно создать класс аспекта, который будет перехватывать методы с аннотацией @Loggable
и собирать логи. Аспект будет использовать @Around
для перехвата выполнения методов.
@Aspect @Component @Slf4j public class LoggableAspect { private static final ThreadLocal<List<String>> threadLocalLogs = ThreadLocal.withInitial(ArrayList::new); @Pointcut("@annotation(Loggable)") public void loggableMethods() { } @Around("loggableMethods()") public Object collectLogs(ProceedingJoinPoint joinPoint) throws Throwable { Object result = joinPoint.proceed(); return result; } public static void addLogMessage(String message) { threadLocalLogs.get().add(message); } public static List<String> getLogs() { return new ArrayList<>(threadLocalLogs.get()); } public static void clearLogs() { threadLocalLogs.get().clear(); } }
3. Создание пользовательского аппендера для логирования
Вместо использования стандартных аппендеров мы создадим кастомный аппендер для логирования сообщений, собранных в потоке. В нашем случае мы будем использовать ThreadLocal
для хранения логов в пределах одного потока, чтобы избежать конфликтов при параллельном выполнении.
import ch.qos.logback.classic.Level; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.classic.spi.ILoggingEvent; public class LogAppender extends AppenderBase<ILoggingEvent> { @Override protected void append(ILoggingEvent eventObject) { if (eventObject.getLevel().isGreaterOrEqual(Level.WARN)) { String logMessage = eventObject.getFormattedMessage(); // Добавляем сообщение в ThreadLocal LoggableAspect.addLogMessage(logMessage); } } }
Конфигурация логирования с Logback
Для настройки логирования используем Logback. Это можно сделать в файле logback.xml
, который должен быть размещен в директории src/main/resources
проекта. Мы добавим туда кастомный аппендер и определим формат сообщений.
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- Регистрация кастомного аппендера с полным путем к классу --> <appender name="LogAppender" class="spring.aop.LogAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>WARN</level> <!-- Указываем уровень WARN --> <onMatch>ACCEPT</onMatch> <!-- При совпадении уровня, логируем --> <onMismatch>DENY</onMismatch> <!-- При несоответствии уровня, не логируем --> </filter> </appender> <!-- Конфигурация root логгера, логируем только WARN сообщения и выше --> <root level="WARN"> <appender-ref ref="LogAppender"/> </root> </configuration>
Добавляем несколько сервисов, которые будут содержать в себе методы в логами.
@Slf4j @Service public class EmailService { @Loggable public void sendEmail() { log.warn("email sending.."); log.warn("sent email!"); } } @Slf4j @Service public class SmsService { @Loggable public void sendSms() { log.warn("sms sending.."); log.warn("sent sms!"); } } @Slf4j @Service public class PushService { @Loggable public void sendPush() { log.warn("push sending.."); log.warn("sent push!"); } } @Slf4j @Service public class PhoneService { @Loggable public void calling() { log.warn("calling.."); log.warn("conversation is over!"); } @Loggable public void callingToSkype() { log.error("Skype is not available"); } public void callingToZoom() { log.error("Zoom calls are not logged"); } } @Slf4j @Service public class MessengerService { @Loggable public void sendMessageToTelegram() { log.warn("(TELEGRAM) message sending.."); log.warn("(TELEGRAM) sent message!"); } @Loggable public void sendMessageToViber() { log.warn("(VIBER) message sending.."); log.warn("(VIBER) sent message!"); } }
Сохранение логов в базу данных
Теперь, когда все логи собраны в потоке, нам нужно сохранить их в базе данных. В контроллере мы будем вызывать метод saveLogs()
, который получит все собранные логи и сохранит их в базе данных.
@Entity @Data public class LogEntry { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String message; }
import org.springframework.data.jpa.repository.JpaRepository; public interface LogEntryRepository extends JpaRepository<LogEntry, Long> { }
@RequiredArgsConstructor @Service public class LogService { private final LogEntryRepository logEntryRepository; @Transactional public void saveLogs() { List<String> logs = LoggableAspect.getLogs(); if (!logs.isEmpty()) { List<LogEntry> logEntries = logs.stream() .map(message -> { LogEntry logEntry = new LogEntry(); logEntry.setMessage(message); return logEntry; }) .toList(); logEntryRepository.saveAll(logEntries); } LoggableAspect.clearLogs(); } }
@RestController @RequiredArgsConstructor public class CallCenterController { private final EmailService emailService; private final MessengerService messengerService; private final SmsService smsService; private final PushService pushService; private final PhoneService phoneService; private final LogService logService; @GetMapping("/call") public String call() { try { emailService.sendEmail(); smsService.sendEmail(); pushService.sendPush(); phoneService.calling(); phoneService.callingToSkype(); phoneService.callingToZoom(); messengerService.sendMessageToTelegram(); messengerService.sendMessageToViber(); } finally { logService.saveLogs(); } return "Completed!"; } }
В результате работы метода, в базе данных мы увидим логи уровня warn из вызванных методов помеченных аннотацией @Loggable. Логи уровня error, а также логи из методов не помеченных аннотацией, не сохранились.
Преимущества использования AOP для логирования
-
Минимизация дублирования кода: Логика логирования отделена от основной бизнес‑логики.
-
Гибкость: Мы можем добавлять логирование в любые методы, помеченные аннотацией
@Loggable
, без изменения их кода. -
Легкость тестирования: Логи собираются и сохраняются централизованно, что упрощает их тестирование и сохранение.
Заключение
Использование AOP для логирования в Spring позволяет легко разделить кросс-функциональные задачи, такие как логирование, от основной бизнес-логики. Этот подход делает код более читаемым и тестируемым, позволяя сосредоточиться на реализации основной функциональности. В этой статье мы рассмотрели создание кастомной аннотации для логирования, аспект для перехвата методов и сохранение логов в базе данных с помощью Spring.
ссылка на оригинал статьи https://habr.com/ru/articles/861262/
Добавить комментарий