Los conceptos básicos de los cargadores de clases de Java

El concepto de cargador de clases, una de las piedras angulares de la máquina virtual Java, describe el comportamiento de convertir una clase con nombre en los bits responsables de implementar esa clase. Debido a que existen cargadores de clases, el tiempo de ejecución de Java no necesita saber nada sobre archivos y sistemas de archivos cuando se ejecutan programas Java.

Que hacen los cargadores de clases

Las clases se introducen en el entorno Java cuando se hace referencia a ellas por su nombre en una clase que ya se está ejecutando. Hay un poco de magia que continúa para que la primera clase se ejecute (por lo que debe declarar el método main () como estático, tomando una matriz de cadenas como argumento), pero una vez que esa clase se está ejecutando, los intentos futuros de las clases de carga las realiza el cargador de clases.

En su forma más simple, un cargador de clases crea un espacio de nombres plano de cuerpos de clase que son referenciados por un nombre de cadena. La definición del método es:

Clase r = loadClass (String className, boolean resolveIt); 

La variable className contiene una cadena que el cargador de clases entiende y se utiliza para identificar de forma única una implementación de clase. La variable resolveIt es una bandera para decirle al cargador de clases que las clases referenciadas por este nombre de clase deben resolverse (es decir, cualquier clase referenciada también debe cargarse).

Todas las máquinas virtuales Java incluyen un cargador de clases integrado en la máquina virtual. Este cargador integrado se denomina cargador de clases primordial. Es algo especial porque la máquina virtual asume que tiene acceso a un repositorio de clases confiables que la VM puede ejecutar sin verificación.

El cargador de clases primordial implementa la implementación predeterminada de loadClass () . Por lo tanto, este código comprende que el nombre de la clase java.lang.Object se almacena en un archivo con el prefijo java / lang / Object.class en algún lugar de la ruta de la clase. Este código también implementa la búsqueda de rutas de clases y la búsqueda de clases en archivos zip. Lo realmente interesante de la forma en que está diseñado es que Java puede cambiar su modelo de almacenamiento de clases simplemente cambiando el conjunto de funciones que implementa el cargador de clases.

Indagando en las entrañas de la máquina virtual Java, descubrirá que el cargador de clases primordial se implementa principalmente en las funciones FindClassFromClass y ResolveClass .

Entonces, ¿cuándo se cargan las clases? Hay exactamente dos casos: cuando se ejecuta el nuevo bytecode (por ejemplo, FooClass f = new FooClass () ;) y cuando los bytecodes hacen una referencia estática a una clase (por ejemplo, System. Out ).

Un cargador de clases no primordial

"¿Y qué?" podría preguntar.

La máquina virtual Java tiene ganchos para permitir que se use un cargador de clases definido por el usuario en lugar del primordial. Además, dado que el cargador de clases de usuario obtiene el primer crack en el nombre de la clase, el usuario puede implementar cualquier cantidad de repositorios de clases interesantes, entre los que se encuentran los servidores HTTP, lo que hizo que Java despegara en primer lugar.

Sin embargo, hay un costo debido a que el cargador de clases es tan poderoso (por ejemplo, puede reemplazar java.lang.Object con su propia versión), las clases de Java como los applets no pueden crear instancias de sus propios cargadores. (Por cierto, esto lo aplica el cargador de clases). Esta columna no será útil si está intentando hacer estas cosas con un applet, solo con una aplicación que se ejecuta desde el repositorio de clases de confianza (como archivos locales).

Un cargador de clases de usuario tiene la oportunidad de cargar una clase antes de que lo haga el cargador de clases primordial. Debido a esto, puede cargar los datos de implementación de clases desde alguna fuente alternativa, que es como AppletClassLoader puede cargar clases usando el protocolo HTTP.

Construyendo un SimpleClassLoader

