Spring AOP

July 21, 2026

AOP (Aspect-Oriented Programming - Lập trình hướng khía cạnh) trong Spring Boot là một kỹ thuật lập trình giúp tách biệt các Cross-cutting Concerns (các tính năng phụ/cắt ngang) ra khỏi luồng nghiệp vụ chính (Business Logic) của ứng dụng.

1. Spring Boot AOP là gì?

Trong phát triển phần mềm, ứng dụng thường có 2 nhóm logic:

  • Core Business Logic: Xử lý nghiệp vụ chính (ví dụ: Thanh toán đơn hàng, Tạo tài khoản, Tính tiền lương).

  • Cross-cutting Concerns: Các tác vụ xuất hiện lặp đi lặp lại ở nhiều tầng/module khác nhau (ví dụ: Logging, Security, Transaction, Caching, Performance Monitoring Execution Time).

Nếu viết trực tiếp các tác vụ phụ này vào hàm nghiệp vụ, mã nguồn sẽ bị rối (Code Tangling) và lặp lại (Code Scattering). AOP giải quyết vấn đề này bằng cách cho phép bạn định nghĩa các tác vụ phụ ở một nơi duy nhất và tự động "tiêm" (inject) chúng vào các hàm nghiệp vụ khi ứng dụng chạy.

Các khái niệm cốt lõi trong AOP

  • Aspect: Lớp chứa logic cắt ngang (ví dụ: LoggingAspect).
  • Join Point: Điểm trong chương trình nơi Aspect có thể can thiệp (trong Spring AOP, đây luôn là việc gọi một method).
  • Pointcut: Biểu thức xác định chính xác những method nào sẽ bị can thiệp (ví dụ: "tất cả method trong package service").
  • Advice: Hành động sẽ thực hiện và khi nào thực hiện:
    • @Before: Chạy trước khi method chính chạy.
    • @AfterReturning: Chạy sau khi method chính hoàn tất thành công.
    • @AfterThrowing: Chạy khi method chính ném ra Exception.
    • @Around: Can thiệp toàn bộ (trước, sau, hoặc thay đổi kết quả/ngắt luồng thực thi).

Hiểu khái niệm, ít dùng đôi khi không nhớ chi tiết, khi nào cần làm thì google hoặc AI thêm.

2. Các trường hợp sử dụng AOP trong dự án thực tế

Trong các dự án Spring Boot thực tế, AOP được ứng dụng rộng rãi ở nhiều bài toán:

2.1. Logging & Auditing (Ghi log hệ thống và nhật ký hành động)

  • Bài toán: Cần ghi log dữ liệu đầu vào (Input), đầu ra (Output), hoặc thời điểm gọi API của tất cả các Controller/Service để debug hoặc lưu lịch sử thao tác người dùng (Audit Trail).
  • Giải pháp AOP: Viết 1 Aspect @Around chặn các Controller/Service. Trước khi method chạy thì log tham số truyền vào, sau khi chạy xong thì log kết quả trả về, thay vì phải viết log.info(...) ở hàng chục method khác nhau.

2.2. Logging & Auditing (Ghi log hệ thống và nhật ký hành động)

  • Bài toán: Khi thực hiện thao tác dữ liệu phức tạp (ví dụ: Trừ. tiền ví A -> Cộng tiền ví B), nếu bước 2 lỗi thì bước 1 phải Rollback.
  • Giải pháp AOP: Annotation @Transactional huyền thoại của Spring chính là AOP. AOP sẽ mở Transaction trước khi method chạy, và tự động Commit nếu thành công hoặc Rollback nếu có RuntimeException.

2.3. Performance Monitoring (Đo hiệu năng / Execution Time)

  • Bài toán: Cần phát hiện các API hoặc Service chạy chậm (bottleneck) mà không muốn chèn hàm tính thời gian System.currentTimeMillis() vào từng hàm.
  • Giải pháp AOP: Tạo một custom annotation (ví dụ: @LogExecutionTime). Bất kỳ method nào đánh dấu annotation này sẽ được Aspect đếm thời gian thực thi và cảnh báo nếu vượt ngưỡng (ví dụ: > 500ms).

