Backend

99 Ngày Spring — Ngày 61: AOP

SSite Admin
26 tháng 09, 2026 7 phút đọc 0 lượt xem
99 Ngày Spring — Ngày 61: AOP

Nếu mỗi service đều có cùng đoạn ghi nhật ký hoặc đo thời gian, sửa một quy tắc sẽ phải sửa nhiều nơi. AOP, lập trình hướng khía cạnh, giúp tách những mối quan tâm xuyên suốt này khỏi nghiệp vụ. Nhưng AOP của Spring dựa trên proxy: hiểu đường đi của lời gọi quan trọng hơn việc nhớ tên annotation. Bài này dùng Spring Boot 3.x, Spring Framework 6 và Java 21.

Spring AOP: aspect, pointcut, advice, đo thời gian và giới hạn proxy

1. Aspect, pointcut, advice là gì?

  • Cross-cutting concern: xử lý trải qua nhiều phần của ứng dụng, ví dụ quan sát thời gian thực thi. Không nên giấu toàn bộ nghiệp vụ quan trọng vào aspect khiến luồng xử lý khó theo dõi.

  • Aspect: lớp gom phần xử lý dùng chung và quy tắc áp dụng.

  • Join point: điểm có thể chèn xử lý. Với Spring AOP, đó là thực thi phương thức của Spring bean.

  • Pointcut: biểu thức chọn các join point cần áp dụng, chẳng hạn các public method của OrderService.

  • Advice: hành động chạy tại các điểm đã chọn; có thể trước, sau hoặc bao quanh lời gọi.

  • Proxy: đối tượng đại diện đứng trước target. Lời gọi qua proxy được áp dụng advice rồi mới chuyển tới nghiệp vụ, tùy loại advice.

2. Dựng ví dụ nhỏ với Spring Boot

Trong project Boot 3.x hiện có, dùng Java 21 và thêm starter dưới đây; để dependency management của Boot chọn phiên bản, không tự trộn phiên bản AspectJ. Đặt các class trong package vn.motdev.day61 để component scan nhìn thấy. Boot tự cấu hình AOP khi các điều kiện và dependency được đáp ứng; không cần thêm @EnableAspectJAutoProxy chỉ để lặp lại cấu hình đó.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package vn.motdev.day61;

import org.springframework.stereotype.Service;

@Service
public class OrderService {
    public String place(String code) {
        if (code == null || code.isBlank()) {
            throw new IllegalArgumentException("code must not be blank");
        }
        return "accepted:" + code;
    }
}

@Aspect mô tả một aspect; @Component khiến class được phát hiện thành Spring bean. Chỉ gắn @Aspect lên một đối tượng không được đăng ký không khiến nó tự hoạt động. Pointcut dưới chọn public method của đúng OrderService; dấu * đầu tiên khớp kiểu trả về, dấu * sau tên lớp khớp tên phương thức, còn (..) khớp danh sách tham số bất kỳ.

package vn.motdev.day61;

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 TimingAspect {
    private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);

    @Around("execution(public * vn.motdev.day61.OrderService.*(..))")
    public Object time(ProceedingJoinPoint call) throws Throwable {
        long begin = System.nanoTime();
        try {
            return call.proceed(); // exactly once for this timing advice
        } finally {
            log.info("method={} elapsedNs={}",
                call.getSignature().toShortString(), System.nanoTime() - begin);
        }
    }
}

Dùng runner sau để gọi OrderService qua bean được tiêm từ context. Một lời gọi thành công trả accepted:A61, lời gọi còn lại vẫn ném IllegalArgumentException. Cả hai đường đều có log thời gian vì phần ghi nhận ở finally. Chạy bằng ./mvnw spring-boot:run, hoặc mvn spring-boot:run nếu project không có wrapper.

package vn.motdev.day61;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Day61Application {
    public static void main(String[] args) {
        SpringApplication.run(Day61Application.class, args);
    }

    @Bean
    CommandLineRunner demo(OrderService orders) {
        return args -> {
            System.out.println(orders.place("A61"));
            try {
                orders.place("");
            } catch (IllegalArgumentException expected) {
                System.out.println(expected.getMessage());
            }
        };
    }
}

3. Chọn advice vừa đủ

  • @Before: trước phương thức; phù hợp cho thao tác trước lời gọi. Nó vẫn có thể ném lỗi và ngăn nghiệp vụ tiếp tục.

  • @AfterReturning: chỉ sau khi phương thức trả về bình thường; có thể nhận giá trị trả về để quan sát.

  • @AfterThrowing: khi phương thức ném ngoại lệ phù hợp; không tương đương bắt lỗi và tự phục hồi.

  • @After: sau khi lời gọi kết thúc, theo ngữ nghĩa finally, cả khi thành công lẫn thất bại.

  • @Around: bao quanh lời gọi, có thể kiểm soát việc gọi tiếp qua ProceedingJoinPoint.proceed(). Dùng khi cần thực sự bao quanh, như đo khoảng thời gian.

API Around cho phép gọi proceed không lần nào hoặc nhiều lần; nhưng với ví dụ timing, hợp đồng của ta là gọi đúng một lần, trả nguyên kết quả và để lỗi truyền tiếp. Quên proceed sẽ bỏ qua nghiệp vụ. Gọi hai lần có thể tạo hai đơn hàng. Catch mọi lỗi rồi trả null làm thay đổi hành vi của service. System.nanoTime phù hợp đo khoảng thời gian, không dùng giá trị đó làm thời điểm lịch.

