+1

Bí mật của "Thư ký" Proxy: Làm chủ Spring AOP và tránh bẫy gọi hàm nội bộ (Phần 1)

Bạn gõ @Transactional lên đầu một hàm, nhấn Run, và mỉm cười tự tin rằng dữ liệu của mình đã được an toàn. Gõ thêm @Async, và bạn tin chắc tác vụ nặng nề kia đã được tự động chuyển sang một luồng khác để chạy ngầm.

Trong thế giới của Spring Boot, các annotation thường mang lại cảm giác giống như những "câu thần chú" quyền năng. Chỉ với một dòng code ngắn gọn, framework sẽ tự động lo liệu hàng tá logic phức tạp bên dưới mà chúng ta chẳng cần động tay. Thế nhưng... sự tự tin đó đôi khi lại vỡ vụn khi hệ thống gặp sự cố, dữ liệu không hề được rollback như mong đợi, hoặc API bỗng dưng chậm rì vì tác vụ ngầm lại chạy rành rành trên luồng chính.

Câu thần chú rõ ràng đã được viết đúng từng chữ, vậy tại sao "phép thuật" lại bốc hơi?

Sự thật là, Spring Boot không hề có phép thuật. Đằng sau sự tiện lợi đến kỳ diệu ấy là một cơ sở hạ tầng được vận hành vô cùng chặt chẽ nhưng cũng ẩn chứa những cạm bẫy vô hình. Nếu chỉ dùng annotation như một thói quen học thuộc lòng mà không hiểu rõ bản chất bên dưới, bạn sẽ rất dễ tự đưa mình vào "điểm mù" của hệ thống.

Bài viết này sẽ đưa bạn ra phía sau bức màn sân khấu của Spring Boot để bóc tách một kỹ thuật kinh điển: Proxy Design Pattern. Chúng ta sẽ cùng nhau giải phẫu cách Spring AOP thực sự hoạt động, và quan trọng nhất, làm thế nào để né tránh triệt để 'Self-invocation' (gọi hàm nội bộ) – một cái bẫy khét tiếng đã từng đánh lừa vô số lập trình viên.

I. "Phép thuật" đằng sau các Annotation

Trong đời thực, "proxy" có nghĩa là người đại diện hoặc người được ủy quyền. Trong lập trình, Proxy Design Pattern cũng mang ý nghĩa tương tự: nó là một "kẻ đóng thế" hoặc "người trung gian" 🕴️ đứng ra nhận các yêu cầu thay cho một đối tượng thực sự.

Dưới đây là bức tranh tổng quan về cách Proxy hoạt động:

  • Vai trò cốt lõi: Khi một client muốn gọi một phương thức của đối tượng A, nó sẽ không gọi trực tiếp A. Thay vào đó, nó gọi đối tượng Proxy_A.

  • Chức năng: Proxy_A sẽ nhận yêu cầu, có thể làm thêm một số việc phụ (như kiểm tra quyền truy cập, ghi log, mở kết nối database) ở trước hoặc sau khi xử lý, rồi mới chuyển tiếp yêu cầu đó đến đối tượng A thực sự.

  • Trong Spring Boot 🪄: Đây chính là "phép thuật" ẩn sau các annotation cực kỳ phổ biến như @Transactional, @Cacheable, hay @Async. Khi bạn gắn @Transactional lên một service, Spring không đưa cho controller service gốc của bạn. Nó tạo ra một Proxy bọc bên ngoài service đó. Khi controller gọi service, Proxy sẽ chặn (intercept) cuộc gọi, tự động mở một transaction với database, gọi hàm của bạn, rồi tự động commit hoặc rollback dựa trên kết quả.

II. Giải phẫu Proxy: JDK vs CGLIB

Rồi, đã có cái nhìn tổng quan rồi, chúng ta hãy cùng đến với Trạm dừng chân đầu tiên Cơ chế hoạt động bên dưới ⚙️.

Trong Spring Boot, các Proxy không được tạo sẵn từ trước mà được tạo ra một cách tự động khi ứng dụng đang chạy (gọi là Dynamic Proxy). Spring sử dụng hai "công cụ" chính để làm việc này:

