Jetez un œil à l'intérieur des classes Java

Bienvenue dans la tranche de ce mois-ci de "Java In Depth". L'un des premiers défis pour Java était de savoir s'il pouvait ou non se présenter comme un langage «système» capable. La racine de la question concernait les fonctionnalités de sécurité de Java qui empêchent une classe Java de connaître d'autres classes qui s'exécutent à côté d'elle dans la machine virtuelle. Cette capacité à «regarder à l'intérieur» des classes est appelée introspection . Dans la première version publique de Java, connue sous le nom d'Alpha3, les règles de langage strictes concernant la visibilité des composants internes d'une classe pouvaient être contournées par l'utilisation de la ObjectScopeclasse. Puis, pendant la version bêta, quand il a ObjectScopeété retiré du runtime pour des raisons de sécurité, de nombreuses personnes ont déclaré Java impropre au développement "sérieux".

Pourquoi l'introspection est-elle nécessaire pour qu'une langue soit considérée comme une langue «systémique»? Une partie de la réponse est assez banale: passer de «rien» (c'est-à-dire une VM non initialisée) à «quelque chose» (c'est-à-dire une classe Java en cours d'exécution) nécessite qu'une partie du système puisse inspecter les classes pour être courir pour savoir quoi faire avec eux. L'exemple canonique de ce problème est simplement le suivant: «Comment un programme, écrit dans un langage qui ne peut pas regarder« à l'intérieur »d'un autre composant de langage, commence-t-il à exécuter le premier composant de langage, qui est le point de départ de l'exécution pour tous les autres composants? "

Il existe deux façons de gérer l'introspection en Java: l'inspection des fichiers de classe et la nouvelle API de réflexion qui fait partie de Java 1.1.x. Je couvrirai les deux techniques, mais dans cette colonne je me concentrerai sur l'inspection de dossier de première classe. Dans une prochaine colonne, j'examinerai comment l'API de réflexion résout ce problème. (Des liens vers le code source complet de cette colonne sont disponibles dans la section Ressources.)

Regardez profondément dans mes fichiers ...

Dans les versions 1.0.x de Java, l'une des plus grandes verrues sur l'exécution de Java est la façon dont l'exécutable Java démarre un programme. Quel est le problème? L'exécution transite du domaine du système d'exploitation hôte (Win 95, SunOS, etc.) vers le domaine de la machine virtuelle Java. Taper la ligne " java MyClass arg1 arg2" met en mouvement une série d'événements qui sont complètement codés en dur par l'interpréteur Java.

Comme premier événement, le shell de commande du système d'exploitation charge l'interpréteur Java et lui transmet la chaîne "MyClass arg1 arg2" comme argument. L'événement suivant se produit lorsque l'interpréteur Java tente de localiser une classe nommée MyClassdans l'un des répertoires identifiés dans le chemin de classe. Si la classe est trouvée, le troisième événement est de localiser une méthode à l'intérieur de la classe nommée main, dont la signature a les modificateurs "public" et "static" et qui prend un tableau d' Stringobjets comme argument. Si cette méthode est trouvée, un thread primordial est construit et la méthode est appelée. L'interpréteur Java convertit ensuite "arg1 arg2" en un tableau de chaînes. Une fois cette méthode invoquée, tout le reste est purement Java.

C'est bien beau, sauf que la mainméthode doit être statique car le runtime ne peut pas l'appeler avec un environnement Java qui n'existe pas encore. De plus, la première méthode doit être nommée maincar il n'y a aucun moyen de dire à l'interpréteur le nom de la méthode sur la ligne de commande. Même si vous avez dit à l'interpréteur le nom de la méthode, il n'y a pas de moyen général de savoir si elle était dans la classe que vous aviez nommée en premier lieu. Enfin, comme la mainméthode est statique, vous ne pouvez pas la déclarer dans une interface, ce qui signifie que vous ne pouvez pas spécifier une interface comme celle-ci:

interface publique Application {public void main (String args []); }

Si l'interface ci-dessus a été définie et que les classes l'ont implémentée, vous pouvez au moins utiliser l' instanceofopérateur en Java pour déterminer si vous avez une application ou non et ainsi déterminer si elle convient ou non à l'appel à partir de la ligne de commande. L'essentiel est que vous ne pouvez pas (définir l'interface), ce n'était pas le cas (intégré à l'interpréteur Java), et donc vous ne pouvez pas (déterminer si un fichier de classe est une application facilement). Alors, qu'est-ce que tu peux faire?

