Java ClassLoader là một trong những thành phần quan trọng nhưng hiếm khi được sử dụng trong quá trình phát triển dự án. Tôi chưa bao giờ mở rộng ClassLoader trong bất kỳ dự án nào của mình. Nhưng ý tưởng về việc có ClassLoader riêng có thể tùy chỉnh quá trình tải Java class thật thú vị. Bài viết này sẽ cung cấp cho bạn cái nhìn tổng quan về Java ClassLoader và sau đó sẽ tiến tới việc tạo một custom ClassLoader (ClassLoader tùy chình) trong Java.

Java ClassLoader là gì?
Chúng ta biết rằng chương trình Java chạy trên JVM (Java Virtual Machine). Khi chúng ta biên dịch một Java Class, JVM tạo ra bytecode, là mã độc lập với nền tảng và máy. Loại mã này được lưu trữ trong một file .class. Khi chúng ta cố gắng sử dụng một class, ClassLoader sẽ tải nó vào bộ nhớ.
Các loại ClassLoader tích hợp sẵn
Có ba loại ClassLoader tích hợp sẵn trong Java.
- Bootstrap ClassLoader (ClassLoader khởi động) – Nó tải các JDK internal class. Nó tải
rt.jarvà các core class khác, ví dụ như các class trong packagejava.lang.*. - Extensions ClassLoader (ClassLoader mở rộng) – Nó tải các class từ thư mục mở rộng của JDK, thường là thư mục $JAVA_HOME/lib/ext.
- System ClassLoader (ClassLoader hệ thống) – ClassLoader này tải các class từ classpath hiện tại. Chúng ta có thể đặt classpath khi gọi một chương trình bằng cách sử dụng tùy chọn dòng lệnh cp hoặc classpath.
Phân cấp ClassLoader
ClassLoader có tính phân cấp trong việc tải một class vào bộ nhớ. Bất cứ khi nào có yêu cầu tải một class, nó sẽ ủy quyền cho parent ClassLoader (ClassLoader cha). Đây là cách mà tính duy nhất của các class được duy trì trong môi trường runtime. Nếu parent ClassLoader không tìm thấy class thì ClassLoader đó sẽ tự cố gắng tải class. Chúng ta hãy cùng tìm hiểu điều này bằng cách thực thi chương trình Java dưới đây.
package com.journaldev.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("class loader for HashMap: "
+ java.util.HashMap.class.getClassLoader());
System.out.println("class loader for DNSNameService: "
+ sun.net.spi.nameservice.dns.DNSNameService.class
.getClassLoader());
System.out.println("class loader for this class: "
+ ClassLoaderTest.class.getClassLoader());
System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());
}
}
Kết quả
class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37
Cách Java ClassLoader hoạt động?
Hãy cùng tìm hiểu cách hoạt động của các class loader từ output chương trình trên.
- ClassLoader của
java.util.HashMapxuất hiện là null, điều này phản ánh Bootstrap ClassLoader. ClassLoader của DNSNameService class là ExtClassLoader. Vì bản thân class này nằm trong CLASSPATH nên System ClassLoader sẽ tải nó. - Khi chúng ta cố gắng tải HashMap, System ClassLoader của chúng ta sẽ ủy quyền nó cho Extension ClassLoader. Extension ClassLoader ủy quyền nó cho Bootstrap ClassLoader. Bootstrap ClassLoader tìm thấy HashMap class và tải nó vào bộ nhớ JVM.
- Quá trình tương tự được tuân thủ cho DNSNameService class. Nhưng, Bootstrap ClassLoader không thể định vị nó vì nó nằm trong $JAVA_HOME/lib/ext/dnsns.jar. Do đó, nó được tải bởi Extensions Classloader.
- Blob class được bao gồm trong MySql JDBC Connector jar (mysql-connector-java-5.0.7-bin.jar), vốn có mặt trong build path của dự án. Nó cũng đang được tải bởi System Classloader.
- Các class được tải bởi một child class loader (ClassLoader con) có quyền hiển thị vào các class được tải bởi các parent class loader của nó. Vì vậy, các class được tải bởi System Classloader có quyền hiển thị vào các class được tải bởi Extensions và Bootstrap Classloader.
- Nếu có các sibling class loader thì chúng không thể truy cập các class được tải bởi nhau.
Tại sao phải viết một Custom ClassLoader trong Java?
Java default ClassLoader có thể tải các class từ local file system, điều này là đủ tốt cho hầu hết các trường hợp. Tuy nhiên, nếu bạn mong đợi một class tại runtime hoặc từ FTP server hoặc thông qua một web service của bên thứ ba tại thời điểm tải class, thì bạn phải mở rộng class loader hiện có. Ví dụ, AppletViewers tải các class từ một web server từ xa.
Java ClassLoader Methods
- Khi JVM yêu cầu một class, nó gọi hàm
loadClass()của ClassLoader bằng cách truyền vào tên đủ điều kiện (fully classified name) của Class đó. - Hàm
loadClass()gọi phương thứcfindLoadedClass()để kiểm tra xem class đã được tải trước đó hay chưa. Điều này là cần thiết để tránh tải cùng một class nhiều lần. - Nếu Class chưa được tải, thì nó sẽ ủy quyền (delegate) yêu cầu cho parent ClassLoader để tải class.
- Nếu parent ClassLoader không tìm thấy class, thì nó sẽ gọi phương thức
findClass()để tìm kiếm các class trong file system.
Ví dụ về Java Custom ClassLoader
Chúng ta sẽ tạo ClassLoader riêng bằng cách mở rộng lớp ClassLoader và ghi đè phương thức loadClass(String name). Nếu tên class bắt đầu bằng com.journaldev thì chúng ta sẽ tải nó bằng custom class loader của mình, hoặc nếu không, chúng ta sẽ gọi phương thức loadClass() của parent ClassLoader để tải class.