1. JDK Dynamic Proxy (Dựa trên Interface)

  • Cách thức: Nó hoạt động bằng cách tạo ra một Proxy class triển khai (implements) cùng một Interface với đối tượng gốc. Khi bạn gọi hàm qua Interface, Proxy sẽ nhận lệnh.

  • Điểm lưu ý: Nó là "đồ nhà trồng" có sẵn trong Java. Tuy nhiên, nó bắt buộc đối tượng gốc của bạn phải implements ít nhất một Interface. Nếu không có Interface, JDK Proxy sẽ "bó tay".

2. CGLIB Proxy (Dựa trên Class - Subclassing)

  • Cách thức: Vậy nếu class của bạn không có Interface nào thì sao? Spring sẽ dùng thư viện CGLIB. CGLIB (Code Generation Library) sẽ tạo ra một lớp con (subclass) kế thừa trực tiếp từ class gốc của bạn. Nó sẽ ghi đè (override) lại các phương thức để chèn thêm logic của proxy vào.

  • Điểm lưu ý: Vì nó dùng tính chất kế thừa, nên nó không thể hoạt động với các class bị đánh dấu là final hoặc các phương thức final, private (vì trong Java, bạn không thể override chúng).

(Lưu ý nhỏ: Kể từ Spring Boot 2.0, CGLIB đã được chọn làm mặc định do tính linh hoạt của nó, nhưng Spring vẫn có thể chuyển đổi qua lại giữa hai loại này).

Để mình kiểm tra xem bạn đã nắm rõ sự khác biệt cốt lõi này chưa nhé:

Giả sử bạn có một class OrderService được đánh dấu là final và nó không implements bất kỳ interface nào. Theo bạn, Spring có thể tạo Proxy cho class này để sử dụng annotation @Transactional được không? Dựa vào kiến thức bên trên, vì sao bạn lại chọn đáp án đó?

Câu trả lời trong trường hợp này là: Spring sẽ báo lỗi khi khởi chạy ứng dụng nếu nó bắt buộc phải tạo Proxy để gắn các tính năng như Transaction do final chặn tính kế thừa của CGLIB, và việc thiếu Interface chặn JDK Proxy

III. Cạm bẫy thực tế: Self-invocation (Gọi hàm nội bộ)

Bây giờ, chúng ta sẽ bước sang trạm thứ hai: "Cạm bẫy" với Spring Proxy ⚠️. Lỗi phổ biến nhất khi làm việc với Proxy trong Spring được gọi là Self-invocation (Gọi hàm nội bộ).

1. Self-invocation (Gọi hàm nội bộ) là gì?

Hãy tưởng tượng Proxy giống như một người thư ký 👩‍💼 ngồi ở cửa phòng giám đốc.

  • Khi người ngoài (ví dụ: Controller) yêu cầu làm việc, họ phải đi qua thư ký. Thư ký sẽ làm các việc phụ (mở Transaction, kiểm tra quyền, ghi log) rồi mới chuyển lời cho giám đốc (đối tượng gốc).

  • Tuy nhiên, nếu giám đốc đang ở trong phòng và tự làm một việc gì đó rồi chuyển sang làm việc khác (một hàm trong class gọi một hàm khác trong cùng class bằng từ khóa this), người thư ký ở ngoài cửa sẽ không hề hay biết.

Về mặt code, khi bạn gọi this.methodB() từ methodA(), luồng thực thi đi trực tiếp bên trong đối tượng gốc, hoàn toàn bỏ qua lớp vỏ Proxy bọc bên ngoài. Do đó, mọi "phép thuật" của Spring (như @Transactional, @Async) gắn trên methodB sẽ bị vô hiệu hóa.

2. Cạm bẫy thực tế

Để xem "cạm bẫy" này hoạt động thế nào trong thực tế, hãy xét tình huống sau: Bạn có một OrderService với hai hàm:

  1. Hàm createOrder() : Không có @Transactional. Hàm này thực hiện các tính toán logic, sau đó gọi trực tiếp hàm saveToDb() bên dưới.

  2. Hàm saveToDb() : Có đánh dấu @Transactional để đảm bảo an toàn khi lưu vào database.

Nếu một Controller từ bên ngoài gọi vào hàm createOrder() của service này, theo bạn, quá trình lưu dữ liệu có được đặt trong một Transaction hay không?

Có thể nói đây là một cạm bẫy rất lắt léo. Câu trả lời chính xác cho bài toán này đó là: KHÔNG, quá trình lưu dữ liệu sẽ không được đặt trong một Transaction.

Chúng ta cùng áp dụng ví dụ "thư ký" (Proxy) và "giám đốc" (Service thực sự) để xem luồng đi của đoạn code nhé:

  1. Controller gọi createOrder(): Controller yêu cầu thư ký (Proxy). Thư ký kiểm tra hàm createOrder() và thấy không có @Transactional. Thư ký nghĩ: "Việc này không cần thủ tục Transaction", rồi chuyển lệnh vào cho giám đốc.

  2. Giám đốc thực thi createOrder(): Giám đốc ngồi trong phòng kín và bắt đầu xử lý logic.

  3. Giám đốc tự gọi saveToDb(): Trong lúc xử lý, giám đốc tự gọi hàm this.saveToDb() ở ngay bên trong cùng một class.

  4. Thư ký bị bỏ qua: Lúc này, giám đốc tự chuyển việc nội bộ với nhau. Người thư ký đứng ngoài cửa hoàn toàn không biết hàm saveToDb() vừa được gọi.

  5. Hậu quả: Vì thư ký (Proxy) không bắt được cuộc gọi này, nên dù saveToDb() có gắn @Transactional, nó cũng vô tác dụng. Spring không thể tự động mở Transaction được.

Đây chính là hiện tượng Self-invocation mà các dự án thực tế rất hay mắc phải. Lớp vỏ Proxy chỉ có tác dụng khi có một cuộc gọi từ bên ngoài (như Controller) đi qua nó.

Bây giờ chúng ta đã biết nguyên nhân gốc rễ là do Proxy không "thấy" được cuộc gọi nội bộ. Theo bạn, nếu vẫn giữ cấu trúc hai hàm này, chúng ta có thể sửa đổi cách đặt annotation như thế nào để thư ký (Proxy) biết đường mở Transaction ngay từ đầu?

Cách giải quyết vấn đề này một cách hiệu quả nhất đó chính là di dời @Transactional lên hàm createOrder()

Luồng đi bây giờ sẽ thay đổi như sau:

  1. Controller gọi createOrder().

  2. Thư ký (Proxy) nhận lệnh, kiểm tra thấy có @Transactional nên ngay lập tức mở một Transaction 🗄️.

  3. Thư ký chuyển lệnh vào cho giám đốc (Service gốc) thực thi createOrder().

Bên trong createOrder(), giám đốc có tự gọi saveToDb() đi chăng nữa thì Transaction cũng đã được mở sẵn từ trước rồi. Dữ liệu sẽ được lưu an toàn!

(Một mẹo nhỏ thêm: Nếu bạn bắt buộc phải giữ @TransactionalsaveToDb(), cách giải quyết thứ hai là tách saveToDb() sang một class Service khác. Khi đó, createOrder() sẽ gọi sang Service mới này, tức là phải đi qua một lớp Proxy khác, và Transaction lại hoạt động bình thường).

Vậy là chúng ta đã nắm được "cạm bẫy" gọi hàm nội bộ (Self-invocation). Bây giờ, hãy cùng mình bước sang trạm dừng chân tiếp theo, trạm này chúng ta sẽ cùng vận động một chút, khởi động kĩ trước khi bắt đầu đấy nhé: Tự tay viết một Proxy 💻 bằng Java thuần (sử dụng JDK Dynamic Proxy) mà không nhờ đến Spring.

IV. Custom Proxy theo cách riêng

Để bắt đầu, chúng ta cần một kịch bản đơn giản. Giả sử chúng ta có một Interface MayTinh với một hàm cong(int a, int b) và một class MayTinhThucSu thực thi Interface đó.

Khi tạo ra một Proxy bọc bên ngoài MayTinhThucSu, chúng ta sẽ chọn làm một hành động kinh điển nhất: in ra log (ghi chú) 📝 nhé. Đây cũng chính là cách mà nhiều hệ thống ghi lại lịch sử hoạt động mà không làm bẩn code logic chính yếu.

Vậy thì chúng ta sẽ chọn làm một hành động kinh điển nhất: in ra log (ghi chú) 📝 nhé. Đây chính là cách mà các hệ thống ghi lại lịch sử hoạt động mà không làm bẩn code logic chính yếu.

1. JDK Dynamic Proxy Custom

Trong Java thuần, để làm được một JDK Dynamic Proxy, chúng ta sẽ dùng một interface có sẵn tên là InvocationHandler. Bạn cứ coi nó như một "bản mô tả công việc của người thư ký".

Dưới đây là 3 bước cực kỳ đơn giản để tạo ra phép thuật này:

Bước 1: Chuẩn bị Interface và Đối tượng thực sự (Giám đốc)

// Interface
public interface MayTinh {
    int cong(int a, int b);
}

// Đối tượng thực sự
public class MayTinhThucSu implements MayTinh {
    @Override
    public int cong(int a, int b) {
        System.out.println("⚙️ Giám đốc đang cặm cụi tính toán...");
        return a + b;
    }
}

Bước 2: Viết "Bản mô tả công việc" cho Thư ký (InvocationHandler)

Lớp này sẽ quyết định thư ký làm gì trước và sau khi giám đốc làm việc.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ThuKyProxy implements InvocationHandler {
    private final Object giamDoc; // Thư ký cần biết mình đang phục vụ ai

    public ThuKyProxy(Object giamDoc) {
        this.giamDoc = giamDoc;
    }

    // Hàm này tự động chạy mỗi khi có ai đó gọi bất kỳ hàm nào qua Proxy
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. Hành động phụ TRƯỚC KHI gọi hàm gốc
        System.out.println("📝 [Log thư ký]: Nhận yêu cầu cộng 2 số " + args[0] + " và " + args[1]);

        // 2. Chuyển lệnh cho "giám đốc" làm việc (gọi hàm gốc)
        Object ketQua = method.invoke(giamDoc, args);

        // 3. Hành động phụ SAU KHI gọi hàm gốc
        System.out.println("📝 [Log thư ký]: Sếp đã tính xong, kết quả trả về là: " + ketQua);

        return ketQua;
    }
}

Bước 3: Chạy thử và xem phép thuật

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        MayTinh giamDoc = new MayTinhThucSu();

        // Spring tự động làm đoạn code dài dòng này ở dưới background để tạo ra Proxy
        MayTinh thuKy = (MayTinh) Proxy.newProxyInstance(
                giamDoc.getClass().getClassLoader(),
                giamDoc.getClass().getInterfaces(), // <-- LƯU Ý DÒNG NÀY
                new ThuKyProxy(giamDoc)
        );

        // Khách hàng nhờ thư ký tính toán
        thuKy.cong(5, 10);
    }
}

Phân tích đoạn code "dài ngoằng" 🔍

Hàm Proxy.newProxyInstance(...) giống như việc bạn đi đến "Trung tâm nhân sự của Java" để yêu cầu tạo ra một cô Thư ký (Proxy). Để trung tâm làm được việc này, bạn phải cung cấp đủ 3 thông tin:

  • giamDoc.getClass().getClassLoader() (Người quản lý nhân sự): Java cần một ClassLoader để chịu trách nhiệm "tạo hình" và quản lý lớp Proxy mới này trong bộ nhớ. Thường chúng ta lấy luôn quản lý của Giám đốc để quản lý Thư ký cho đồng bộ.

  • giamDoc.getClass().getInterfaces() (Danh sách kỹ năng): Thư ký phải có năng lực giao tiếp giống hệt Giám đốc thì khách hàng mới làm việc được. Do đó, chúng ta phải đưa danh sách các Interface (ở đây là MayTinh) để Java "dạy" cho Thư ký biết hàm cong() là gì.

  • new ThuKyProxy(giamDoc) (Sổ tay hành động): Đây chính là lớp InvocationHandler chúng ta viết ở Bước 2. Nó là một cuốn sổ tay hướng dẫn chi tiết: "Khi khách yêu cầu tính toán, cô phải in log này ra trước, sau đó đưa số liệu cho ông Giám đốc tôi truyền vào đây, ổng tính xong thì cô in cái log này ra sau cùng nhé".

Luồng đi của bài toán (Execution Flow) 🔄

Khi đoạn code trên chạy xong, chúng ta có một đối tượng thuKy giả danh hoàn hảo. Luồng thực thi diễn ra như sau:

  1. Khách hàng gọi: Hàm thuKy.cong(5, 10) được gọi.

  2. Chặn (Intercept): Lời gọi này không đi đến lớp MayTinhThucSu. Thay vào đó, Java tự động chuyển nó vào hàm invoke(...) nằm trong cuốn "Sổ tay hành động" (ThuKyProxy).

  3. Hành động trước: Lệnh System.out.println đầu tiên trong hàm invoke chạy, in ra log nhận yêu cầu.

  4. Gọi đối tượng thực: Lệnh method.invoke(giamDoc, args) được thực thi. Lúc này, Giám đốc (MayTinhThucSu) mới thực sự làm phép tính 5 + 10 và trả về 15.

  5. Hành động sau: Lệnh System.out.println thứ hai chạy, in ra kết quả.

  6. Trả kết quả: Hàm invoke hoàn thành và trả số 15 về lại cho khách hàng.

Bạn thấy đấy, mọi thứ được liên kết với nhau rất chặt chẽ!

Kết quả in ra màn hình sẽ là:

📝 [Log thư ký]: Nhận yêu cầu cộng 2 số 5 và 10
⚙️  Giám đốc đang cặm cụi tính toán...
📝 [Log thư ký]: Sếp đã tính xong, kết quả trả về là: 15

Thật tuyệt phải không? Code hàm cong của giám đốc hoàn toàn sạch sẽ, không có dòng log nào, nhưng chúng ta vẫn có log đầy đủ nhờ Proxy bọc bên ngoài.

Nhưng các bạn thấy việc tạo một Proxy có loằng ngoằng không? Cũng khó là lằng nhằng khó nhớ đúng không. Nhưng đó lại chính là điều mà chúng ta phải làm khi code Java thuần. Và đây cũng là lý do các framework như Spring Boot ra đời: nó tự động hóa toàn bộ quá trình viết code rườm rà này và thu gọn lại chỉ bằng một annotation ngắn gọn như @Transactional...

2. CGLIB Custom

CGLIB (Code Generation Library) đúng là một thư viện mã nguồn mở bên ngoài và không nằm trong bộ công cụ chuẩn của Java (JDK). Tuy nhiên, khi bạn sử dụng Spring Boot, Spring đã tự động "nhúng" lõi của CGLIB vào bên trong thư viện spring-core của họ. Do đó, bạn có thể sử dụng nó luôn mà không cần tải thêm.

Chúng ta cũng hoàn toàn có thể tự tay tạo một Proxy bằng CGLIB giống như cách đã làm với JDK. Điểm khác biệt lớn nhất là thay vì dùng InvocationHandler và ép lớp gốc phải có Interface, CGLIB sẽ dùng một công cụ tên là Enhancer để tạo ra một lớp con (subclass) kế thừa trực tiếp từ lớp gốc của bạn.

Dưới đây là cách chúng ta tạo lại ví dụ Thư ký bằng CGLIB:

1. Đối tượng gốc (Không cần Interface)

public class GiamDocIT {
    public void code() {
        System.out.println("💻 Giám đốc đang gõ code...");
    }
}