4. Bẫy self-invocation: gọi trong cùng đối tượng

Khi caller gọi bean qua proxy, advice có cơ hội chạy. Nhưng sau khi đã vào target, this.inner() là lời gọi trực tiếp trên chính đối tượng đó, không đi ra proxy lần nữa. Dù inner khớp pointcut, advice cho lời gọi nội bộ ấy không tự chạy. Đây cũng là gốc của nhiều bất ngờ với những tính năng khác dựa trên Spring proxy.

@Service
class CheckoutService {
    public void checkout() {
        this.charge(); // internal call: no second trip through the proxy
    }
    public void charge() { /* ... */ }
}

// Clearer boundary: another Spring bean, injected via constructor.
@Service
class CheckoutFacade {
    private final PaymentService payments;
    CheckoutFacade(PaymentService payments) { this.payments = payments; }
    public void checkout() { payments.charge(); }
}
@Service
class PaymentService {
    public void charge() { /* ... */ }
}
// Adapt the pointcut to PaymentService when testing this example.
// TimingAspect above deliberately only selects OrderService.

Cách sửa thường rõ ràng nhất là tách trách nhiệm sang bean khác và tiêm nó vào, để lời gọi thực sự đi qua proxy. Đừng sửa bằng cách tự new PaymentService, vì như vậy lại bỏ Spring container. Ví dụ trên minh họa cấu trúc; muốn đo PaymentService phải đổi pointcut tương ứng, vì aspect timing ban đầu chủ ý chỉ chọn OrderService.

5. Giới hạn của proxy và cách kiểm tra

  • Spring Framework có JDK dynamic proxy dựa trên interface và proxy CGLIB dựa trên kế thừa. Spring Boot thường mặc định dùng class-based proxy; kiểm tra spring.aop.proxy-target-class nếu muốn thay đổi.

  • Với CGLIB, final class không thể được kế thừa để tạo proxy; final method không thể được ghi đè; private method không thể được advice theo cơ chế đó. Không đánh đồng giới hạn này với mọi lời gọi interface qua JDK proxy.

  • AOP của Spring không phải toàn bộ AspectJ weaving. Gắn annotation không tự mở khả năng intercept constructor, truy cập field hoặc self-invocation.

  • Dùng pointcut hẹp và có chủ đích. Không log password, token hay toàn bộ arguments chỉ vì aspect nhìn thấy chúng.

  • Test phải gọi bean lấy từ Spring context. Test trực tiếp new TimingAspect() chỉ chứng minh logic của method advice, không chứng minh pointcut hoặc proxy được nối đúng.

6. Bài tập thực hành

  • Chạy ví dụ với mã đơn hợp lệ và rỗng: xác nhận kết quả và loại lỗi vẫn giữ nguyên, mỗi lời gọi có một bản ghi thời gian.

  • Tạo phương thức outer gọi inner trong cùng bean, chỉ chọn inner bằng pointcut. Vì sao outer được gọi từ context mà inner vẫn không được advice?

  • Tách inner sang bean khác, điều chỉnh pointcut và chạy lại. Không dùng self-injection để che mất ranh giới trách nhiệm.

  • Đổi timing advice thành @Before: bạn đo được toàn bộ thời gian thực thi chỉ bằng advice đó không? Giải thích.

AOP giúp xử lý dùng chung gọn hơn khi chọn đúng điểm và giữ hợp đồng nghiệp vụ. Khi advice không chạy, kiểm tra bean có thuộc container không, lời gọi có qua proxy không, pointcut có khớp không, rồi mới xét annotation. Ngày 62 ta sẽ tạo annotation riêng để đánh dấu phương thức cần đo thời gian.

Tài liệu đối chiếu

Spring Framework 6.2 AOP: https://docs.spring.io/spring-framework/reference/6.2/core/aop/proxying.html ; advice: https://docs.spring.io/spring-framework/reference/6.2/core/aop/ataspectj/advice.html ; Spring Boot AOP: https://docs.spring.io/spring-boot/3.5/reference/features/aop.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 61: Thread cơ bản

Thread và Runnable, vòng đời, start khác run, sleep khác join và interrupt để dừng hợp tác.

26 thg 9, 20266 phút0
99 Ngày Spring — Ngày 60: Tổng kết chiến lược test

Một chiến lược test cho service thật: mỗi lớp một câu hỏi và tỉ lệ 300 unit, 40 slice, 8 hành trình; fake có hành vi và WireMock ở biên giới; Surefire/Failsafe, Awaitility, Clock và chính sách test chập chờn — cùng checklist 12 câu để review test.

25 thg 9, 202611 phút7
99 Ngày Java — Ngày 60: Stream thực chiến

groupingBy hai cấp với TreeMap, flatMap xuống dòng hàng, top N bằng collectingAndThen, teeing cho min và max, toMap có merge giữ thứ tự — rồi khi nào đặt tên collector, dùng record thay tuple hay quay về vòng for, và một báo cáo từ CSV khép lại Giai đoạn 6.

25 thg 9, 202611 phút7