AOP & proxy: thêm hành vi mà không sửa method

4 câu hỏi~25 phútTừ khóa: aspect, advice, pointcut, join point, @Around, JDK dynamic proxy, CGLIB, self-invocation

Tiếp đi, giờ tới AOP. Mình nghe @Transactional, @Async đều chạy bằng proxy?

Đúng vậy. Ở bài trước ta thấy class bean là PaymentService$$SpringCGLIB$$0. Bài này giải thích proxy đó là gì và vì sao nó là nền móng của @Transactional, @Async, @Cacheable, @PreAuthorize.

1. Bài toán: log thời gian chạy mọi method service

Team muốn biết createOrder, charge, sendOrderConfirmation chạy bao lâu. Cách đầu tiên:

public Order createOrder(CreateOrderRequest req) {
    long start = System.currentTimeMillis();
    try {
        Order order = orderRepository.save(Order.from(req));
        paymentService.charge(order);
        emailService.sendOrderConfirmation(order);
        return order;
    } finally {
        log.info("createOrder took {}ms", System.currentTimeMillis() - start);
    }
}

2. Vấn đề

Những việc "cắt ngang" nhiều class như vậy gọi là cross-cutting concern.

3. Giải pháp: AOP

Khái niệmNghĩaVí dụ
AspectModule chứa logic cross-cuttingTimingAspect
Join pointĐiểm có thể chen vào (Spring: lời gọi method)OrderService.createOrder()
PointcutBiểu thức chọn join pointwithin(com.example.shop..*Service)
AdviceCode chạy ở join point@Before, @After, @AfterReturning, @AfterThrowing, @Around
ProxyObject bọc bean thật, chạy advice rồi gọi beanOrderService$$SpringCGLIB$$0

4. Code từng bước

Bước 1: Dependency

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

Boot tự bật auto-proxy, không cần @EnableAspectJAutoProxy.

Bước 2: Viết Aspect

@Aspect
@Component
public class TimingAspect {

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

    @Around("within(com.example.shop..*) && @within(org.springframework.stereotype.Service)")
    public Object time(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);
        }
    }
}

Bước 3: Code business không đổi gì

@Service
public class OrderService {
    public Order createOrder(CreateOrderRequest req) {
        Order order = orderRepository.save(Order.from(req));
        paymentService.charge(order);
        emailService.sendOrderConfirmation(order);
        return order;
    }
}

Bước 4: Test

curl -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"productId":1,"quantity":2}'

# log:
PaymentService.charge(..) took 180ms
EmailService.sendOrderConfirmation(..) took 40ms
OrderService.createOrder(..) took 235ms

5. Cách hoạt động

OrderController
     |  orderService.createOrder(req)
     v
┌───────────────────────────────┐
│ OrderService PROXY (CGLIB)    │
│   TimingAspect: start timer   │
│   pjp.proceed() ──────────────┼──→ OrderService THẬT.createOrder()
│   TimingAspect: log time      │
└───────────────────────────────┘
     |
     v
  return order

Controller được inject proxy, không phải object thật. Proxy được tạo ở bước postProcessAfterInitialization (bài 4.1.2).

JDK dynamic proxy và CGLIB

JDK proxy:   class $Proxy42 implements PaymentGateway { ... }
             → chỉ proxy được method của INTERFACE
             → inject theo class VnpayGateway sẽ lỗi

CGLIB:       class OrderService$$SpringCGLIB$$0 extends OrderService { ... }
             → subclass, override method
             → không proxy được method final / private / static, class final

6. So sánh

JDK dynamic proxyCGLIB
Cơ chếImplement interface (java.lang.reflect.Proxy)Tạo subclass bằng bytecode
Yêu cầuBean phải có interfaceClass không final
Không chặn đượcMethod không có trong interfaceMethod final, private, static
Spring Boot mặc địnhCGLIB (spring.aop.proxy-target-class=true)

7. Khi nào nên dùng / không nên dùng

Tôi có @Transactional trên method saveOrder() và gọi nó từ createOrder() trong cùng class, sao không rollback?

Đây là bẫy hay hỏi nhất: self-invocation. Gọi this.saveOrder() là gọi thẳng object thật, không qua proxy, nên advice transaction không chạy.

@Service
public class OrderService {

    public void createOrder(CreateOrderRequest req) {
        saveOrder(req);          // = this.saveOrder(req) → KHÔNG qua proxy
    }

