Lập Trình Hướng Khía Cạnh (AOP) trong Spring: Tổng Quan, Thuật Ngữ và Ứng Dụng Thực Tế

Lập trình hướng khía cạnh (AOP – Aspect Oriented Programming) là một kỹ thuật lập trình bổ sung cho lập trình hướng đối tượng (OOP), mang đến một cách tiếp cận mới trong việc cấu trúc chương trình. Thay vì đối tượng là class như trong OOP, AOP tập trung vào aspect để xử lý các mối quan tâm cắt ngang (cross-cutting concerns).

(Lưu ý: Lý thuyết AOP có thể trừu tượng và khó nắm bắt ban đầu. Việc xem xét các ví dụ và ứng dụng thực tế sẽ giúp bạn hiểu rõ hơn về khái niệm này.)

Ứng Dụng Thực Tế: Chèn Log vào Method

Một trong những ứng dụng phổ biến nhất của AOP là chèn log vào các method mà không cần sửa đổi trực tiếp code của chúng. Ví dụ, giả sử bạn có một method sau:

public String callDaoSuccess(){
    return "dao1";
}

Bạn muốn thêm log khi method này được gọi. Cách tiếp cận thông thường sẽ là sửa đổi trực tiếp method:

public String callDaoSuccess(){
    System.out.println("callDaoSuccess is called");
    return "dao1";
}

Cách này có một số nhược điểm:

  1. Phải sửa đổi code trực tiếp trong method.
  2. Nếu một class có nhiều method cần thêm log, bạn phải lặp lại việc sửa đổi cho tất cả các method đó.
  3. Nếu method được gọi ở nhiều nơi, bạn phải tìm và chèn log vào tất cả các vị trí gọi method, gây tốn thời gian.

AOP giúp giải quyết vấn đề này một cách hiệu quả hơn. Các bước thực hiện như sau (với Spring Boot):

  1. Thêm thư viện Spring AOP vào project:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>2.4.5</version>
    </dependency>
  2. Tạo một class đánh dấu là Aspect:

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.Configuration;
    
    @Aspect
    @Configuration
    public class TestServiceAspect {
    
        private Logger logger = LoggerFactory.getLogger(TestServiceAspect.class);
    
        @Before("execution(* com.example.demo.*.*(..))")
        public void before(JoinPoint joinPoint){
            logger.info("before called " + joinPoint.getSignature().toString());
        }
    }

    Giải thích:

    • @Aspect: Đánh dấu class này là một Aspect.

    • @Before: Chỉ định method before sẽ được thực thi trước khi method được chỉ định bởi Pointcut được gọi.

    • *`execution( com.example.demo..(..))**: Đây là một Pointcut expression, nó định nghĩa khi nào thì Advice (trong trường hợp này là methodbefore) sẽ được thực thi. Cụ thể, nó chỉ định rằng Advice sẽ được thực thi trước khi bất kỳ method nào trong bất kỳ class nào trong packagecom.example.demo` được gọi. Bạn có thể điều chỉnh Pointcut expression để chỉ định chính xác các method mà bạn muốn áp dụng AOP. Ví dụ:

      @Before("execution(* com.example.demo.MyService.callDaoSuccess(..))")
      public void before(JoinPoint joinPoint){
          logger.info("before called " + joinPoint.getSignature().toString());
      }

      Chỉ áp dụng cho method callDaoSuccess trong class MyService.

Khi chạy ứng dụng, bạn sẽ thấy log được sinh ra từ Aspect trước khi log từ service:

2021-05-11 17:42:41.460 INFO 36803 – [ main] com.example.demo.TestServiceAspect : before called execution(String com.example.demo.MyService.callDaoSuccess())
2021-05-11 17:42:41.474 INFO 36803 – [ main] com.example.demo.MyService : callDaoSuccess is called

Ảnh minh họa log được tạo ra từ AOP, cho thấy advice được thực thi trước khi method gốc.

Để hiểu sâu hơn về AOP, chúng ta cần làm rõ các thuật ngữ quan trọng.

Các Thuật Ngữ Quan Trọng trong AOP

Pointcut

Pointcut định nghĩa thời điểm một Aspect sẽ được áp dụng. Nó sử dụng các biểu thức để chọn các điểm cụ thể trong chương trình, chẳng hạn như việc thực thi một method, truy cập một trường hoặc ném ra một exception. Trong ví dụ trên, execution(* com.example.demo.*.*(..)) là một Pointcut.

Advice

Advice là hành động được thực hiện khi Pointcut được kích hoạt. Nó là logic mà chúng ta muốn thực thi tại một điểm cụ thể trong chương trình. Trong ví dụ trên, method before là một Advice.

