Logging với AOP trong Spring Boot

Để tiếp tục của bài viết Aspect Oriented Programming (AOP) và Spring AOP. Trong bài viết này sẽ trình bày chi tiết hơn về Spring AOP. Cách dẫn tạo aspect bằng một vị dụ về việc ghi log cho ứng dụng.

Giới thiệu

Trong bài viết này, mình phát triển một ứng dụng CRUD đơn giản, và sử dụng AOP cho việc ghi log. Cấu trúc dự án như hình bên dưới. Việc ghi log của ứng dụng sẽ triển khai trong lớp LoggingAspect.java

loggingwithaop
    │   LoggingWithAopApplication.java
    │
    ├───aspect
    │       LoggingAspect.java
    │
    ├───controller
    │       ProductController.java
    │
    ├───exception
    │       ErrorDetails.java
    │       GlobalExceptionHandler.java
    │       ResourceNotFoundException.java
    │
    ├───model
    │       Product.java
    │
    ├───payload
    │       ResponseDTO.java
    │
    ├───repository
    │       ProductRepository.java
    │
    └───service
            ProductService.java

Dependency

Thêm dependency sau để sử dụng Spring AOP

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Tạo logging aspect

Đây là lớp định nghĩa các advice, pointcut liên quan đến việc ghi log cho toàn ứng dụng.

package com.pad.loggingwithaop.aspect;
@Aspect
@Component
public class LoggingAspect {
    // khai báo các advice, poincut ở đây
}

Tạo một advice

Ví dụ cho trường hợp: ghi log trước khi phương thức updateProduct() được thực hiện, cùng xem đoạn code dưới đây và phân tích advice này

@Before("execution(* com.pad.loggingwithaop.service.ProductService.updateProduct(..))")
public void logParamBeforeExecuteUpdateProduct(JoinPoint joinPoint){
    log.debug("Log before executing method...");
    log.debug("This is qualified name: {}", joinPoint.getSignature().getDeclaringTypeName());
    log.debug("This is method name: {}", joinPoint.getSignature().getName());
    log.debug("Arguments of method: {}", Arrays.toString(joinPoint.getArgs()));
}

Và kết quả của advice kia như sau:

Phân tích qua advice ví dụ trên

Logging với AOP trong Spring Boot

Tạo pointcut

Để hiểu rõ hơn cách viết pointcut, hãy phân tích một vài biểu thức pointcut sau:

Logging với AOP trong Spring boot - cách tạo apsect

Trong trường hợp bạn muốn hạn chế các điểm joint point khớp với biểu thức, bạn có thể sử dụng biểu thức && (và), || (hoặc), ! (phủ định) để kết xây dựng biểu thức. Biểu thức ở dưới đây có nghĩa, xác định các điểm join point là thực thi phương thức updateProduct() khi có lời gọi từ phương thức OrderController().

execution(* com.pad.loggingwithaop.service.ProductService.updateProduct(..)) && within(com.pad.loggingwithaop.controller.OrderController)

Bạn cũng có thể định nghĩa một pointcut để tái sử dụng.

    @Pointcut("execution(* com.pad.loggingwithaop.service.ProductService.getProductById(..))")
    public void getProductById(){}

    @Before(value = "getProductById()")
    public void beforeGetProductById(JoinPoint joinPoint){
        log.debug("Get product with id {}", joinPoint.getArgs());
    }
    @AfterReturning(value = "getProductById()", returning = "result")
    public void afterReturnGetProductById(JoinPoint joinPoint, ResponseDTO result){
        log.debug("Value of product {} : {}", joinPoint.getArgs(), result);
    }

Mã nguồn hoàn chỉnh

package com.pad.loggingwithaop.aspect;

import com.pad.loggingwithaop.model.Product;
import com.pad.loggingwithaop.payload.ResponseDTO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
public class LoggingAspect {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Before("execution(* com.pad.loggingwithaop.service.ProductService.updateProduct(..)) && within(com.pad.loggingwithaop.repository.*)")
    public void logParamBeforeExecuteUpdateProduct(JoinPoint joinPoint){
        log.debug("Log before executing method...");
        log.debug("This is qualified name: {}", joinPoint.getSignature().getDeclaringTypeName());
        log.debug("This is method name: {}", joinPoint.getSignature().getName());
        log.debug("Arguments of method: {}", Arrays.toString(joinPoint.getArgs()));
    }

    @Pointcut("execution(* com.pad.loggingwithaop.service.ProductService.getProductById(..))")
    public void getProductById(){}

    @Before(value = "getProductById()")
    public void beforeGetProductById(JoinPoint joinPoint){
        log.debug("Get product with id {}", joinPoint.getArgs());
    }

    @AfterReturning(value = "getProductById()", returning = "result")
    public void afterReturnGetProductById(JoinPoint joinPoint, ResponseDTO result){
        log.debug("Value of product {} : {}", joinPoint.getArgs(), result);
    }

    @AfterThrowing(value = "getProductById()")
    public void handleGetProductByIdException(JoinPoint joinPoint){
        log.debug("Can not find product with id: {}", joinPoint.getArgs());
    }

    @Around("execution(* com.pad.loggingwithaop.service.ProductService.deleteProduct(..))")
    public void logAfterDeleteProduct(ProceedingJoinPoint joinPoint) throws Throwable{
        try{
            joinPoint.proceed();
            log.debug("Delete product {} successfully", joinPoint.getArgs());
        }
        catch (Exception e){
            log.debug("Can not find product with id: {}", joinPoint.getArgs());
            throw e;
        }
    }

}

Source code:

GitHub: https://github.com/pad1092/SpringBoot-AOP-Logging