Cách phá vỡ cơ chế ủy quyền hai chiều trong Java

Trước khi đọc bài viết, hãy suy nghĩ về hai câu hỏi sau và tìm lời giải trong nội dung dưới đây:

  1. Nếu bạn tự viết lại lớp java.lang.String và tải nó bằng URLClassLoader, liệu có thể ghi đè lên lớp String gốc trong JDK không?
  2. Nếu câu trả lời là không, thì có cách nào để thay thế được lớp java.lang.String trong JDK không?

1. Cơ chế ủy quyền hai chiều (Double Parent Delegation)

Trong JVM, cơ chế nạp lớp (class loading) mặc định tuân theo mô hình "ủy quyền hai chiều": khi một ClassLoader nhận yêu cầu nạp lớp, nó sẽ chuyển tiếp yêu cầu này lên ClassLoader cha của nó trước. Quá trình này tiếp tục đệ quy cho đến khi đạt tới Bootstrap ClassLoader — bộ nạp lớp gốc do JVM cung cấp. Chỉ khi bộ nạp cha không thể tìm thấy hoặc nạp lớp đó, bộ nạp con mới tự thực hiện việc nạp.

Mô hình này mang lại hai lợi ích chính:

  • Tránh trùng lặp: Một lớp đã được nạp bởi bộ nạp cha sẽ không bị nạp lại bởi bộ nạp con.
  • Bảo vệ an toàn: Các lớp nền tảng như java.lang.* không thể bị thay thế bởi mã độc từ bên ngoài, vì chúng luôn được nạp trước tiên bởi Bootstrap ClassLoader.

2. Phá vỡ cơ chế ủy quyền

Mặc dù cơ chế ủy quyền hai chiều rất hữu ích, nhưng trong một số tình huống cụ thể, việc phá vỡ nó là cần thiết.

2.1. SPI – Giao diện nhà cung cấp dịch vụ

Một ví dụ kinh điển là JDBC. Giao diện java.sql.Driver nằm trong JDK và được nạp bởi Bootstrap ClassLoader. Tuy nhiên, các triển khai cụ thể (như MySQL, PostgreSQL) lại nằm trong thư viện do nhà cung cấp cung cấp, thường được nạp bởi AppClassLoader (hay còn gọi là hệ thống class loader).

Do Bootstrap ClassLoader không thể nhìn thấy các lớp do AppClassLoader quản lý, nên cơ chế ủy quyền hai chiều thông thường không đủ. Giải pháp là sử dụng SPI (Service Provider Interface), trong đó:

  • Giao diện được định nghĩa trong JDK.
  • Triển khai được khai báo trong file META-INF/services/.
  • Khi cần nạp triển khai, JDK sử dụng Thread.currentThread().getContextClassLoader() — thường là AppClassLoader — để tải lớp triển khai.

Điều này đồng nghĩa với việc bộ nạp cha đang nhờ bộ nạp con nạp lớp, trái ngược hoàn toàn với nguyên tắc ủy quyền hai chiều.

2.2. Hỗ trợ plugin động (hot-pluggable plugins)

Một trường hợp khác là framework DataX — công cụ đồng bộ dữ liệu hỗ trợ kiến trúc plugin. Mỗi plugin (Reader/Writer) được đóng gói riêng biệt dưới dạng JAR và cần được nạp động mà không ảnh hưởng đến môi trường lớp chính.

DataX triển khai điều này bằng cách:

  1. Tạo một URLClassLoader riêng cho mỗi plugin (gọi là JarLoader).
  2. Lưu trữ ClassLoader hiện tại của luồng.
  3. Thay đổi ContextClassLoader của luồng thành JarLoader.
  4. Nạp và khởi tạo lớp plugin.
  5. Khôi phục lại ClassLoader gốc sau khi hoàn tất.

Đoạn mã minh họa:

private Reader.Job initJobReader(JobPluginCollector collector) {
    var swapper = ClassLoaderSwapper.newInstance();
    var pluginLoader = PluginUtil.getJarLoader(READER, readerName);
    
    swapper.swapTo(pluginLoader);
    var job = (Reader.Job) PluginUtil.loadJobPlugin(READER, readerName);
    swapper.restore();
    
    return job;
}

Với lớp hỗ trợ chuyển đổi:

public final class ClassLoaderSwapper {
    private ClassLoader original;

    public static ClassLoaderSwapper newInstance() {
        return new ClassLoaderSwapper();
    }

    public void swapTo(ClassLoader target) {
        this.original = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(target);
    }

    public void restore() {
        Thread.currentThread().setContextClassLoader(this.original);
    }
}

Cách tiếp cận này hoàn toàn bỏ qua cơ chế ủy quyền hai chiều: plugin được nạp trực tiếp bởi URLClassLoader tùy chỉnh, không thông qua bất kỳ bộ nạp cha nào.

Thẻ: Java ClassLoader SPI Plugin Architecture JVM

Đăng vào ngày 28 tháng 5 lúc 14:56