    @Transactional
    public void saveOrder(CreateOrderRequest req) {
        orderRepository.save(Order.from(req));
        inventoryRepository.decrease(req.productId(), req.quantity());
        throw new RuntimeException("boom");   // không rollback!
    }
}
Controller ──→ PROXY.createOrder()      (createOrder không có @Transactional)
                  │
                  v
               THẬT.createOrder()
                  │ this.saveOrder()   ← "this" là object thật
                  v
               THẬT.saveOrder()        → không có transaction

@Async cũng y hệt: gọi trong cùng class thì chạy đồng bộ trên thread hiện tại. Cách sửa:

  1. Tách sang bean khác (khuyên dùng): OrderWriter.saveOrder() được inject vào OrderService.
  2. Đặt @Transactional lên method được gọi từ ngoài (createOrder).
  3. Self-inject: @Lazy inject chính OrderService rồi gọi self.saveOrder(). Chạy được nhưng khó đọc.
  4. Dùng AspectJ weaving (compile/load-time) thay vì proxy. Ít dùng.

Thêm hai bẫy cùng gốc: method private hoặc final có @Transactional cũng bị bỏ qua vì CGLIB không override được.

Vậy AOP của Spring khác AspectJ chỗ nào?

Bạn hiểu gần đúng là chúng liên quan, chỉ cần chỉnh: Spring AOP mượn cú pháp của AspectJ (@Aspect, @Around, pointcut expression) nhưng chạy bằng proxy lúc runtime.

Spring AOPAspectJ
Cơ chếProxy runtimeSửa bytecode (compile-time / load-time weaving)
Join pointChỉ method public của beanMethod, constructor, field, self-call, private
Đối tượngChỉ Spring beanMọi object
Cấu hìnhĐơn giảnCần agent/plugin compiler

Hầu hết project chỉ cần Spring AOP.

Phỏng vấn hỏi AOP và proxy thì trả lời sao?

Q1. Spring AOP hoạt động thế nào?

🇻🇳 Tiếng Việt:

Spring tạo proxy bọc quanh bean khi bean khớp pointcut. Caller được inject proxy. Khi gọi method, proxy chạy advice (before, around, after) rồi gọi method thật. Spring Boot mặc định dùng CGLIB tạo subclass; nếu cấu hình dùng interface thì dùng JDK dynamic proxy.

🇬🇧 English:

Spring wraps a bean in a proxy when it matches a pointcut, and callers receive the proxy. On each call, the proxy runs the advice, such as before, around or after, and then delegates to the real method. Spring Boot uses CGLIB subclass proxies by default; JDK dynamic proxies are used for interface-based proxying.

Q2. Vì sao @Transactional không chạy khi gọi method trong cùng class?

🇻🇳 Tiếng Việt:

Vì transaction được áp dụng bởi proxy. Gọi this.method() là gọi trực tiếp object thật, không qua proxy, nên không có interceptor nào mở transaction. Cách sửa là tách method sang bean khác, hoặc đặt annotation lên method được gọi từ ngoài. @Async và @Cacheable cũng bị y hệt.

🇬🇧 English:

Because transactions are applied by the proxy. Calling this.method() goes straight to the target object and bypasses the proxy, so no interceptor starts a transaction. The fix is to move the method into another bean or put the annotation on the externally called method. @Async and @Cacheable have the same limitation.

Q3. JDK proxy và CGLIB khác nhau thế nào?

🇻🇳 Tiếng Việt:

JDK proxy implement interface nên chỉ chặn method của interface. CGLIB tạo subclass nên proxy được class không có interface, nhưng không chặn được method final, private, static và không dùng được với class final.

🇬🇧 English:

A JDK proxy implements the bean's interfaces, so it only intercepts interface methods. CGLIB generates a subclass, so it works without interfaces, but it cannot intercept final, private or static methods and cannot proxy final classes.

Cách nhớ ngắn gọn


Tự kiểm tra

pjp.proceed() làm gì? Quên gọi thì sao?

Gọi method thật (hoặc advice kế tiếp). Quên gọi thì method thật không chạy và caller nhận null.

Method private có @Transactional có tác dụng không?

Không. Proxy không chặn được method private.

Bean chỉ có class, không interface. Spring dùng proxy gì?

CGLIB (và Spring Boot mặc định luôn dùng CGLIB).

Sửa self-invocation cách nào sạch nhất?

Tách method cần proxy sang một bean khác và inject vào.

Spring AOP chặn được việc set field không?

Không. Spring AOP chỉ có join point là method call trên bean. Cần AspectJ weaving.