Skip to content

Spring AOP: hiểu từ khái niệm đến proxy bên dưới

9 min read

@Transactional, @Cacheable, @Async, @PreAuthorize… tất cả đều chạy trên cùng một nền móng: Spring AOP. Hiểu AOP là hiểu vì sao những annotation này “thần kỳ” — và vì sao đôi khi chúng im lặng không hoạt động.


TL;DR#

  • AOP (Aspect-Oriented Programming) tách các cross-cutting concern (logging, transaction, security, caching, metrics) ra khỏi business logic.
  • Spring AOP hoạt động bằng proxy lúc runtime: Spring bọc bean của bạn trong một proxy, proxy chặn lời gọi method và chạy thêm logic (advice) trước/sau.
  • Hai loại proxy: JDK Dynamic Proxy (dựa trên interface) và CGLIB (tạo subclass). Spring Boot mặc định dùng CGLIB.
  • 5 loại advice: @Before, @After, @AfterReturning, @AfterThrowing, @Around.
  • Giới hạn quan trọng nhất: chỉ chặn được lời gọi đi qua proxy. Gọi nội bộ (this.method() — self-invocation), method private, method/class final, object không phải Spring bean → AOP không có tác dụng.
  • Cần sức mạnh lớn hơn (field access, constructor, self-invocation) → dùng AspectJ weaving thật sự.
flowchart LR
    C[Caller] --> P["Proxy (chứa advice)"]
    P --> T[Target bean thật]
    T -. "this.otherMethod() - không qua proxy" .-> T

1. Vấn đề AOP giải quyết#

Giả sử bạn cần đo thời gian thực thi và log cho mọi method trong service layer:

public Order placeOrder(OrderRequest req) {
    long start = System.currentTimeMillis();
    log.info("placeOrder start");
    try {
        // ... business logic thật sự, chỉ vài dòng ...
        return order;
    } finally {
        log.info("placeOrder took {} ms", System.currentTimeMillis() - start);
    }
}

Lặp lại đoạn này ở 200 method thì: code trùng lặp (code scattering), business logic bị lẫn với technical concern (code tangling), sửa format log phải sửa 200 chỗ.

AOP cho phép viết logic đó một lần và khai báo nơi áp dụng.


2. Thuật ngữ cốt lõi#

Thuật ngữ Ý nghĩa Ví dụ
Aspect Module chứa cross-cutting concern Class LoggingAspect
Join Point Điểm trong quá trình thực thi có thể chèn logic. Với Spring AOP, join point luôn là method execution Lời gọi placeOrder()
Pointcut Biểu thức chọn ra các join point execution(* com.shop.service..*(..))
Advice Logic được chạy tại join point, kèm thời điểm chạy @Around đo thời gian
Target Object gốc bị advise Bean OrderService
Proxy Object bọc target, do Spring tạo ra OrderService$$SpringCGLIB$$0
Weaving Quá trình gắn aspect vào target Spring: runtime; AspectJ: compile/load-time

Công thức dễ nhớ: Aspect = Pointcut (ở đâu) + Advice (làm gì, khi nào).


3. Viết aspect đầu tiên#

Dependency:

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

Spring Boot tự bật @EnableAspectJAutoProxy khi starter này có trên classpath.

@Aspect
@Component
@Slf4j
public class PerformanceAspect {

    @Pointcut("execution(* com.shop.service..*(..))")
    public void serviceLayer() {}

    @Around("serviceLayer()")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();                 // gọi method thật
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} took {} ms", pjp.getSignature().toShortString(), ms);
        }
    }
}

Lưu ý: @Aspect không tự biến class thành bean — vẫn cần @Component (hoặc khai báo @Bean).


4. Năm loại Advice#

sequenceDiagram
    participant C as Caller
    participant P as Proxy
    participant T as Target

    C->>P: gọi method
    Note over P: @Around - phần trước proceed()
    Note over P: @Before
    P->>T: invoke method thật
    alt return bình thường
        T-->>P: kết quả
        Note over P: @AfterReturning
    else throw exception
        T-->>P: exception
        Note over P: @AfterThrowing
    end
    Note over P: @After - luôn chạy, như finally
    Note over P: @Around - phần sau proceed()
    P-->>C: kết quả / exception
Advice Chạy khi Thay đổi được kết quả? Use case
@Before Trước method Không (chỉ có thể throw để chặn) Validate, check quyền, log input
@AfterReturning Method return thành công Không đổi reference trả về Log kết quả, audit
@AfterThrowing Method throw exception Không nuốt được exception Log lỗi, dịch exception, alert
@After Luôn chạy (như finally) Không Dọn dẹp tài nguyên, MDC
@Around Bao quanh toàn bộ Có: đổi argument, đổi return, nuốt/dịch exception, không gọi method Transaction, retry, cache, đo thời gian
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logResult(JoinPoint jp, Object result) { ... }

@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logError(JoinPoint jp, Exception ex) { ... }

Nguyên tắc: dùng advice “yếu” nhất đủ cho nhu cầu. @Around mạnh nhưng dễ quên gọi proceed() hoặc quên return kết quả — method thật sẽ không chạy hoặc trả về null.

Thứ tự trong cùng một aspect (từ Spring 5.2.7): @Around → @Before → @After → @AfterReturning/@AfterThrowing theo thứ tự ưu tiên vào; chiều ra thì ngược lại.


5. Pointcut expression#

5.1. execution — phổ biến nhất#

execution( modifiers? return-type declaring-type? method-name(params) throws? )
Biểu thức Khớp với
execution(* com.shop.service.*.*(..)) Mọi method của class trong package service (không gồm sub-package)
execution(* com.shop.service..*(..)) Như trên, kể cả sub-package
execution(public * *(..)) Mọi method public
execution(* *..*Service.find*(..)) Method bắt đầu bằng find trong class tên kết thúc bằng Service
execution(* *(String, ..)) Method có tham số đầu là String

Ký hiệu: * là một phần tử bất kỳ, .. là “0 hoặc nhiều” (package hoặc parameter).

5.2. Các designator khác#

Designator Ý nghĩa
within(com.shop.service..*) Mọi method trong các type thuộc package
@annotation(com.shop.Audited) Method có annotation @Audited
@within(org.springframework.stereotype.Service) Method trong class có annotation @Service
bean(*Service) Bean có tên khớp pattern (riêng của Spring)
args(java.lang.Long, ..) Theo kiểu argument lúc runtime
this(...) / target(...) Theo kiểu của proxy / của target

Kết hợp bằng &&, ||, !:

@Pointcut("within(com.shop..*) && @annotation(com.shop.Audited)")
public void auditedMethods() {}

5.3. Pattern thực tế: custom annotation#

Cách sạch và rõ ràng nhất — method nào cần thì gắn annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
    String action();
}

@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {
    private final AuditRepository auditRepository;

    @AfterReturning("@annotation(audited)")   // bind annotation vào tham số
    public void audit(JoinPoint jp, Audited audited) {
        auditRepository.save(new AuditLog(audited.action(), currentUser(), Instant.now()));
    }
}

@Service
public class AccountService {
    @Audited(action = "WITHDRAW")
    public void withdraw(Long accountId, BigDecimal amount) { ... }
}

6. Bên dưới: Proxy hoạt động thế nào#

6.1. Proxy được tạo khi nào?#

Trong bean lifecycle, AnnotationAwareAspectJAutoProxyCreator (một BeanPostProcessor) kiểm tra ở bước postProcessAfterInitialization: nếu bean có method khớp pointcut nào đó, nó trả về proxy thay cho bean gốc. Mọi nơi inject bean này thực chất đều nhận proxy.

flowchart TB
    A[Tạo instance OrderService] --> B[Inject dependency]
    B --> C["@PostConstruct / init"]
    C --> D{"BeanPostProcessor: có pointcut khớp?"}
    D -- Không --> E[Đăng ký bean gốc]
    D -- Có --> F[Tạo proxy bọc bean gốc]
    F --> G[Đăng ký PROXY vào ApplicationContext]

