Trong hệ sinh thái Java, cơ chế ủy quyền cha (Parent Delegation Model) đóng vai trò then chốt trong việc quản lý và nạp các lớp (class). Cơ chế này đảm bảo tính an toàn và ổn định cho môi trường thực thi bằng cách thiết lập một trật tự ưu tiên khi tìm kiếm bytecode.
Hệ thống phân cấp ClassLoader
Trước khi đi sâu vào mã nguồn, cần hiểu rõ các tầng lớp của bộ nạp lớp trong JVM:
- Bootstrap ClassLoader: Được viết bằng mã nguồn gốc (C++), chịu trách nhiệm nạp các thư viện lõi của Java nằm trong
JAVA_HOME/jre/lib (ví dụ: rt.jar).
- Extension ClassLoader: Nạp các lớp mở rộng từ thư mục
JAVA_HOME/jre/lib/ext.
- Application ClassLoader: Bộ nạp mặc định cho các lớp nằm trong đường dẫn
classpath của người dùng.
- Custom ClassLoader: Các bộ nạp do lập trình viên tự định nghĩa để phục vụ các mục đích đặc thù như nạp class từ mạng hoặc mã hóa.
Nguyên lý hoạt động
Quy trình ủy quyền diễn ra theo các bước sau:
1. Khi một ClassLoader nhận yêu cầu nạp một lớp, nó sẽ không tự nạp ngay lập tức.
2. Nó chuyển tiếp yêu cầu đó lên cho ClassLoader cha.
3. Quá trình này tiếp tục cho đến khi lên tới đỉnh là Bootstrap ClassLoader.
4. Nếu ClassLoader cha không tìm thấy lớp, ClassLoader con mới tự thực hiện việc tìm kiếm và nạp lớp đó.
Phân tích mã nguồn java.lang.ClassLoader
Phương thức quan trọng nhất thực hiện cơ chế này là
loadClass. Dưới đây là cấu trúc logic được tái cấu trúc dựa trên phiên bản JDK 8:
protected Class<?> loadTargetClass(String className, boolean resolveLink) throws ClassNotFoundException {
// Đồng bộ hóa để đảm bảo an toàn đa luồng khi nạp class
synchronized (getClassLoadingLock(className)) {
// Bước 1: Kiểm tra xem class này đã được nạp trước đó chưa
Class<?> targetClass = findLoadedClass(className);
if (targetClass == null) {
try {
if (parent != null) {
// Bước 2: Ủy quyền cho cha nạp lớp
targetClass = parent.loadClass(className, false);
} else {
// Bước 3: Nếu không có cha, nhờ Bootstrap ClassLoader
targetClass = findBootstrapClassOrNull(className);
}
} catch (ClassNotFoundException e) {
// Nếu các cấp trên không tìm thấy, catch exception để tiếp tục xử lý ở dưới
}
if (targetClass == null) {
// Bước 4: Tự thân vận động nếu các bậc cha không tìm thấy
targetClass = findClass(className);
}
}
if (resolveLink) {
resolveClass(targetClass);
}
return targetClass;
}
}
Phương thức findClass
Mặc định, phương thức
findClass trong lớp
ClassLoader chỉ ném ra ngoại lệ. Các lớp con (như
URLClassLoader) sẽ ghi đè phương thức này để thực hiện logic đọc file
.class từ ổ đĩa hoặc từ JAR.
protected Class<?> findClass(String className) throws ClassNotFoundException {
// Logic tìm kiếm file byte code và gọi defineClass
throw new ClassNotFoundException(className);
}
Phương thức defineClass
Đây là phương thức cuối cùng trong chuỗi xử lý, chuyển đổi mảng byte thô thành một đối tượng
Class trong bộ nhớ của JVM.
protected final Class<?> defineClass(String name, byte[] classData, int offset, int length) {
// Chuyển đổi dữ liệu nhị phân thành thực thể Class trong JVM
return defineClassInternal(name, classData, offset, length);
}
Lợi ích của cơ chế ủy quyền cha
Việc tuân thủ mô hình này mang lại hai lợi ích lớn cho Java:
- Tránh nạp lớp trùng lặp: Khi lớp cha đã nạp một Class, lớp con không cần phải nạp lại, giúp tiết kiệm tài nguyên bộ nhớ.
- Bảo mật hệ thống: Ngăn chặn việc thay thế các lớp cốt lõi của Java. Ví dụ, nếu một hacker tạo ra lớp
java.lang.String giả mạo, AppClassLoader sẽ luôn ưu tiên ủy quyền lên Bootstrap ClassLoader, nơi lớp String chuẩn đã được nạp, do đó mã độc sẽ không bao giờ được thực thi.
Phá vỡ cơ chế Parent Delegation
Trong một số trường hợp đặc biệt như OSGi hoặc các Web Container (như Tomcat), lập trình viên có thể phá vỡ quy tắc này bằng cách ghi đè trực tiếp phương thức
loadClass mà không gọi
super.loadClass(). Điều này cho phép nạp các phiên bản khác nhau của cùng một thư viện trong cùng một JVM, phục vụ cho việc cô lập ứng dụng.