Введение в Spring AOP на примере кастомизации логирования

от автора

Аспектно-ориентированное программирование (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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *