Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров3.4K

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

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+4
Комментарии2

Публикации

Работа

Java разработчик
182 вакансии

Ближайшие события