Un cargador de clases comienza siendo una subclase de java.lang.ClassLoader . El único método abstracto que debe implementarse es loadClass () . El flujo de loadClass () es el siguiente:

  • Verifique el nombre de la clase.
  • Verifique si la clase solicitada ya se ha cargado.
  • Compruebe si la clase es una clase de "sistema".
  • Intente recuperar la clase del repositorio de este cargador de clases.
  • Defina la clase para la VM.
  • Resuelve la clase.
  • Devuelva la clase a la persona que llama.

SimpleClassLoader aparece de la siguiente manera, con descripciones sobre lo que hace intercaladas con el código.

loadClass pública sincronizada Class (String className, boolean resolveIt) lanza ClassNotFoundException {Class result; byte classData []; System.out.println (">>>>>> Cargar clase:" + className); / * Consulta nuestro caché local de clases * / result = (Class) classes.get (className); if (result! = null) {System.out.println (">>>>>> devolviendo el resultado en caché."); devolver resultado; }

El código anterior es la primera sección del método loadClass . Como puede ver, toma un nombre de clase y busca una tabla hash local que nuestro cargador de clases mantiene de las clases que ya ha devuelto. Es importante mantener esta tabla hash, ya que debe devolver la misma referencia de objeto de clase para el mismo nombre de clase cada vez que se le solicite. De lo contrario, el sistema creerá que hay dos clases diferentes con el mismo nombre y lanzará una ClassCastException cada vez que asigne una referencia de objeto entre ellas. También es importante mantener un caché porque loadClass () El método se llama de forma recursiva cuando se está resolviendo una clase, y deberá devolver el resultado almacenado en caché en lugar de buscarlo para otra copia.

/ * Consultar con el cargador de clases primordial * / try {result = super.findSystemClass (className); System.out.println (">>>>>> devolviendo la clase del sistema (en CLASSPATH)."); devolver resultado; } catch (ClassNotFoundException e) {System.out.println (">>>>>> No es una clase de sistema."); }

Como puede ver en el código anterior, el siguiente paso es verificar si el cargador de clases primordial puede resolver este nombre de clase. Esta verificación es esencial tanto para la cordura como para la seguridad del sistema. Por ejemplo, si devuelve su propia instancia de java.lang.Object al llamador, entonces este objeto no compartirá una superclase común con ningún otro objeto. La seguridad del sistema puede verse comprometida si su cargador de clases devolvió su propio valor de java.lang.SecurityManager , que no tenía las mismas comprobaciones que el real.

/ * Intenta cargarlo desde nuestro repositorio * / classData = getClassImplFromDataBase (className); if (classData == null) {lanzar nueva ClassNotFoundException (); }

Después de las comprobaciones iniciales, llegamos al código anterior, que es donde el cargador de clases simple tiene la oportunidad de cargar una implementación de esta clase. El SimpleClassLoader tiene un método getClassImplFromDataBase () , que en nuestro ejemplo sencillo simplemente prefija el directorio "tienda \" al nombre de la clase y agrega la extensión ".impl". Elegí esta técnica en el ejemplo para que no hubiera duda de que el cargador de clases primordial encontrara nuestra clase. Tenga en cuenta que sun.applet.AppletClassLoader antepone la URL de la base de código de la página HTML donde reside un subprograma con el nombre y luego realiza una solicitud HTTP para obtener los códigos de bytes.

 / * Definirlo (analizar el archivo de clase) * / result = defineClass (classData, 0, classData.length); 

Si se cargó la implementación de la clase, el penúltimo paso es llamar al método defineClass () desde java.lang.ClassLoader , que puede considerarse el primer paso de la verificación de la clase. Este método se implementa en la máquina virtual Java y es responsable de verificar que los bytes de clase sean un archivo de clase Java legal. Internamente, el método defineClass completa una estructura de datos que la JVM usa para contener clases. Si los datos de la clase están mal formados, esta llamada provocará que se lance un ClassFormatError .

if (resolveIt) {resolveClass (resultado); }

The last class loader-specific requirement is to call resolveClass() if the boolean parameter resolveIt was true. This method does two things: First, it causes any classes that are referenced by this class explicitly to be loaded and a prototype object for this class to be created; then, it invokes the verifier to do dynamic verification of the legitimacy of the bytecodes in this class. If verification fails, this method call will throw a LinkageError, the most common of which is a VerifyError.

Note that for any class you will load, the resolveIt variable will always be true. It is only when the system is recursively calling loadClass() that it may set this variable false because it knows the class it is asking for is already resolved.

 classes.put(className, result); System.out.println(" >>>>>> Returning newly loaded class."); return result; } 