En fait, vous pouvez faire pas mal de choses si vous savez quoi rechercher et comment l'utiliser.

Décompilation des fichiers de classe

Le fichier de classe Java est indépendant de l'architecture, ce qui signifie qu'il s'agit du même ensemble de bits, qu'il soit chargé à partir d'une machine Windows 95 ou d'une machine Sun Solaris. Il est également très bien documenté dans le livre The Java Virtual Machine Specification de Lindholm et Yellin. La structure du fichier de classe a été conçue, en partie, pour être facilement chargée dans l'espace d'adressage SPARC. Fondamentalement, le fichier de classe pourrait être mappé dans l'espace d'adressage virtuel, puis les pointeurs relatifs à l'intérieur de la classe corrigés, et hop! Vous aviez une structure de classe instantanée. Cela était moins utile sur les machines à architecture Intel, mais l'héritage laissait le format de fichier de classe facile à comprendre et encore plus facile à décomposer.

À l'été 1994, je travaillais dans le groupe Java et je construisais ce que l'on appelle un modèle de sécurité du «moindre privilège» pour Java. Je venais de finir de comprendre que ce que je voulais vraiment faire était de regarder à l'intérieur d'une classe Java, de supprimer les éléments qui n'étaient pas autorisés par le niveau de privilège actuel, puis de charger le résultat via un chargeur de classe personnalisé. C'est alors que j'ai découvert qu'il n'y avait aucune classe dans le runtime principal qui connaissait la construction des fichiers de classe. Il y avait des versions dans l'arborescence des classes du compilateur (qui devait générer des fichiers de classe à partir du code compilé), mais j'étais plus intéressé par la construction de quelque chose pour manipuler des fichiers de classe préexistants.

J'ai commencé par construire une classe Java qui pouvait décomposer un fichier de classe Java qui lui était présenté sur un flux d'entrée. Je lui ai donné le nom moins qu'original ClassFile. Le début de ce cours est indiqué ci-dessous.