2.4. Custom Security & Authorization (Phân quyền truy cập).

  • Bài toán: Kiểm tra xem người dùng hiện tại có đủ quyền (Role/Permission) hoặc sở hữu đúng tài nguyên (Resource Ownership) trước khi cho phép thực thi hàm hay không.
  • Giải pháp AOP: Spring Security sử dụng AOP qua các annotation như. @PreAuthorize, @PostAuthorize. Bạn cũng thể viết Aspect riêng ( dụ: @CheckPermission("ORDER_DELETE")`) để chặn request ngay trước khi vào Service.

2.5. Rate Limiting & Throttling (Chống spam API)

  • Bài toán: Giới hạn số lượng request một IP hoặc User có thể gửi trong 1 phút để tránh DDoS hoặc lạm dụng API.
  • Giải pháp AOP: Kết hợp AOP với Redis/Bucket4j via custom annotation. @RateLimit(requests = 10, duration = 60)`. Aspect sẽ kiểm tra counter trong Redis trước khi cho phép gọi method chính.

2.6. Caching

  • Bài toán: Các hàm truy vấn dữ liệu ít thay đổi (như Danh mục sản phẩm, Cấu hình hệ thống) cần lưu vào Cache (Redis/Caffeine) để giảm tải cho DB.
  • Giải pháp AOP: Spring Cache sử dụng AOP qua @Cacheable, @CachePut, @CacheEvict. Aspect kiểm tra xem dữ liệu đã có trong Cache chưa, nếu có thì trả về ngay mà không chạy vào hàm chính.

3. Viết Thử Custom Annotation @LogExecutionTime.

Chúng ta sẽ thực hiện qua 4 bước:

  • Khai báo dependency AOP (nếu chưa có).
  • Tạo Custom Annotation @LogExecutionTime.
  • Tạo Aspect chứa logic đo thời gian thực thi.
  • Sử dụng Annotation trong Service/Controller và kiểm tra kết quả.

3.1: Thêm Dependency vào pom.xml.

Đảm bảo project của bạn đã có thư viện Spring Boot Starter AOP:

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

3.2: Tạo Custom Annotation @LogExecutionTime

Tạo một annotation để đánh dấu trên các method mà bạn muốn đo thời gian:

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) // Chỉ áp dụng cho Method
@Retention(RetentionPolicy.RUNTIME) // Có hiệu lực tại thời điểm ứng dụng đang chạy
public @interface LogExecutionTime {
}

3.3: Tạo Aspect xử lý logic @Around

Tạo lớp Aspect để can thiệp vào trước và sau khi method đánh dấu @LogExecutionTime chạy:

package com.example.demo.aspect;

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.stereotype.Component;

@Aspect
@Component
public class ExecutionTimeAspect {

    private static final Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);

    // Pointcut: Chặn tất cả các method có gắn annotation @LogExecutionTime
    @Around("@annotation(com.example.demo.annotation.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        // Cho phép method chính thực thi và nhận kết quả trả về
        Object proceed = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;

        // Ghi log tên class, tên method và thời gian chạy (ms)
        log.info("[AOP MONITOR] Method [{}.{}] executed in {} ms",
                joinPoint.getSignature().getDeclaringType().getSimpleName(),
                joinPoint.getSignature().getName(),
                executionTime);

        return proceed; // Trả về kết quả ban đầu của method chính
    }
}

3.4: Sử dụng Annotation trong Service

Gắn @LogExecutionTime lên bất kỳ method nào bạn muốn theo dõi:

package com.example.demo.service;

import com.example.demo.annotation.LogExecutionTime;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @LogExecutionTime
    public void processOrder() {
        try {
            // Giả lập công việc tốn 500ms (ví dụ: gọi database, gọi API bên ngoài)
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Kết quả khi chạy ứng dụng. Khi bạn gọi hàm. orderService.processOrder(), Spring AOP sẽ tự động kích hoạt Aspect và in ra log console giống như sau:

2026-08-12 17:52:10.123 INFO 12345 --- [main] c.e.demo.aspect.ExecutionTimeAspect : [AOP MONITOR] Method [OrderService.processOrder] executed in 504 ms

Lưu ý quan trọng khi dùng Spring AOP:.

AOP trong Spring hoạt động dựa trên kỹ thuật Proxy. Do đó, nếu bạn gọi một method có gắn @LogExecutionTime từ bên trong cùng một class (Internal call, ví dụ this.processOrder()), Spring AOP sẽ không bắt được sự kiện này. Method phải được gọi từ ngoài vào Bean (ví dụ Controller gọi sang Service).