Loading classes in Java. Practice
The article describes the implementation of the application framework with a plug-in-modular architecture. As an application engine, a custom class loader will be used, which will load additional application plugins.
The application code does not pretend to be original, but only explains the approaches and principles of writing custom class loaders and methods of invocation of dynamic code.
Motivation
Often, the architecture of complex systems involves the use of a dynamic code loading mechanism. This is necessary when it is not known in advance which code will be executed in runtime. For example, the well-known game for programmers Robocode uses its own class loader to load custom tanks into the game. You can consider a separate tank as a module, developed in isolation from the application via a given interface. A similar situation is considered in the article, only at the most simplified level.
In addition, there are several more obvious examples of using the mechanism for dynamically loading code. Let's say the bytecode of the classes is stored in the database. Obviously, to load such classes, you need a special loader, the duties of which will also include the selection of class code from the database.
Classes may need to be downloaded over the network / over the Internet. For such purposes, you need a bootloader that can receive bytecode using one of the network protocols. You can also highlight the existing in the Java Class Library URLClassLoader , which is able to load classes at the specified path in the URL.
Training
The application implemented within the framework of the article will be a framework of the engine for dynamically loading code into the JRE and its execution. Each module will be a single Java class that implements the Module interface. A common interface for all modules is necessary for their invocation. Here, it’s important to understand that there is another way to execute dynamic code — the Java Reflection API. However, for greater clarity and simplicity, a model with a common interface will be used.
When implementing custom loaders, it is important to remember the following:
1) any loader must explicitly or implicitly extend the java.lang.ClassLoader class;
2) any bootloader should support the delegation model of loading, forming a hierarchy;
3) the java.lang.ClassLoader class already implements the direct loading method - defineClass (...), which converts the bytecode into java.lang.Class, validating it;
4) the recursive search mechanism is also implemented in the java.lang.ClassLoader class and there is no need to take care of it;
5) for the correct implementation of the loader, it is enough to just override the findClass () method of the java.lang.ClassLoader class.
Let us consider in detail the behavior of the class loader when calling the loadClass () method to explain the last item in the above list.
The default implementation implies the following sequence of actions:
1) a call to findLoadedClass () to find the loaded class in the cache;
2) if there is no class in the cache, getParent (). LoadClass () is called to delegate the boot right to the parent loader;
3) if the parent loader hierarchy could not load the class, findClass () is called to directly load the class.
Therefore, for the correct implementation of the loaders, it is recommended to adhere to the specified scenario - overriding the findClass () method.
Implementation
Define the interface of the modules. Let the module first load (load), then execute (run), returning the result, and then unload it (unload). This code is an API for developing modules. It can be compiled separately and packaged in * .jar for delivery separately from the main application.
public interface Module {
public static final int EXIT_SUCCESS = 0;
public static final int EXIT_FAILURE = 1;
public void load();
public int run();
public void unload();
}
* This source code was highlighted with Source Code Highlighter.Consider the implementation of the module loader. This loader loads class code from a specific directory, the path to which is specified in the pathtobin variable.
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class ModuleLoader extends ClassLoader {
/**
* Путь до директории с модулями.
*/
private String pathtobin;
public ModuleLoader(String pathtobin, ClassLoader parent) {
super(parent);
this.pathtobin = pathtobin;
}
@Override
public Class<?> findClass(String className) throws ClassNotFoundException {
try {
/**
* Получем байт-код из файла и загружаем класс в рантайм
*/
byte b[] = fetchClassFromFS(pathtobin + className + ".class");
return defineClass(className, b, 0, b.length);
} catch (FileNotFoundException ex) {
return super.findClass(className);
} catch (IOException ex) {
return super.findClass(className);
}
}
/**
* Взято из www.java-tips.org/java-se-tips/java.io/reading-a-file-into-a-byte-array.html
*/
private byte[] fetchClassFromFS(String path) throws FileNotFoundException, IOException {
InputStream is = new FileInputStream(new File(path));
// Get the size of the file
long length = new File(path).length();
if (length > Integer.MAX_VALUE) {
// File is too large
}
// Create the byte array to hold the data
byte[] bytes = new byte[(int)length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
offset += numRead;
}
// Ensure all the bytes have been read in
if (offset < bytes.length) {
throw new IOException("Could not completely read file "+path);
}
// Close the input stream and return bytes
is.close();
return bytes;
}
}
* This source code was highlighted with Source Code Highlighter.Now consider the implementation of the module loading engine. The directory with modules (.class files) is specified as a parameter to the application.
import java.io.File;
public class ModuleEngine {
public static void main(String args[]) {
String modulePath = args[0];
/**
* Создаем загрузчик модулей.
*/
ModuleLoader loader = new ModuleLoader(modulePath, ClassLoader.getSystemClassLoader());
/**
* Получаем список доступных модулей.
*/
File dir = new File(modulePath);
String[] modules = dir.list();
/**
* Загружаем и исполняем каждый модуль.
*/
for (String module: modules) {
try {
String moduleName = module.split(".class")[0];
Class clazz = loader.loadClass(moduleName);
Module execute = (Module) clazz.newInstance();
execute.load();
execute.run();
execute.unload();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
* This source code was highlighted with Source Code Highlighter.We implement the simplest module, which simply prints information on the stages of its execution to standard output. This can be done in a separate application by adding the path to the compiled .jar file with the class Module (API) to CLASSPATH.
public class ModulePrinter implements Module {
@Override
public void load() {
System.out.println("Module " + this.getClass() + " loading ...");
}
@Override
public int run() {
System.out.println("Module " + this.getClass() + " running ...");
return Module.EXIT_SUCCESS;
}
@Override
public void unload() {
System.out.println("Module " + this.getClass() + " inloading ...");
}
}
* This source code was highlighted with Source Code Highlighter.Having compiled this code, the result in the form of a single class file can be copied to a separate directory, the path to which must be specified as a parameter of the main application.
A bit of irony
Dynamic code loading, a great and legal way to betray the responsibilities of expanding the system to the user;)