Введение в 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> 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 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 {

    @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 проекта. Мы добавим туда кастомный аппендер и определим формат сообщений.




    
    
        
            WARN 
            ACCEPT  
            DENY 
        
    

    
    
        
    


Добавляем несколько сервисов, которые будут содержать в себе методы в логами.

@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 sendEmail() {
        log.warn("email sending..");
        log.warn("sent email!");
    }
}

@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 {
}
@RequiredArgsConstructor
@Service
public class LogService {
    private final LogEntryRepository logEntryRepository;

    @Transactional
    public void saveLogs() {
        List logs = LoggableAspect.getLogs();

        if (!logs.isEmpty()) {
            List 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, а также логи из методов не помеченных аннотацией, не сохранились.

141dea7b692e097ae79accc107b223ec.png

Преимущества использования AOP для логирования

  • Минимизация дублирования кода: Логика логирования отделена от основной бизнес-логики.

  • Гибкость: Мы можем добавлять логирование в любые методы, помеченные аннотацией @Loggable, без изменения их кода.

  • Легкость тестирования: Логи собираются и сохраняются централизованно, что упрощает их тестирование и сохранение.

Заключение

Использование AOP для логирования в Spring позволяет легко разделить кросс-функциональные задачи, такие как логирование, от основной бизнес-логики. Этот подход делает код более читаемым и тестируемым, позволяя сосредоточиться на реализации основной функциональности. В этой статье мы рассмотрели создание кастомной аннотации для логирования, аспект для перехвата методов и сохранение логов в базе данных с помощью Spring.

© Habrahabr.ru