The final step in the process is to store the class we've loaded and resolved into our hash table so that we can return it again if need be, and then to return the Class reference to the caller.

Of course if it were this simple there wouldn't be much more to talk about. In fact, there are two issues that class loader builders will have to deal with, security and talking to classes loaded by the custom class loader.

Security considerations

Whenever you have an application loading arbitrary classes into the system through your class loader, your application's integrity is at risk. This is due to the power of the class loader. Let's take a moment to look at one of the ways a potential villain could break into your application if you aren't careful.

In our simple class loader, if the primordial class loader couldn't find the class, we loaded it from our private repository. What happens when that repository contains the class java.lang.FooBar ? There is no class named java.lang.FooBar, but we could install one by loading it from the class repository. This class, by virtue of the fact that it would have access to any package-protected variable in the java.lang package, can manipulate some sensitive variables so that later classes could subvert security measures. Therefore, one of the jobs of any class loader is to protect the system name space.

In our simple class loader we can add the code:

 if (className.startsWith("java.")) throw newClassNotFoundException(); 

just after the call to findSystemClass above. This technique can be used to protect any package where you are sure that the loaded code will never have a reason to load a new class into some package.

Another area of risk is that the name passed must be a verified valid name. Consider a hostile application that used a class name of "..\..\..\..\netscape\temp\xxx.class" as its class name that it wanted loaded. Clearly, if the class loader simply presented this name to our simplistic file system loader this might load a class that actually wasn't expected by our application. Thus, before searching our own repository of classes, it is a good idea to write a method that verifies the integrity of your class names. Then call that method just before you go to search your repository.

Using an interface to bridge the gap

The second non-intuitive issue with working with class loaders is the inability to cast an object that was created from a loaded class into its original class. You need to cast the object returned because the typical use of a custom class loader is something like:

 CustomClassLoader ccl = new CustomClassLoader(); Object o; Class c; c = ccl.loadClass("someNewClass"); o = c.newInstance(); ((SomeNewClass)o).someClassMethod(); 

However, you cannot cast o to SomeNewClass because only the custom class loader "knows" about the new class it has just loaded.

There are two reasons for this. First, the classes in the Java virtual machine are considered castable if they have at least one common class pointer. However, classes loaded by two different class loaders will have two different class pointers and no classes in common (except java.lang.Object usually). Second, the idea behind having a custom class loader is to load classes after the application is deployed so the application does not know a priory about the classes it will load. This dilemma is solved by giving both the application and the loaded class a class in common.

There are two ways of creating this common class, either the loaded class must be a subclass of a class that the application has loaded from its trusted repository, or the loaded class must implement an interface that was loaded from the trusted repository. This way the loaded class and the class that does not share the complete name space of the custom class loader have a class in common. In the example I use an interface named LocalModule, although you could just as easily make this a class and subclass it.

El mejor ejemplo de la primera técnica es un navegador web. La clase definida por Java que es implementada por todos los applets es java.applet.Applet . Cuando AppletClassLoader carga una clase , la instancia de objeto que se crea se convierte en una instancia de Applet . Si esta conversión tiene éxito , se llama al método init () . En mi ejemplo utilizo la segunda técnica, una interfaz.

Jugando con el ejemplo

Para completar el ejemplo, he creado un par más

.Java

archivos. Estos son:

public interface LocalModule {/ * Iniciar el módulo * / void start (opción String); }