Aspect

Aspect là sự kết hợp giữa Pointcut và Advice. Nó đóng gói logic (Advice) và chỉ định khi nào logic đó sẽ được thực thi (Pointcut).

Join Point

Join Point là một điểm cụ thể trong chương trình mà Advice có thể được áp dụng. Ví dụ, việc thực thi một method là một Join Point. Khi code chạy và điều kiện pointcut đạt được, Advice được chạy. Join Point là một instance của Advice (thường được sử dụng để truy cập thông tin về điểm thực thi).

Các Loại Advice: @Before, @After, @AfterReturning, @AfterThrowing

Spring AOP cung cấp một số loại Advice khác nhau, cho phép bạn kiểm soát thời điểm Advice được thực thi so với Join Point:

  • @Before: Thực thi Advice trước khi Join Point được thực thi.
  • @After: Thực thi Advice sau khi Join Point được thực thi, bất kể Join Point có thành công hay ném ra exception.
  • @AfterReturning: Thực thi Advice sau khi Join Point được thực thi thành công (không có exception nào được ném ra).
  • @AfterThrowing: Thực thi Advice sau khi Join Point ném ra một exception.

Minh họa thời điểm thực thi của các loại Advice khác nhau trong AOP.

@Around

@Around Advice cho phép bạn bao quanh việc thực thi Join Point. Bạn có thể thực thi code trước và sau khi Join Point được thực thi, cũng như kiểm soát việc Join Point có được thực thi hay không.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

@Aspect
@Configuration
public class PerformanceAspect {

    private Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);

    @Around("execution(* com.example.demo.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        logger.info("Start Time Taken by {} is {}", joinPoint, startTime);

        Object result = joinPoint.proceed(); // Thực thi method

        long timeTaken = System.currentTimeMillis() - startTime;
        logger.info("Time Taken by {} is {}", joinPoint, timeTaken);

        return result;
    }
}

Method around được gọi trước khi method được chỉ định bởi Pointcut được gọi. Bên trong method around, joinPoint.proceed() được gọi để thực thi method gốc. Sau khi method gốc hoàn thành, code tiếp theo trong method around được thực thi.

Một cách sử dụng @Around khác là kết hợp với Annotation.

@Around với Annotation

Bạn có thể tạo một Annotation tùy chỉnh để đánh dấu các method mà bạn muốn áp dụng AOP.

  1. Tạo một Annotation:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TrackTime {
    }
    • @Target(ElementType.METHOD): Chỉ định rằng annotation này chỉ có thể được áp dụng cho method.
    • @Retention(RetentionPolicy.RUNTIME): Chỉ định rằng annotation này sẽ có sẵn tại thời điểm runtime.
  2. Sửa đổi @Around Advice để sử dụng Annotation:

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.Configuration;
    
    @Aspect
    @Configuration
    public class PerformanceAnnotationAspect {
    
        private Logger logger = LoggerFactory.getLogger(PerformanceAnnotationAspect.class);
    
        @Around("@annotation(com.example.demo.TrackTime)")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            long startTime = System.currentTimeMillis();
            logger.info("Start Time Taken by {} is {}", joinPoint, startTime);
    
            Object result = joinPoint.proceed();
    
            long timeTaken = System.currentTimeMillis() - startTime;
            logger.info("Time Taken by {} is {}", joinPoint, timeTaken);
    
            return result;
        }
    }

    Thay vì sử dụng execution, chúng ta sử dụng annotation để chỉ định rằng Advice sẽ được áp dụng cho các method được đánh dấu bằng annotation @TrackTime.

  3. Áp dụng Annotation cho Method:

    import org.springframework.stereotype.Service;
    
    @Service
    public class MyService {
    
        @TrackTime
        public String callMethodTrackTime(){
            System.out.println("callDaoSuccess is called");
            return "dao1";
        }
    }

    Method callMethodTrackTime sẽ được đo thời gian thực thi bởi @Around Advice.

Sơ đồ minh họa cách AOP hoạt động với Annotation.

Kết luận

Bài viết này cung cấp một cái nhìn tổng quan về Spring AOP, bao gồm các khái niệm cơ bản, thuật ngữ quan trọng và ví dụ thực tế. AOP là một công cụ mạnh mẽ giúp bạn giải quyết các mối quan tâm cắt ngang một cách hiệu quả, giúp code của bạn trở nên dễ bảo trì và mở rộng hơn. Bằng cách hiểu rõ các khái niệm và ứng dụng của AOP, bạn có thể tận dụng tối đa sức mạnh của nó trong các dự án Spring của mình.