2. "Sổ tay hành động" cho Thư ký (Dùng MethodInterceptor)

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class ThuKyCGLIB implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("📝 [Log CGLIB]: Nhận yêu cầu chạy hàm " + method.getName());
        
        // Gọi hàm gốc (hàm của lớp cha)
        Object ketQua = proxy.invokeSuper(obj, args); 
        
        System.out.println("📝 [Log CGLIB]: Đã chạy xong!");
        return ketQua;
    }
}

3. Dùng Enhancer để tạo Proxy

import org.springframework.cglib.proxy.Enhancer;

public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(GiamDocIT.class); // Chỉ định class gốc làm lớp cha
        enhancer.setCallback(new ThuKyCGLIB());  // Giao sổ tay hành động

        // Tạo ra Proxy (chính là một lớp con của GiamDocIT)
        GiamDocIT thuKy = (GiamDocIT) enhancer.create(); 
        
        thuKy.code();
    }
}

Dựa vào việc CGLIB dùng enhancer.setSuperclass(...) để sinh ra một lớp con và ghi đè (override) các phương thức, bạn hãy thử suy luận xem: Nếu trong class GiamDocIT có một hàm được khai báo là private (ví dụ: private void docTaiLieuMat()), thì cô Thư ký CGLIB này có thể chặn (intercept) cuộc gọi đến hàm đó để in log được không? Vì sao?

⏩️⏩️Đây là một điểm rất hay vì nó chạm đến kiến thức nền tảng của Lập trình hướng đối tượng (OOP) trong Java. Hãy mổ xẻ cùng mình nhé.

Cấu trả lời ngắn gọn là: KHÔNG, cô Thư ký CGLIB không thể chặn được hàm private

Đây là lý do tại sao, dựa trên cách CGLIB hoạt động:

  • Bản chất của CGLIB: Như chúng ta vừa thấy ở đoạn code enhancer.setSuperclass(...), CGLIB tạo ra Proxy bằng cách sinh ra một lớp con (subclass) kế thừa trực tiếp từ lớp GiamDocIT. Để can thiệp vào một hàm để in log, lớp con này bắt buộc phải ghi đè (override) hàm đó của lớp cha.

  • Luật của Java: Trong Java, các phương thức được đánh dấu là private thì chỉ có thể được nhìn thấy và sử dụng ở khu vực nội bộ bên trong chính class chứa nó. Lớp con hoàn toàn không được kế thừa, và dĩ nhiên là không thể ghi đè được hàm private.

🕵️‍♂️ Quay lại ví dụ thực tế: Hàm private void docTaiLieuMat() giống như việc Giám đốc đang khóa trái cửa phòng và tự mình đọc một tài liệu tuyệt mật. Cô thư ký (lớp con) đứng ở ngoài không những không thể can thiệp, mà thực chất về mặt nguyên tắc, cô ấy còn không hề biết đến sự tồn tại của hành động này. Do đó, cô ấy không thể nào thực hiện được hành động phụ là in ra log.

Tương tự như vậy, các hàm được đánh dấu là final (không cho phép ghi đè) cũng sẽ khiến CGLIB "bó tay".

Vậy là chúng ta đã nắm rất trọn vẹn cả hai cơ chế tạo Proxy (JDK Dynamic Proxy và CGLIB) cùng những giới hạn của chúng. Những cơ chế này chính là lõi của Spring AOP (Lập trình hướng khía cạnh).

V. Sự phủ sóng của Proxy trong Springboot

Trước khi thực sự khép lại phần 1 ở đây. Chúng ta hãy cùng nhau điểm mặt gọi tên một vài annotation mạnh mẽ đang được xây dựng hoàn toàn dựa trên cơ chế Proxy (cô "Thư ký") mà chúng ta vừa phân tích. Về bản chất, đội ngũ Spring đã viết sẵn các Aspect (Sổ tay hoạt động) cho những annotation này rồi.

Bây giờ mình tin dựa vào những gì chúng ta đã nghiên cứu phía trên, các bạn đã có thể phần nào hiểu được hoàn toàn cơ chế hoạt động phía sau của những annotation cực kỳ phổ biến dưới đây:

🗄️ Nhóm Quản lý giao dịch (Transaction): @Transactional

  • Thư ký làm gì: Mở kết nối an toàn với Database trước khi hàm gốc chạy. Nếu thành công thì lưu (commit), nếu có lỗi thì hoàn tác (rollback).

⚡ Nhóm Bộ nhớ đệm (Caching): @Cacheable, @CachePut, @CacheEvict

  • Thư ký làm gì: Với @Cacheable, Thư ký chặn yêu cầu ở cửa, lục tủ hồ sơ (Cache) xem kết quả này đã tính trước đây chưa. Nếu có rồi, Thư ký trả kết quả luôn (bỏ qua lệnh proceed(), Giám đốc không cần làm). Nếu chưa có, Thư ký mới mời Giám đốc làm rồi lấy kết quả cất vào tủ.

🚀 Nhóm Chạy ngầm (Bất đồng bộ): @Async

  • Thư ký làm gì: Thư ký nhận tài liệu từ khách, bảo khách "cứ đi làm việc khác đi". Sau đó, Thư ký giao việc này cho một luồng (thread) phụ xử lý ngầm, luồng chính không bị chặn lại để chờ Giám đốc làm xong.

🛡️ Nhóm Bảo mật và Phân quyền (Spring Security): @PreAuthorize, @PostAuthorize, @Secured

  • Thư ký làm gì: Kiểm tra thẻ quyền hạn của người dùng. Nếu không đủ quyền, Thư ký chặn cửa và ném ra lỗi AccessDeniedException ngay lập tức.

🔄 Nhóm Thử lại (Spring Retry): @Retryable

  • Thư ký làm gì: Nếu Giám đốc làm việc gặp lỗi (Exception), Thư ký sẽ tự động yêu cầu Giám đốc làm lại ngay lập tức (gọi lại lệnh proceed()) cho đến khi thành công hoặc đạt số lần tối đa.

VI. Tạm kết Phần 1

Vậy là trong phần đầu tiên này, chúng ta đã cùng nhau gỡ bỏ lớp "phép thuật" của Spring Boot để nhìn rõ bản chất kỹ thuật bên dưới: Proxy Design Pattern.

Qua hình tượng cô Thư ký mẫn cán, chúng ta không chỉ hiểu được cách hệ thống bọc các logic phụ trợ (như mở transaction hay check quyền) ra bên ngoài hàm gốc, mà còn bóc trần được cạm bẫy "Self-invocation" – lỗ hổng chí mạng khi Giám đốc tự gọi hàm nội bộ trong phòng kín. Nắm vững ranh giới hoạt động của JDK Proxy và CGLIB, giờ đây những lỗi "bóng ma" như gắn @Transactional mà dữ liệu không chịu rollback chắc chắn sẽ không thể đánh lừa được bạn nữa.

Đón đọc Phần 2: Nâng cấp "Sổ tay Thư ký" với Spring AOP

Nhưng câu chuyện của chúng ta vẫn chưa dừng lại ở đó! Hiểu cách Proxy đứng chặn cửa là một chuyện, nhưng làm sao để giao việc cho cô Thư ký ấy một cách thanh lịch nhất?

Làm thế nào để ra lệnh cho hệ thống: "Hãy tự động đo thời gian chạy của tất cả các hàm bắt đầu bằng chữ 'find'" mà không phải viết đi viết lại những đoạn code tạo Proxy thủ công dài ngoằng?

Câu trả lời nằm ở mảnh ghép mang tên AOP (Aspect-Oriented Programming - Lập trình hướng khía cạnh). Trong Phần 2 tiếp theo, chúng ta sẽ chính thức bước vào thế giới của AOP để:

  1. Chuẩn hóa bộ từ vựng chuyên ngành (Aspect, Pointcut, Advice, Join Point) dựa trên chính câu chuyện Giám đốc - Thư ký.

  2. Tự tay chế tạo một "phép thuật" của riêng bạn: Viết một Custom Annotation từ con số không!

Hẹn gặp lại các bạn ở Phần 2 để cùng nhau làm chủ hoàn toàn sức mạnh của Spring!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.