6.2. JDK Dynamic Proxy vs CGLIB#

flowchart LR
    subgraph JDK["JDK Dynamic Proxy"]
        I[OrderService interface] --> P1["$Proxy42 implements OrderService"]
        I --> T1[OrderServiceImpl]
        P1 -. "ủy quyền" .-> T1
    end
    subgraph CG["CGLIB"]
        T2[OrderService class] --> P2["OrderService$$SpringCGLIB$$0 extends OrderService"]
        P2 -. "ủy quyền" .-> T2
    end
JDK Dynamic Proxy CGLIB
Cơ chế Implement interface của target Tạo subclass của target
Yêu cầu Target phải implement interface Class không final
Method áp dụng được Chỉ method khai báo trong interface Method không private, không final, không static
Inject theo class cụ thể ❌ Lỗi (proxy không phải instance của class đó) ✅ Được
Mặc định Spring Framework thuần (khi có interface) Spring Boot (spring.aop.proxy-target-class=true)

Lỗi kinh điển với JDK proxy: BeanNotOfRequiredTypeException khi inject OrderServiceImpl thay vì interface OrderService.

Một hệ quả thú vị của CGLIB: constructor của class có thể bị gọi thêm lần nữa khi tạo proxy (các version hiện đại dùng Objenesis để tránh điều này), nên đừng đặt logic có side effect trong constructor.


7. Giới hạn của Spring AOP — phần hay bị hỏi nhất#

7.1. Self-invocation#

@Service
public class ReportService {

    public void generateAll() {
        for (Long id : ids) {
            generate(id);          // this.generate(id) → KHÔNG qua proxy
        }
    }

    @Cacheable("reports")
    public Report generate(Long id) { ... }   // cache không bao giờ được dùng
}
sequenceDiagram
    participant C as Controller
    participant P as Proxy
    participant T as ReportService thật

    C->>P: generateAll()
    P->>T: generateAll()
    T->>T: this.generate(id)
    Note over T: Gọi thẳng trên object thật, proxy không biết gì

Proxy chỉ nằm “bên ngoài” object. Khi code bên trong gọi this.xxx(), this là object thật chứ không phải proxy.

Cách khắc phục (theo thứ tự khuyến nghị):

  1. Tách method sang một bean khác — sạch nhất, thường cũng là thiết kế tốt hơn.
  2. Self-injection: inject chính bean đó (qua @Lazy hoặc ObjectProvider) và gọi qua reference đó.
  3. AopContext.currentProxy() với @EnableAspectJAutoProxy(exposeProxy = true) — hoạt động nhưng làm code phụ thuộc vào AOP, nên tránh.
  4. Chuyển sang AspectJ weaving.

7.2. Các trường hợp khác AOP không hoạt động#

  • Method private hoặc static: không override được.
  • Method hoặc class final (với CGLIB). Chú ý với Kotlin: class mặc định là final, cần plugin kotlin-spring (all-open).
  • Object tự new, không do Spring quản lý.
  • Gọi method trong constructor hoặc @PostConstruct của chính bean đó — lúc đó proxy chưa tồn tại hoặc bạn đang ở trong object thật.
  • Aspect bắt chính aspect khác: aspect không được advise bởi aspect khác.

8. Thứ tự giữa nhiều aspect#

Khi nhiều aspect cùng áp dụng cho một method, dùng @Order (hoặc implement Ordered). Số nhỏ hơn = ưu tiên cao hơn = nằm ngoài cùng.

flowchart LR
    C[Caller] --> A1["Security Aspect @Order 1"]
    A1 --> A2["Transaction @Order 2"]
    A2 --> A3["Logging Aspect @Order 3"]
    A3 --> T[Target method]

Chiều vào: aspect có order nhỏ chạy @Before trước. Chiều ra: aspect có order nhỏ chạy @After sau cùng — giống các lớp của củ hành.

Ví dụ thực tế: nếu bạn viết aspect retry cho lỗi optimistic locking, aspect đó phải nằm ngoài transaction advice (order nhỏ hơn). Nếu nằm trong, việc retry diễn ra trong cùng một transaction đã hỏng và vô nghĩa. Transaction advisor mặc định có order Ordered.LOWEST_PRECEDENCE, có thể chỉnh qua @EnableTransactionManagement(order = ...).


9. Spring AOP vs AspectJ#

Spring AOP AspectJ
Weaving Runtime, bằng proxy Compile-time, post-compile, hoặc load-time (LTW)
Join point Chỉ method execution Method call/execution, constructor, field get/set, static initializer…
Self-invocation ❌ ✅
Private/final method ❌ ✅
Object ngoài Spring ❌ ✅
Setup Đơn giản, chỉ cần starter Cần compiler ajc hoặc java agent
Performance Có overhead nhỏ mỗi lời gọi qua proxy Gần như không overhead lúc chạy

Spring AOP dùng cú pháp annotation của AspectJ (@Aspect, @Around, pointcut expression), nhưng không dùng AspectJ weaver. Đây là điểm dễ nhầm.

Chọn Spring AOP cho 95% trường hợp. Chỉ cân nhắc AspectJ khi thực sự cần join point ngoài method hoặc cần advise object không do Spring quản lý.


10. Các annotation quen thuộc được xây dựng trên AOP#

Annotation Advice bên dưới Hệ quả
@Transactional TransactionInterceptor Self-invocation bỏ qua transaction
@Cacheable, @CacheEvict CacheInterceptor Self-invocation bỏ qua cache
@Async AsyncExecutionInterceptor Self-invocation chạy đồng bộ
@PreAuthorize, @Secured AuthorizationManagerBeforeMethodInterceptor Self-invocation bỏ qua kiểm tra quyền — lỗ hổng bảo mật
@Retryable (Spring Retry) RetryOperationsInterceptor Self-invocation không retry
@Validated trên class MethodValidationInterceptor Self-invocation không validate

Hiểu một cái là hiểu tất cả: cùng cơ chế proxy, cùng giới hạn.


11. Best practices#

  • Ưu tiên pointcut theo annotation (@annotation(...)) thay vì pattern tên method rộng — rõ ràng, người đọc code biết method nào bị ảnh hưởng.
  • Giữ aspect nhẹ: advice chạy trên mọi lời gọi khớp. Tránh I/O nặng hoặc logic phức tạp.
  • Pointcut càng hẹp càng tốt: execution(* *(..)) sẽ proxy gần như mọi bean, kể cả bean của thư viện.
  • Không đặt business logic trong aspect — aspect dành cho technical concern.
  • Với @Around: luôn gọi proceed() (trừ khi cố ý chặn), luôn return kết quả, không nuốt exception vô tình.
  • Test aspect: dùng @SpringBootTest hoặc tạo proxy thủ công với AspectJProxyFactory để unit test.

12. Câu hỏi phỏng vấn thường gặp#

  1. AOP là gì, giải quyết vấn đề gì? → Tách cross-cutting concern, tránh scattering và tangling.
  2. Spring AOP hoạt động thế nào? → BeanPostProcessor tạo proxy runtime; caller gọi proxy, proxy chạy chuỗi advice rồi ủy quyền cho target.
  3. JDK proxy khác CGLIB thế nào? Spring Boot dùng cái nào? → Interface vs subclass; Boot mặc định CGLIB.
  4. Tại sao @Transactional gọi từ method cùng class không có tác dụng? → Self-invocation không qua proxy.
  5. @Around khác @Before thế nào? → @Around kiểm soát toàn bộ việc gọi method, đổi được input/output và exception.
  6. Spring AOP khác AspectJ thế nào? → Proxy runtime, chỉ method execution vs weaving thật, mọi loại join point.
  7. Nhiều aspect cùng áp dụng thì chạy theo thứ tự nào? → @Order, số nhỏ nằm ngoài cùng.