classe publique ClassFile {int magic; short majorVersion; short minorVersion; ConstantPoolInfo constantPool []; short accessFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; Interfaces ConstantPoolInfo []; Champs FieldInfo []; Méthodes MethodInfo []; AttributeInfo attributs []; booléen isValidClass = false; public static final int ACC_PUBLIC = 0x1; public static final int ACC_PRIVATE = 0x2; public static final int ACC_PROTECTED = 0x4; public static final int ACC_STATIC = 0x8; public static final int ACC_FINAL = 0x10; public static final int ACC_SYNCHRONIZED = 0x20; public static final int ACC_THREADSAFE = 0x40; public static final int ACC_TRANSIENT = 0x80; public static final int ACC_NATIVE = 0x100; public static final int ACC_INTERFACE = 0x200; public static final int ACC_ABSTRACT = 0x400;

Comme vous pouvez le voir, les variables d'instance de classe ClassFiledéfinissent les principaux composants d'un fichier de classe Java. En particulier, la structure de données centrale d'un fichier de classe Java est appelée pool de constantes. D'autres morceaux intéressants de fichier de classe obtiennent leurs propres classes: MethodInfopour les méthodes, FieldInfopour les champs (qui sont les déclarations de variables dans la classe), AttributeInfopour contenir les attributs de fichier de classe, et un ensemble de constantes qui a été pris directement de la spécification sur décoder les différents modificateurs qui s'appliquent aux déclarations de champ, de méthode et de classe.

La méthode principale de cette classe est read, qui est utilisée pour lire un fichier de classe à partir du disque et créer une nouvelle ClassFileinstance à partir des données. Le code de la readméthode est indiqué ci-dessous. J'ai intercalé la description avec le code car la méthode a tendance à être assez longue.

1 public boolean read(InputStream in) 2 throws IOException { 3 DataInputStream di = new DataInputStream(in); 4 int count; 5 6 magic = di.readInt(); 7 if (magic != (int) 0xCAFEBABE) { 8 return (false); 9 } 10 11 majorVersion = di.readShort(); 12 minorVersion = di.readShort(); 13 count = di.readShort(); 14 constantPool = new ConstantPoolInfo[count]; 15 if (debug) 16 System.out.println("read(): Read header..."); 17 constantPool[0] = new ConstantPoolInfo(); 18 for (int i = 1; i < constantPool.length; i++) { 19 constantPool[i] = new ConstantPoolInfo(); 20 if (! constantPool[i].read(di)) { 21 return (false); 22 } 23 // These two types take up "two" spots in the table 24 if ((constantPool[i].type == ConstantPoolInfo.LONG) || 25 (constantPool[i].type == ConstantPoolInfo.DOUBLE)) 26 i++; 27 } 

As you can see, the code above begins by first wrapping a DataInputStream around the input stream referenced by the variable in. Further, in lines 6 through 12, all of the information necessary to determine that the code is indeed looking at a valid class file is present. This information consists of the magic "cookie" 0xCAFEBABE, and the version numbers 45 and 3 for the major and minor values respectively. Next, in lines 13 through 27, the constant pool is read into an array of ConstantPoolInfo objects. The source code to ConstantPoolInfo is unremarkable -- it simply reads in data and identifies it based on its type. Later elements from the constant pool are used to display information about the class.

Following the above code, the read method re-scans the constant pool and "fixes up" references in the constant pool that refer to other items in the constant pool. The fix-up code is shown below. This fix-up is necessary since the references typically are indexes into the constant pool, and it is useful to have those indexes already resolved. This also provides a check for the reader to know that the class file isn't corrupt at the constant pool level.

28 for (int i = 1; i  0) 32 constantPool[i].arg1 = constantPool[constantPool[i].index1]; 33 if (constantPool[i].index2 > 0) 34 constantPool[i].arg2 = constantPool[constantPool[i].index2]; 35 } 36 37 if (dumpConstants) { 38 for (int i = 1; i < constantPool.length; i++) { 39 System.out.println("C"+i+" - "+constantPool[i]); 30 } 31 } 

In the above code each constant pool entry uses the index values to figure out the reference to another constant pool entry. When complete in line 36, the entire pool is optionally dumped out.

Once the code has scanned past the constant pool, the class file defines the primary class information: its class name, superclass name, and implementing interfaces. The read code scans for these values as shown below.

32 accessFlags = di.readShort(); 33 34 thisClass = constantPool[di.readShort()]; 35 superClass = constantPool[di.readShort()]; 36 if (debug) 37 System.out.println("read(): Read class info..."); 38 39 /* 30 * Identify all of the interfaces implemented by this class 31 */ 32 count = di.readShort(); 33 if (count != 0) { 34 if (debug) 35 System.out.println("Class implements "+count+" interfaces."); 36 interfaces = new ConstantPoolInfo[count]; 37 for (int i = 0; i < count; i++) { 38 int iindex = di.readShort(); 39 if ((iindex  constantPool.length - 1)) 40 return (false); 41 interfaces[i] = constantPool[iindex]; 42 if (debug) 43 System.out.println("I"+i+": "+interfaces[i]); 44 } 45 } 46 if (debug) 47 System.out.println("read(): Read interface info..."); 

Once this code is complete, the read method has built up a pretty good idea of the structure of the class. All that remains is to collect the field definitions, the method definitions, and, perhaps most importantly, the class file attributes.

The class file format breaks each of these three groups into a section consisting of a number, followed by that number of instances of the thing you are looking for. So, for fields, the class file has the number of defined fields, and then that many field definitions. The code to scan in the fields is shown below.

48 count = di.readShort(); 49 if (debug) 50 System.out.println("This class has "+count+" fields."); 51 if (count != 0) { 52 fields = new FieldInfo[count]; 53 for (int i = 0; i < count; i++) { 54 fields[i] = new FieldInfo(); 55 if (! fields[i].read(di, constantPool)) { 56 return (false); 57 } 58 if (debug) 59 System.out.println("F"+i+": "+ 60 fields[i].toString(constantPool)); 61 } 62 } 63 if (debug) 64 System.out.println("read(): Read field info..."); 

Le code ci-dessus commence par lire un compte à la ligne # 48, puis, bien que le compte soit différent de zéro, il lit de nouveaux champs en utilisant la FieldInfoclasse. La FieldInfoclasse remplit simplement les données qui définissent un champ pour la machine virtuelle Java. Le code pour lire les méthodes et les attributs est le même, en remplaçant simplement les références à FieldInfopar des références à MethodInfoou AttributeInfoselon le cas. Cette source n'est pas incluse ici, mais vous pouvez consulter la source en utilisant les liens dans la section Ressources ci-dessous.