Введение в 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, а также логи из методов не помеченных аннотацией, не сохранились.
Преимущества использования AOP для логирования
Минимизация дублирования кода: Логика логирования отделена от основной бизнес-логики.
Гибкость: Мы можем добавлять логирование в любые методы, помеченные аннотацией
@Loggable
, без изменения их кода.Легкость тестирования: Логи собираются и сохраняются централизованно, что упрощает их тестирование и сохранение.
Заключение
Использование AOP для логирования в Spring позволяет легко разделить кросс-функциональные задачи, такие как логирование, от основной бизнес-логики. Этот подход делает код более читаемым и тестируемым, позволяя сосредоточиться на реализации основной функциональности. В этой статье мы рассмотрели создание кастомной аннотации для логирования, аспект для перехвата методов и сохранение логов в базе данных с помощью Spring.