1. CCLoader.java
Đây là custom class loader của chúng ta với các method dưới đây.
private byte[] loadClassFileData(String name): Method này sẽ đọc file class từ file system thành byte array.private Class<?> getClass(String name): Method này sẽ gọi hàmloadClassFileData()và bằng cách gọi methoddefineClass()của parent, nó sẽ tạo ra Class và trả về.public Class<?> loadClass(String name): Method này chịu trách nhiệm tải class. Nếu tên class bắt đầu bằngcom.journaldev(các sample class của chúng ta) thì nó sẽ tải bằng methodgetClass(), hoặc nếu không, nó sẽ gọi hàmloadClass()của parent để tải.public CCLoader(ClassLoader parent): Đây là constructor (hàm khởi tạo), chịu trách nhiệm thiết lập parent ClassLoader
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Our Custom ClassLoader to load the classes. Any class in the com.journaldev
* package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
*
*/
public class CCLoader extends ClassLoader {
/**
* This constructor is used to set the parent ClassLoader
*/
public CCLoader(ClassLoader parent) {
super(parent);
}
/**
* Loads the class from the file system. The class file should be located in
* the file system. The name should be relative to get the file location
*
* @param name
* Fully Classified name of the class, for example, com.journaldev.Foo
*/
private Class getClass(String name) throws ClassNotFoundException {
String file = name.replace('.', File.separatorChar) + ".class";
byte[] b = null;
try {
// This loads the byte code data from the file
b = loadClassFileData(file);
// defineClass is inherited from the ClassLoader class
// that converts byte array into a Class. defineClass is Final
// so we cannot override it
Class c = defineClass(name, b, 0, b.length);
resolveClass(c);
return c;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* Every request for a class passes through this method. If the class is in
* com.journaldev package, we will use this classloader or else delegate the
* request to parent classloader.
*
*
* @param name
* Full class name
*/
@Override
public Class loadClass(String name) throws ClassNotFoundException {
System.out.println("Loading Class '" + name + "'");
if (name.startsWith("com.journaldev")) {
System.out.println("Loading Class using CCLoader");
return getClass(name);
}
return super.loadClass(name);
}
/**
* Reads the file (.class) into a byte array. The file should be
* accessible as a resource and make sure that it's not in Classpath to avoid
* any confusion.
*
* @param name
* Filename
* @return Byte array read from the file
* @throws IOException
* if an exception comes in reading the file
*/
private byte[] loadClassFileData(String name) throws IOException {
InputStream stream = getClass().getClassLoader().getResourceAsStream(
name);
int size = stream.available();
byte buff[] = new byte[size];
DataInputStream in = new DataInputStream(stream);
in.readFully(buff);
in.close();
return buff;
}
}
2. CCRun.java
Đây là test class của chúng ta với hàm main. Chúng ta đang tạo một instance của ClassLoader của mình và tải các class mẫu bằng cách sử dụng method loadClass() của nó. Sau khi tải class, chúng ta đang sử dụng Java Reflection API để gọi các method của nó.
import java.lang.reflect.Method;
public class CCRun {
public static void main(String args[]) throws Exception {
String progClass = args[0];
String progArgs[] = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
Class clas = ccl.loadClass(progClass);
Class mainArgType[] = { (new String[0]).getClass() };
Method main = clas.getMethod("main", mainArgType);
Object argsArray[] = { progArgs };
main.invoke(null, argsArray);
// Below method is used to check that the Foo is getting loaded
// by our custom class loader i.e CCLoader
Method printCL = clas.getMethod("printCL", null);
printCL.invoke(null, new Object[0]);
}
}
3. Foo.java và Bar.java
Đây là các test class của chúng ta đang được tải bởi custom classloader của chúng ta. Chúng có một method printCL() đang được gọi để in thông tin về ClassLoader. Foo class sẽ được tải bởi custom class loader của chúng ta. Foo sử dụng Bar class, vì vậy Bar class cũng sẽ được tải bởi custom class loader của chúng ta.
package com.journaldev.cl;
public class Foo {
static public void main(String args[]) throws Exception {
System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
Bar bar = new Bar(args[0], args[1]);
bar.printCL();
}
public static void printCL() {
System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
}
}
package com.journaldev.cl;
public class Bar {
public Bar(String a, String b) {
System.out.println("Bar Constructor >>> " + a + " " + b);
}
public void printCL() {
System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
}
}
4. Các bước thực thi Java Custom ClassLoader
Trước hết, chúng ta sẽ biên dịch tất cả các class thông qua command line. Sau đó, chúng ta sẽ chạy CCRun class bằng cách truyền ba đối số (argument). Đối số đầu tiên là tên đầy đủ (fully classified name) cho Foo class sẽ được tải bởi class loader của chúng ta. Hai đối số còn lại được truyền cho hàm main của Foo class và constructor của Bar. Các bước thực thi và output sẽ như dưới đây.
$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$
Nếu bạn nhìn vào output, nó đang cố gắng tải com.journaldev.cl.Foo class. Vì nó đang mở rộng java.lang.Object class, nên nó cố gắng tải Object class trước. Do đó, yêu cầu đến method loadClass của CCLoader, vốn đang ủy quyền nó cho parent class. Vì vậy, các parent class loader đang tải các Object, String, và các Java class khác. ClassLoader của chúng ta chỉ tải Foo và Bar class từ file system. Điều này rõ ràng từ output của hàm printCL(). Chúng ta có thể thay đổi chức năng của loadClassFileData() để đọc byte array từ FTP Server hoặc bằng cách gọi bất kỳ third party service nào để lấy class byte array một cách nhanh chóng. Tôi hy vọng rằng bài viết này sẽ hữu ích trong việc hiểu cách hoạt động của Java ClassLoader và cách chúng ta có thể mở rộng nó để làm được nhiều điều hơn là chỉ lấy từ file system.
Biến Custom ClassLoader thành Default ClassLoader
Chúng ta có thể biến custom class loader của mình thành default class loader khi JVM khởi động bằng cách sử dụng Java Options. Ví dụ, tôi sẽ chạy chương trình ClassLoaderTest một lần nữa sau khi cung cấp tùy chọn Java classloader.
$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$
CCLoader đang tải lớp ClassLoaderTest vì nó nằm trong package com.journaldev.
Bạn có thể tải mã ví dụ về ClassLoader từ kho lưu trữ GitHub của chúng tôi.