Part One. Prompt Do I need to read this article?
The Java class loader is crucial to the operation of the Java system, but it is often ignored by us. The Java class loader loads classes at runtime by finding and loading them. Custom class loaders can completely change the way classes are loaded, personalizing your Java virtual machine the way you like. This article briefly introduces the Java class loader, and then illustrates it through an example of constructing a custom class loader. This class loader will automatically compile the code before loading the class. You'll learn what a class loader actually does and how to create your own. As long as you have some basic Java knowledge, know how to create, compile, and run a command-line Java program and some basic concepts of Java class files, you can understand the content of this article. After reading this article, you should be able to:
* Expand the functions of Java virtual machine
* Create a custom class loader
* How to integrate a custom class loader into your application
* Modify your class loader to be Java 2 compatible
Part 2. Introduction What is a class loader?
The difference between Java and other languages is that Java runs on the Java Virtual Machine (JVM). This means that the compiled code is saved in a platform-independent format, rather than a format that runs on a specific machine. This format has many important differences from the traditional executable code format. Specifically, unlike a C or C++ program, a Java program is not an independent executable file, but consists of many separate class files, each class file corresponding to a Java class. In addition, these class files are not loaded into memory immediately, but are loaded when the program needs them. A class loader is a tool used in the Java virtual machine to load classes into memory. Moreover, the Java class loader is also implemented in Java. This way you can easily create your own class loader without having an in-depth understanding of the Java virtual machine.
Why create a class loader?
Now that Java Virtual Gold already has a class loader, do we need to create others ourselves? Good question. The default class loader only knows how to load classes from the local system. When your program is compiled entirely natively, the default class loader generally works well. But one of the most exciting things about Java is how easy it is to load classes from the network instead of just locally.
For example, a browser can load classes through a custom class loader. There are also many ways to load classes. One of the most exciting things about Java is that you can customize it besides simply from local or network:
* Automatically verify digital signatures before executing untrusted code
* Decrypt the code based on the password provided by the user
* Dynamically create classes according to user needs. Anything you care about can be easily integrated into your application in the form of bytecode. Examples of custom class loaders if you have used JDK (Java Software Development Kit) appletviewer (small application browser) or other
For Java embedded browsers, you already use a custom class loader. When Sun first released the Java language, one of the most exciting things was watching how Java executed code downloaded from a remote website. Execute from remote site via HTTP
The bytecode transmitted by the P connection looks a bit weird. This works because Java has the ability to install custom class loaders. The applet browser contains a class loader. This class loader does not find Java classes locally. Instead, it accesses the remote server, loads the original bytecode file through HTTP, and then converts it into a Java class in the Java virtual machine. Of course class loaders do a lot of other things: they block unsafe Java classes and keep different applets on different pages from interfering with each other. Echidna, a package written by Luke Gorrie, is an open Java software package that allows multiple Java applications to be safely run in a Java virtual machine. It prevents interference between applications by using a custom class loader to give each application a copy of the class file.
Our class loader example Now that we know how a class loader works and how to define our own class loader, we create a custom class loader named CompilingClassLoader (CCL). CCL does the compilation work for us, so we don’t have to compile it manually ourselves. This is basically equivalent to having a "make" program that builds into our runtime environment.
Note: Before we proceed to the next step, it is necessary to understand some related concepts.
The system has been greatly improved in JDK version 1.2 (which is what we call the Java 2 platform). This article was written under JDK 1.0 and 1.1, but everything will work in later versions. ClassLoader has also been improved in Java2.
Detailed introduction is provided in the fifth part.
Part 3. Overview of the structure of ClassLoader The basic purpose of a class loader is to serve requests for Java classes. When the Java virtual machine needs a class, it gives a class name to the class loader, and then the class loader tries to return a corresponding class instance. Custom class loaders can be created by overriding the corresponding methods at different stages. Next we will learn about some of the main methods of the class loader. You'll understand what these methods do and how they work when loading class files. You'll also know what code you need to write when creating a custom class loader. In the next part, you'll leverage this knowledge and our custom CompilingCl
assLoader works together.
Method loadClass
ClassLoader.loadClass() is the entry point of ClassLoader. The method signature is as follows:
Class loadClass(String name, boolean resolve);
The parameter name specifies the full name of the class (including the package name) required by the Java virtual machine, such as Foo or java.lang.Object.
The resolve parameter specifies whether the class needs to be resolved. You can understand the resolution of the class as being completely ready for running. Parsing is generally not required. If the Java virtual machine only wants to know whether this class exists or wants to know its parent class, parsing is completely unnecessary. In Java 1.1 and its previous versions, if you want to customize the class loader, the loadClass method is the only method that needs to be overridden in the subclass.
(ClassLoader changed in Java1.2 and provided the method findClass()).
methoddefineClass
defineClass is a very mysterious method in ClassLoader. This method builds a class instance from a byte array. This raw byte array containing data may come from the file system or from the network. defineClass illustrates the complexity, mystery, and platform dependence of the Java Virtual Machine - it interprets bytecode to turn it into runtime data structures, checks for validity, and more. But don’t worry, you don’t have to do any of this. Actually, you can't override it at all,
Because the method is modified by the keyword final.
MethodfindSystemClass
The findSystemClass method loads files from the local system. It looks for class files on the local system and, if found, calls
defineClass converts the original byte array into a class object. This is the default mechanism for the Java virtual machine to load classes when running Java applications. For custom class loaders, we only need to use findSystemClass after we fail to load. The reason is simple: our class loader is responsible for performing certain steps in class loading, but not all classes. for example,
Even if our class loader loads some classes from the remote site, there are still many basic classes that need to be loaded from the local system.
These classes are not of concern to us, so we let the Java virtual machine load them the default way: from the local system. This is what findSystemClass does. The whole process is roughly as follows:
* The Java virtual machine requests our custom class loader to load the class.
* We check if the remote site has the class that needs to be loaded.
* If there is, we get this class.
* If not, we think this class is in the basic class library and call findSystemClass to load it from the file system.
In most custom class loaders, you should call findSystemClass first to save time looking up from the remote.
In fact, as we will see in the next section, the Java virtual machine is only allowed to load classes from the local file system when we are sure that we have automatically compiled our code.
Method resolveClass
As mentioned above, class records can be divided into partial loading (without parsing) and complete loading (including parsing). When we create a custom class loader, we may need to call resolveClass.
MethodfindLoadedClass
findLoadedClass implements a cache: when loadClass is required to load a class, you can first call this method to see if the class has been loaded to prevent reloading an already loaded class. This method must be called first. Let's take a look at how these methods are organized together.
Our example implementation of loadClass performs the following steps. (We do not specify a specific technology to obtain the class file - it may be from the network, from a compressed package or dynamically compiled. In any case, what we obtain is the original bytecode file)
* Call findLoadedClass to check whether this class has been loaded.
* If not loaded, we get the original byte array somehow.
* If the array has been obtained, call defineClass to convert it into a class object.
* If the original byte array cannot be obtained, call findSystemClass to check whether it can be recorded from the local file system.
* If the parameter resolve is true, call resolveClass to resolve the class object.
* If the class has not been found, throw a ClassNotFoundException.
* Otherwise, return this class.
Now that we have a more comprehensive understanding of the working knowledge of class loaders, we can create a custom class loader. In the next section, we will discuss CCL.
Part 4. CompilingClassLoader
CCL shows us the function of the class loader. The purpose of CCL is to enable our code to be automatically compiled and updated. Here's how it works:
* When there is a request for a class, first check whether the class file exists in the current directory and subdirectories of the disk.
* If there is no class file, but there is a source code file, call the Java compiler to compile and generate the class file.
* If the class file already exists, check whether the class file is older than the source code file. If the class file is older than the source code file, call the Java compiler to regenerate the class file.
* If compilation fails, or the class file cannot be generated from the source file due to other reasons, throw the exception ClassNotFou
ndException.
* If you haven't obtained this class yet, it may exist in other class libraries. Call findSystemClass to see if it can be found.
* If not found, throw ClassNotFoundException.
* Otherwise, return this class.
How is Java compilation implemented?
Before we go any further, we need to understand the Java compilation process. Normally, the Java compiler compiles only those classes specified. It will also compile other related classes if required by the specified classes. CCL will compile the classes we need to compile in the application one by one. However, generally speaking, after the compiler compiles the first class,
CCL will find that other required related classes have actually been compiled. Why? The Java compiler uses similar rules as we did: if a class does not exist or the source file has been updated, the class will be compiled. The Java compiler is basically one step ahead of CCL, and most of the work is done by the Java compiler. We look like CCL is compiling these classes.
In most cases, you will find that it is calling the compiler in the main function class, and that's it - a simple call is enough. However, there is a special case where these classes are not compiled the first time they appear. If you load a class based on its name, using the method Class.forName, the Java compiler does not know whether the class is needed. in this case,
You find that CCL calls the compiler again to compile the class. The code in Section 6 illustrates this process.
Using CompilationClassLoader
In order to use CCL, we cannot run our program directly, it must be run in a special way, like this:
% java Foo arg1 arg2
We run it like this:
% java CCLRun Foo arg1 arg2
CCLRun is a special stub program that creates CompilingClassLoader and uses it to load our main function class. This ensures that all the entire program is loaded by CompilingClassLoader. CCLRun utilizes Ja
The va reflection API calls the main function of the main function class and passes parameters to this function. To learn more, refer to the source code in Part 6.
Let's run the example to demonstrate how the entire process works.
The main program is a class called Foo, which creates an instance of class Bar. This Bar instance in turn creates an instance of class Baz, which exists in package baz. This is to demonstrate how CCL loads classes from sub-packages. Bar also loads class Boo based on class name
, this is also done by CCL. All classes are loaded and ready to run. Use the source code from Chapter 6 to execute this program. Compile CCLRun and CompilingClassLoader. Make sure you don't compile other classes (Foo, Bar, Baz, a
nd Boo), otherwise CCL will not work.
% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!
Notice that the compiler is called for the first time for Foo.java, and Bar and baz.Baz are also compiled together. And like Boo
When the channel needs to be loaded, CCL calls the compiler again to compile it.
Part 5. Overview of Class Loader Improvements in Java 2 In Java 1.2 and later versions, the class loader has been greatly improved. The old code still works, but the new system makes our implementation easier. This new model is the proxy delegation model, which means that if the class loader cannot find a class, it will ask its parent class loader to find it. The system class loader is the ancestor of all class loaders. The system class loader loads classes by default-that is, from the local file system. Overriding the loadClass method generally tries several ways to load the class. If you write a lot of class loaders, you will find that you just make some modifications in this complicated method again and again. The default implementation of loadClass in Java 1.2 includes the most common way of finding a class, allowing you to override the findClass method and loadClass to call the findClass method appropriately. The advantage of this is that you don't need to override loadClass, you only need to override findClass, which reduces the workload.
New method: findClass
This method is called by the default implementation of loadClass. The goal of findClass is to include all class loader specific code,
There is no need to repeat the code (such as calling the system class loader when the specified method fails).
New method: getSystemClassLoader
Regardless of whether you override the findClass and loadClass methods, the getSystemClassLoader method can directly access the system class loader (rather than indirect access through findSystemClass).
New method: getParent
In order to delegate the request to the parent class loader, the parent class loader of this class loader can be obtained through this method. You may delegate the request to the parent class loader when a specific method in a custom class loader cannot find the class. A class loader's parent class loader contains the code that creates the class loader.
Part 6. Source code
CompilingClassLoader.java
The following is the content of the file CompilingClassLoader.java
import java.io.*;
/*
CompilingClassLoader dynamically compiles Java source files. It checks whether the .class file exists and whether the .class file is older than the source file.
*/
public class CompilingClassLoader extends ClassLoader
{
//Specify a file name, read the entire file content from disk, and return a byte array.
private byte[] getBytes( String filename ) throws IOException {
// Get the file size.
File file = new File( filename );
long len = file.length();
//Create an array just enough to store the contents of the file.
byte raw[] = new byte[(int)len];
//Open file
FileInputStream fin = new FileInputStream( file );
// Read all content. If it cannot be read, an error occurred.
int r = fin.read(raw);
if (r != len)
throw new IOException( "Can''''t read all, "+r+" != "+len );
// Don't forget to close the file.
fin.close();
// Return this array.
return raw;
}
// Generate a process to compile the specified Java source file and specify file parameters. If the compilation is successful, return true, otherwise,
// Return false.
private boolean compile(String javaFile) throws IOException {
//Show current progress
System.out.println( "CCL: Compiling "+javaFile+"..." );
//Start the compiler
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// Wait for compilation to end
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// Check the return code to see if there are compilation errors.
int ret = p.exitValue();
//Return whether compilation is successful.
return ret==0;
}
// The core code of the class loader - loading classes automatically compiles source files when needed.
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
//Our purpose is to obtain a class object.
Class class = null;
// First, check whether this class has been processed.
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// Get the path name through the class name, for example: java.lang.Object => java/lang/Object
String fileStub = name.replace( ''''.'''', ''''/'''' );
// Construct objects pointing to source files and class files.
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
File javaFile = new File( javaFilename );
File classFile = new File( classFilename );
//System.out.println( "j "+javaFile.lastModified()+" c "
//+classFile.lastModified() );
// First, determine whether compilation is required. If the source file exists but the class file does not exist, or both exist but the source file
// Newer, indicating that it needs to be compiled.
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// Compile, if compilation fails, we must declare the reason for failure (just using stale classes is not enough).
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch(IOException ie) {
// An IO error may occur during compilation.
throw new ClassNotFoundException(ie.toString());
}
}
// Make sure it has been compiled correctly or does not require compilation, we start loading raw bytes.
try {
// Read bytes.
byte raw[] = getBytes( classFilename );
//Convert to class object
clas = defineClass( name, raw, 0, raw.length );
} catch(IOException ie) {
// This does not mean failure, maybe the class we are dealing with is in the local class library, such as java.lang.Object.
}
//System.out.println( "defineClass: "+clas );
//Maybe in the class library, loaded in the default way.
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// If the parameter resolve is true, interpret the class as needed.
if (resolve && clas != null)
resolveClass(clas);
// If the class has not been obtained, something went wrong.
if (class == null)
throw new ClassNotFoundException( name );
// Otherwise, return this class object.
return clas;
}
}
CCRun.java
Here is the CCRun.java file
import java.lang.reflect.*;
/*
CCLRun loads classes through CompilingClassLoader to run the program.
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
//The first parameter specifies the main function class that the user wants to run.
String progClass = args[0];
//The next parameters are the parameters passed to this main function class.
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// Create CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
//Load the main function class through CCL.
Class clas = ccl.loadClass( progClass );
// Use reflection to call its main function and pass parameters.
// Generate a class object representing the parameter type of the main function.
Class mainArgType[] = { (new String[0]).getClass() };
// Find the standard main function in the class.
Method main = clas.getMethod( "main", mainArgType );
// Create a parameter list - in this case, an array of strings.
Object argsArray[] = { progArgs };
// Call the main function.
main.invoke( null, argsArray );
}
}
Foo.java
The following is the content of the file Foo.java
public class Foo
{
static public void main( String args[] ) throws Exception {
System.out.println( "foo! "+args[0]+" "+args[1] );
new Bar( args[0], args[1] );
}
}
Bar.java
The following is the content of the file Bar.java
import baz.*;
public class Bar
{
public Bar( String a, String b ) {
System.out.println( "bar! "+a+" "+b );
new Baz( a, b );
try {
Class booClass = Class.forName( "Boo" );
Object boo = booClass.newInstance();
} catch(Exception e) {
e.printStackTrace();
}
}
}
baz/Baz.java
The following is the content of the file baz/Baz.java
package baz;
public class Baz
{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}
Boo.java
The following is the content of the file Boo.java
public class Boo
{
publicBoo() {
System.out.println( "Boo!" );
}
}
Part 7. Summary Summary After reading this article, did you realize that creating a custom class loader allows you to go deep into the internals of the Java virtual machine. You can load a class file from any resource, or generate it dynamically, so that you can do a lot of things you are interested in by extending these functions, and you can also complete some powerful functions.
Other topics about ClassLoader As mentioned at the beginning of this article, custom class loaders play an important role in Java embedded browsers and applet browsers.