Primera parte. Pregunta ¿Necesito leer este artículo?
El cargador de clases Java es crucial para el funcionamiento del sistema Java, pero a menudo lo ignoramos. El cargador de clases de Java carga clases en tiempo de ejecución buscándolas y cargándolas. Los cargadores de clases personalizados pueden cambiar completamente la forma en que se cargan las clases, personalizando su máquina virtual Java como desee. Este artículo presenta brevemente el cargador de clases de Java y luego lo ilustra mediante un ejemplo de construcción de un cargador de clases personalizado. Este cargador de clases compilará automáticamente el código antes de cargar la clase. Aprenderá qué hace realmente un cargador de clases y cómo crear el suyo propio. Siempre que tenga algunos conocimientos básicos de Java, sepa cómo crear, compilar y ejecutar un programa Java de línea de comandos y algunos conceptos básicos de archivos de clase Java, podrá comprender el contenido de este artículo. Después de leer este artículo, debería poder:
* Ampliar las funciones de la máquina virtual Java.
* Crear un cargador de clases personalizado
* Cómo integrar un cargador de clases personalizado en su aplicación
* Modifique su cargador de clases para que sea compatible con Java 2
Parte 2. Introducción ¿Qué es un cargador de clases?
La diferencia entre Java y otros lenguajes es que Java se ejecuta en la Máquina Virtual Java (JVM). Esto significa que el código compilado se guarda en un formato independiente de la plataforma, en lugar de un formato que se ejecuta en una máquina específica. Este formato tiene muchas diferencias importantes con respecto al formato de código ejecutable tradicional. Específicamente, a diferencia de un programa C o C++, un programa Java no es un archivo ejecutable independiente, sino que consta de muchos archivos de clases separados, cada archivo de clase correspondiente a una clase Java. Además, estos archivos de clase no se cargan en la memoria inmediatamente, sino que se cargan cuando el programa los necesita. Un cargador de clases es una herramienta utilizada en la máquina virtual Java para cargar clases en la memoria. Además, el cargador de clases de Java también está implementado en Java. De esta manera, puede crear fácilmente su propio cargador de clases sin tener un conocimiento profundo de la máquina virtual Java.
¿Por qué crear un cargador de clases?
Ahora que Java Virtual Gold ya tiene un cargador de clases, ¿necesitamos crear otros nosotros mismos? Buena pregunta. El cargador de clases predeterminado solo sabe cómo cargar clases desde el sistema local. Cuando su programa se compila de forma totalmente nativa, el cargador de clases predeterminado generalmente funciona bien. Pero una de las cosas más interesantes de Java es lo fácil que es cargar clases desde la red en lugar de hacerlo sólo localmente.
Por ejemplo, un navegador puede cargar clases a través de un cargador de clases personalizado. También hay muchas formas de cargar clases. Una de las cosas más interesantes de Java es que puedes personalizarlo además de simplemente desde la red local o:
* Verificar automáticamente las firmas digitales antes de ejecutar código que no sea de confianza
* Descifrar el código según la contraseña proporcionada por el usuario
* Cree clases dinámicamente según las necesidades del usuario. Cualquier cosa que le interese se puede integrar fácilmente en su aplicación en forma de código de bytes. Ejemplos de cargadores de clases personalizados si ha utilizado el appletviewer (navegador de aplicaciones pequeñas) JDK (Java Software Development Kit).
Para los navegadores integrados de Java, ya utiliza un cargador de clases personalizado. Cuando Sun lanzó por primera vez el lenguaje Java, una de las cosas más interesantes fue ver cómo Java ejecutaba el código descargado desde un sitio web remoto. Ejecutar desde un sitio remoto a través de HTTP
El código de bytes transmitido por la conexión P parece un poco extraño. Esto funciona porque Java tiene la capacidad de instalar cargadores de clases personalizados. El navegador de subprogramas contiene un cargador de clases. Este cargador de clases no encuentra clases Java localmente, sino que accede al servidor remoto, carga el archivo de código de bytes original a través de HTTP y luego lo convierte en una clase Java en la máquina virtual Java. Por supuesto, los cargadores de clases hacen muchas otras cosas: bloquean clases Java no seguras y evitan que diferentes subprogramas en diferentes páginas interfieran entre sí. Echidna, un paquete escrito por Luke Gorrie, es un paquete de software Java abierto que permite ejecutar de forma segura múltiples aplicaciones Java en una máquina virtual Java. Evita la interferencia entre aplicaciones mediante el uso de un cargador de clases personalizado para darle a cada aplicación una copia del archivo de clase.
Nuestro ejemplo de cargador de clases Ahora que sabemos cómo funciona un cargador de clases y cómo definir nuestro propio cargador de clases, creamos un cargador de clases personalizado llamado CompilingClassLoader (CCL). CCL hace el trabajo de compilación por nosotros, por lo que no tenemos que compilarlo manualmente nosotros mismos. Esto es básicamente equivalente a tener un programa "make" integrado en nuestro entorno de ejecución.
Nota: Antes de continuar con el siguiente paso, es necesario comprender algunos conceptos relacionados.
El sistema se ha mejorado mucho en la versión 1.2 de JDK (que es lo que llamamos la plataforma Java 2). Este artículo fue escrito bajo JDK 1.0 y 1.1, pero todo funcionará en versiones posteriores. ClassLoader también se ha mejorado en Java2.
En la quinta parte se proporciona una introducción detallada.
Parte 3. Descripción general de la estructura de ClassLoader El propósito básico de un cargador de clases es atender solicitudes de clases Java. Cuando la máquina virtual Java necesita una clase, le da un nombre de clase al cargador de clases y luego el cargador de clases intenta devolver una instancia de clase correspondiente. Se pueden crear cargadores de clases personalizados anulando los métodos correspondientes en diferentes etapas. A continuación conoceremos algunos de los métodos principales del cargador de clases. Comprenderá qué hacen estos métodos y cómo funcionan al cargar archivos de clase. También sabrá qué código necesita escribir al crear un cargador de clases personalizado. En la siguiente parte, aprovechará este conocimiento y nuestro CompilingCl personalizado.
assLoader trabaja en conjunto.
Método loadClass
ClassLoader.loadClass() es el punto de entrada de ClassLoader. La firma del método es la siguiente:
Clase loadClass (nombre de cadena, resolución booleana);
El nombre del parámetro especifica el nombre completo de la clase (incluido el nombre del paquete) requerida por la máquina virtual Java, como Foo o java.lang.Object.
El parámetro de resolución especifica si la clase necesita resolverse. Puede entender que la resolución de la clase está completamente lista para ejecutarse. Generalmente no es necesario realizar análisis. Si la máquina virtual Java solo quiere saber si esta clase existe o quiere saber su clase principal, el análisis es completamente innecesario. En Java 1.1 y sus versiones anteriores, si desea personalizar el cargador de clases, el método loadClass es el único método que debe anularse en la subclase.
(ClassLoader cambió en Java1.2 y proporcionó el método findClass()).
métododefinirClase
defineClass es un método muy misterioso en ClassLoader. Este método crea una instancia de clase a partir de una matriz de bytes. Esta matriz de bytes sin formato que contiene datos puede provenir del sistema de archivos o de la red. defineClass ilustra la complejidad, el misterio y la dependencia de la plataforma de la máquina virtual Java: interpreta el código de bytes para convertirlo en estructuras de datos en tiempo de ejecución, verifica su validez y más. Pero no te preocupes, no tienes que hacer nada de esto. En realidad, no puedes anularlo en absoluto,
Porque el método es modificado por la palabra clave final.
MétodofindSystemClass
El método findSystemClass carga archivos desde el sistema local. Busca archivos de clase en el sistema local y, si los encuentra, llama
defineClass convierte la matriz de bytes original en un objeto de clase. Este es el mecanismo predeterminado para que la máquina virtual Java cargue clases cuando ejecuta aplicaciones Java. Para cargadores de clases personalizados, solo necesitamos usar findSystemClass después de que falla la carga. La razón es simple: nuestro cargador de clases es responsable de realizar ciertos pasos en la carga de clases, pero no de todas las clases. Por ejemplo,
Incluso si nuestro cargador de clases carga algunas clases desde el sitio remoto, todavía hay muchas clases básicas que deben cargarse desde el sistema local.
Estas clases no nos preocupan, por lo que dejamos que la máquina virtual Java las cargue de la forma predeterminada: desde el sistema local. Esto es lo que hace findSystemClass. Todo el proceso es aproximadamente el siguiente:
* La máquina virtual Java solicita a nuestro cargador de clases personalizado que cargue la clase.
* Comprobamos si el sitio remoto tiene la clase que necesita cargarse.
* Si lo hay, obtenemos esta clase.
* Si no, pensamos que esta clase está en la biblioteca de clases básica y llamamos a findSystemClass para cargarla desde el sistema de archivos.
En la mayoría de los cargadores de clases personalizados, primero debe llamar a findSystemClass para ahorrar tiempo buscando desde el control remoto.
De hecho, como veremos en la siguiente sección, la máquina virtual Java sólo puede cargar clases desde el sistema de archivos local cuando estamos seguros de haber compilado nuestro código automáticamente.
Método resolverClase
Como se mencionó anteriormente, los registros de clase se pueden dividir en carga parcial (sin análisis) y carga completa (incluido el análisis). Cuando creamos un cargador de clases personalizado, es posible que necesitemos llamar a resolveClass.
MétodofindLoadedClass
findLoadedClass implementa un caché: cuando se requiere loadClass para cargar una clase, primero puede llamar a este método para ver si la clase se ha cargado para evitar volver a cargar una clase ya cargada. Primero se debe llamar a este método. Veamos cómo se organizan estos métodos juntos.
Nuestra implementación de ejemplo de loadClass realiza los siguientes pasos. (No especificamos una tecnología específica para obtener el archivo de clase; puede ser de la red, de un paquete comprimido o compilado dinámicamente. En cualquier caso, lo que obtenemos es el archivo de código de bytes original)
* Llame a findLoadedClass para comprobar si esta clase se ha cargado.
* Si no está cargado, obtenemos de alguna manera la matriz de bytes original.
* Si se ha obtenido la matriz, llame a defineClass para convertirla en un objeto de clase.
* Si no se puede obtener la matriz de bytes original, llame a findSystemClass para verificar si se puede registrar desde el sistema de archivos local.
* Si el parámetro resolver es verdadero, llame a resolveClass para resolver el objeto de clase.
* Si no se ha encontrado la clase, lanza una excepción ClassNotFoundException.
* En caso contrario, devuelve esta clase.
Ahora que tenemos una comprensión más completa del conocimiento práctico de los cargadores de clases, podemos crear un cargador de clases personalizado. En la siguiente sección, discutiremos CCL.
Parte 4. Compilación de ClassLoader
CCL nos muestra la función del cargador de clases. El propósito de CCL es permitir que nuestro código se compile y actualice automáticamente. Así es como funciona:
* Cuando hay una solicitud de una clase, primero verifique si el archivo de clase existe en el directorio y subdirectorios actuales del disco.
* Si no hay un archivo de clase, pero hay un archivo de código fuente, llame al compilador de Java para compilar y generar el archivo de clase.
* Si el archivo de clase ya existe, verifique si el archivo de clase es más antiguo que el archivo de código fuente. Si el archivo de clase es más antiguo que el archivo de código fuente, llame al compilador de Java para regenerar el archivo de clase.
* Si la compilación falla, o el archivo de clase no se puede generar a partir del archivo fuente por otros motivos, lanza la excepción ClassNotFou
ndExcepción.
* Si aún no ha obtenido esta clase, es posible que exista en otras bibliotecas de clases. Llame a findSystemClass para ver si se puede encontrar.
* Si no se encuentra, lanza ClassNotFoundException.
* En caso contrario, devuelve esta clase.
¿Cómo se implementa la compilación de Java?
Antes de continuar, debemos comprender el proceso de compilación de Java. Normalmente, el compilador de Java compila sólo aquellas clases especificadas. También compilará otras clases relacionadas si así lo requieren las clases especificadas. CCL compilará las clases que necesitamos compilar en la aplicación una por una. Sin embargo, en términos generales, después de que el compilador compila la primera clase,
CCL encontrará que otras clases relacionadas requeridas realmente se han compilado. ¿Por qué? El compilador de Java utiliza reglas similares a las nuestras: si una clase no existe o el archivo fuente se ha actualizado, la clase se compilará. El compilador de Java está básicamente un paso por delante de CCL y la mayor parte del trabajo lo realiza el compilador de Java. Parece que CCL está compilando estas clases.
En la mayoría de los casos, encontrará que está llamando al compilador en la clase de función principal, y eso es todo: una simple llamada es suficiente. Sin embargo, existe un caso especial en el que estas clases no se compilan la primera vez que aparecen. Si carga una clase según su nombre, utilizando el método Class.forName, el compilador de Java no sabe si la clase es necesaria. en este caso,
Encuentra que CCL vuelve a llamar al compilador para compilar la clase. El código de la Sección 6 ilustra este proceso.
Usando CompilationClassLoader
Para usar CCL, no podemos ejecutar nuestro programa directamente, debe ejecutarse de una manera especial, así:
% java Foo arg1 arg2
Lo ejecutamos así:
% java CCLRun Foo arg1 arg2
CCLRun es un programa auxiliar especial que crea CompilingClassLoader y lo usa para cargar nuestra clase de función principal. Esto garantiza que CompilingClassLoader cargue todo el programa. CCLRun utiliza Ja
La API de reflexión va llama a la función principal de la clase de función principal y pasa parámetros a esta función. Para obtener más información, consulte el código fuente en la Parte 6.
Ejecutemos el ejemplo para demostrar cómo funciona todo el proceso.
El programa principal es una clase llamada Foo, que crea una instancia de la clase Bar. Esta instancia de Bar, a su vez, crea una instancia de la clase Baz, que existe en el paquete baz. Esto es para demostrar cómo CCL carga clases desde subpaquetes. La barra también carga la clase Boo según el nombre de la clase.
, esto también lo hace CCL. Todas las clases están cargadas y listas para ejecutarse. Utilice el código fuente del Capítulo 6 para ejecutar este programa. Compile CCLRun y CompilingClassLoader. Asegúrate de no compilar otras clases (Foo, Bar, Baz, a
nd Boo), de lo contrario, CCL no funcionará.
% java CCLRun Foo arg1 arg2
CCL: Compilando Foo.java...
¡foo! arg1 arg2
barra! arg1 arg2
baz!arg1arg2
CCL: Compilando Boo.java...
¡Abucheo!
Observe que se llama al compilador por primera vez para Foo.java, y Bar y baz.Baz también se compilan juntos. y como boo
Cuando es necesario cargar el canal, CCL vuelve a llamar al compilador para compilarlo.
Parte 5. Descripción general de las mejoras del cargador de clases en Java 2 En Java 1.2 y versiones posteriores, el cargador de clases se ha mejorado enormemente. El código antiguo todavía funciona, pero el nuevo sistema facilita nuestra implementación. Este nuevo modelo es el modelo de delegación de proxy, lo que significa que si el cargador de clases no puede encontrar una clase, le pedirá a su cargador de clases principal que la encuentre. El cargador de clases del sistema es el antepasado de todos los cargadores de clases. El cargador de clases del sistema carga clases de forma predeterminada, es decir, desde el sistema de archivos local. Anular el método loadClass generalmente prueba varias formas de cargar la clase. Si escribe muchos cargadores de clases, encontrará que simplemente realiza algunas modificaciones en este complicado método una y otra vez. La implementación predeterminada de loadClass en Java 1.2 incluye la forma más común de encontrar una clase, lo que le permite anular el método findClass y loadClass para llamar al método findClass de forma adecuada. La ventaja de esto es que no es necesario anular loadClass, solo necesita anular findClass, lo que reduce la carga de trabajo.
Nuevo método: findClass
Este método lo llama la implementación predeterminada de loadClass. El objetivo de findClass es incluir todo el código específico del cargador de clases,
No es necesario repetir el código (como llamar al cargador de clases del sistema cuando falla el método especificado).
Nuevo método: getSystemClassLoader
Independientemente de si anula los métodos findClass y loadClass, el método getSystemClassLoader puede acceder directamente al cargador de clases del sistema (en lugar de acceso indirecto a través de findSystemClass).
Nuevo método: getParent
Para delegar la solicitud al cargador de clases principal, el cargador de clases principal de este cargador de clases se puede obtener a través de este método. Puede delegar la solicitud al cargador de clases principal cuando un método específico en un cargador de clases personalizado no puede encontrar la clase. El cargador de clases principal de un cargador de clases contiene el código que crea el cargador de clases.
Parte 6. Código fuente
CompilandoClassLoader.java
El siguiente es el contenido del archivo CompilingClassLoader.java
importar java.io.*;
/*
CompilingClassLoader compila dinámicamente archivos fuente Java. Comprueba si el archivo .class existe y si el archivo .class es más antiguo que el archivo fuente.
*/
clase pública CompilingClassLoader extiende ClassLoader
{
//Especifique un nombre de archivo, lea todo el contenido del archivo desde el disco y devuelva una matriz de bytes.
byte privado [] getBytes (nombre de archivo de cadena) lanza IOException {
// Obtener el tamaño del archivo.
Archivo archivo = nuevo archivo (nombre de archivo);
longitud larga = archivo.longitud();
//Crea una matriz suficiente para almacenar el contenido del archivo.
byte sin procesar[] = nuevo byte[(int)len];
//Abrir archivo
FileInputStream fin = nuevo FileInputStream (archivo);
// Leer todo el contenido. Si no se puede leer, se produjo un error.
int r = fin.read(sin procesar);
si (r != len)
lanzar nueva IOException( "No se puede leer todo, "+r+" != "+len );
// No olvides cerrar el archivo.
fin.cerrar();
// Devuelve esta matriz.
volver crudo;
}
// Genera un proceso para compilar el archivo fuente Java especificado y especifica los parámetros del archivo. Si la compilación es exitosa, devuelve verdadero; de lo contrario,
// Devuelve falso.
compilación booleana privada (String javaFile) lanza IOException {
//Mostrar el progreso actual
System.out.println( "CCL: Compilando "+javaFile+"..." );
//Inicia el compilador
Proceso p = Runtime.getRuntime().exec( "javac "+javaFile);
// Espera a que termine la compilación
intentar {
p.waitFor();
} catch( InterruptedException es decir) { System.out.println( es decir });
// Verifique el código de retorno para ver si hay errores de compilación.
int ret = p.exitValue();
//Devuelve si la compilación fue exitosa.
retorno ret==0;
}
// El código central del cargador de clases: la carga de clases compila automáticamente los archivos fuente cuando es necesario.
clase pública loadClass (nombre de cadena, resolución booleana)
lanza ClassNotFoundException {
//Nuestro propósito es obtener un objeto de clase.
Clase clase = nulo;
// Primero, verifica si esta clase ha sido procesada.
clas = findLoadedClass( nombre );
//System.out.println( "findLoadedClass: "+clas);
// Obtenga el nombre de la ruta a través del nombre de la clase, por ejemplo: java.lang.Object => java/lang/Object
String fileStub = nombre.replace( ''''.'''', ''''/'''' );
// Construir objetos que apunten a archivos fuente y archivos de clase.
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
Archivo javaFile = nuevo archivo (javaFilename);
Archivo archivo de clase = nuevo archivo (nombre de archivo de clase);
//System.out.println( "j "+javaFile.lastModified()+" c "
//+classFile.lastModified() );
// Primero, determine si se requiere compilación. Si el archivo fuente existe pero el archivo de clase no existe, o ambos existen pero el archivo fuente
// Más reciente, lo que indica que es necesario compilarlo.
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
intentar {
// Compile, si la compilación falla, debemos declarar el motivo del error (no basta con usar clases obsoletas).
if (!compile( javaFilename ) || !classFile.exists()) {
lanzar nueva ClassNotFoundException( "Error al compilar: "+javaFilename);
}
} catch(IOException es decir) {
// Puede ocurrir un error de IO durante la compilación.
lanzar una nueva ClassNotFoundException (es decir, toString());
}
}
//Aseguramos que se haya compilado correctamente o que no requiera compilación, comenzamos a cargar bytes sin formato.
intentar {
// Leer bytes.
byte sin formato [] = getBytes (nombre de archivo de clase);
//Convertir en objeto de clase
clas = defineClass( nombre, raw, 0, raw.length );
} catch(IOException es decir) {
// Esto no significa falla, tal vez la clase con la que estamos tratando esté en la biblioteca de clases local, como java.lang.Object.
}
//System.out.println( "defineClass: "+clas );
//Quizás en la biblioteca de clases, cargado de la forma predeterminada.
si (clas==nulo) {
clas = encontrarSystemClass( nombre );
}
//System.out.println( "findSystemClass: "+clas);
// Si el parámetro resolver es verdadero, interpreta la clase según sea necesario.
si (resolver && clas! = nulo)
resolverClass(clase);
// Si no se ha obtenido la clase, algo salió mal.
si (clase == nula)
lanzar una nueva ClassNotFoundException (nombre);
// De lo contrario, devuelve este objeto de clase.
clase de retorno;
}
}
CCRun.java
Aquí está el archivo CCRun.java
importar java.lang.reflect.*;
/*
CCLRun carga clases a través de CompilingClassLoader para ejecutar el programa.
*/
CCLRun de clase pública
{
static public void main (String args []) lanza una excepción {
//El primer parámetro especifica la clase de función principal que el usuario desea ejecutar.
Cadena progClass = args[0];
// Los siguientes parámetros son los parámetros pasados a esta clase de función principal.
String progArgs[] = new String[args.length-1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
// Crear cargador de clases de compilación
CompilingClassLoader ccl = nuevo CompilingClassLoader();
//Carga la clase de función principal a través de CCL.
Clase clas = ccl.loadClass( progClass );
// Utilice la reflexión para llamar a su función principal y pasar parámetros.
// Genera un objeto de clase que representa el tipo de parámetro de la función principal.
Clase mainArgType[] = { (nueva cadena[0]).getClass() };
// Encuentra la función principal estándar en la clase.
Método principal = clas.getMethod( "principal", mainArgType );
// Crea una lista de parámetros; en este caso, una matriz de cadenas.
Objeto argsArray[] = {progArgs};
// Llama a la función principal.
main.invoke (nulo, argsArray);
}
}
foo.java
El siguiente es el contenido del archivo Foo.java
clase pública foo
{
static public void main (String args []) lanza una excepción {
System.out.println( "foo! "+args[0]+" "+args[1] );
nueva barra (argumentos[0], argumentos[1]);
}
}
barra.java
El siguiente es el contenido del archivo Bar.java
importar baz.*;
bar de clase pública
{
Barra pública (cadena a, cadena b) {
System.out.println( "barra! "+a+" "+b );
nuevo Baz(a,b);
intentar {
Clase booClass = Class.forName( "Boo" );
Objeto boo = booClass.newInstance();
} captura (Excepción e) {
e.printStackTrace();
}
}
}
baz/Baz.java
El siguiente es el contenido del archivo baz/Baz.java
paquete baz;
clase publica baz
{
Baz público (cadena a, cadena b) {
System.out.println( "baz! "+a+" "+b );
}
}
boo.java
El siguiente es el contenido del archivo Boo.java
clase pública abucheo
{
públicoBoo() {
System.out.println ("¡Abucheo!");
}
}
Parte 7. Resumen Resumen Después de leer este artículo, ¿se dio cuenta de que crear un cargador de clases personalizado le permite profundizar en los aspectos internos de la máquina virtual Java? Puede cargar un archivo de clase desde cualquier recurso o generarlo dinámicamente, de modo que pueda hacer muchas cosas que le interesan al extender estas funciones, y también puede completar algunas funciones poderosas.
Otros temas sobre ClassLoader Como se mencionó al principio de este artículo, los cargadores de clases personalizados desempeñan un papel importante en los navegadores integrados de Java y en los navegadores de subprogramas.