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:
- Nếu bạn tự viết lại lớp
java.lang.Stringvà tải nó bằngURLClassLoader, liệu có thể ghi đè lên lớpStringgốc trong JDK không? - Nếu câu trả lời là không, thì có cách nào để thay thế được lớp
java.lang.Stringtrong 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ởiBootstrap 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:
- Tạo một
URLClassLoaderriêng cho mỗi plugin (gọi làJarLoader). - Lưu trữ
ClassLoaderhiện tại của luồng. - Thay đổi
ContextClassLoadercủa luồng thànhJarLoader. - Nạp và khởi tạo lớp plugin.
- Khôi phục lại
ClassLoadergố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.