Examinez en profondeur l'API Java Reflection

Dans "Java In-Depth" du mois dernier, j'ai parlé d'introspection et de la manière dont une classe Java ayant accès aux données de classe brutes pouvait regarder "à l'intérieur" d'une classe et comprendre comment la classe était construite. De plus, j'ai montré qu'avec l'ajout d'un chargeur de classe, ces classes pouvaient être chargées dans l'environnement en cours d'exécution et exécutées. Cet exemple est une forme d' introspection statique . Ce mois-ci, je vais jeter un œil à l'API Java Reflection, qui donne aux classes Java la possibilité d'effectuer une introspection dynamique : la possibilité de regarder à l'intérieur des classes déjà chargées.

L'utilité de l'introspection

L'une des forces de Java est qu'il a été conçu en supposant que l'environnement dans lequel il s'exécutait changerait de manière dynamique. Les classes sont chargées dynamiquement, la liaison est effectuée dynamiquement et les instances d'objet sont créées dynamiquement à la volée lorsqu'elles sont nécessaires. Ce qui n'a pas été très dynamique historiquement, c'est la capacité à manipuler des classes «anonymes». Dans ce contexte, une classe anonyme est une classe chargée ou présentée à une classe Java au moment de l'exécution et dont le type était auparavant inconnu du programme Java.

Cours anonymes

La prise en charge des classes anonymes est difficile à expliquer et encore plus difficile à concevoir dans un programme. Le défi de la prise en charge d'une classe anonyme peut être énoncé comme ceci: "Ecrire un programme qui, lorsqu'on lui donne un objet Java, peut incorporer cet objet dans son fonctionnement continu." La solution générale est assez difficile, mais en contraignant le problème, certaines solutions spécialisées peuvent être créées. Il existe deux exemples de solutions spécialisées à cette classe de problème dans la version 1.0 de Java: les applets Java et la version en ligne de commande de l'interpréteur Java.

Les applets Java sont des classes Java chargées par une machine virtuelle Java en cours d'exécution dans le contexte d'un navigateur Web et appelées. Ces classes Java sont anonymes car le moteur d'exécution ne connaît pas à l'avance les informations nécessaires pour appeler chaque classe individuelle. Cependant, le problème de l'appel d'une classe particulière est résolu en utilisant la classe Java java.applet.Applet.

Les superclasses courantes, comme Applet, et les interfaces Java, comme AppletContext, résolvent le problème des classes anonymes en créant un contrat préalablement convenu. Plus précisément, un fournisseur d'environnement d'exécution annonce qu'il peut utiliser n'importe quel objet conforme à une interface spécifiée, et le consommateur d'environnement d'exécution utilise cette interface spécifiée dans tout objet qu'il a l'intention de fournir à l'exécution. Dans le cas des applets, une interface bien spécifiée existe sous la forme d'une superclasse commune.

L'inconvénient d'une solution de superclasse commune, en particulier en l'absence d'héritage multiple, est que les objets créés pour s'exécuter dans l'environnement ne peuvent pas également être utilisés dans un autre système à moins que ce système n'implémente l'intégralité du contrat. Dans le cas des Appletinterfaces, l'environnement d'hébergement doit être implémenté AppletContext. Cela signifie pour la solution applet que la solution ne fonctionne que lorsque vous chargez des applets. Si vous placez une instance d'un Hashtableobjet sur votre page Web et pointez votre navigateur vers elle, le chargement échouera car le système d'applet ne peut pas fonctionner en dehors de sa plage limitée.

En plus de l'exemple d'applet, l'introspection permet de résoudre un problème que j'ai mentionné le mois dernier: trouver comment démarrer l'exécution dans une classe que la version en ligne de commande de la machine virtuelle Java vient de charger. Dans cet exemple, la machine virtuelle doit appeler une méthode statique dans la classe chargée. Par convention, cette méthode est nommée mainet prend un seul argument - un tableau d' Stringobjets.

La motivation pour une solution plus dynamique

Le défi avec l'architecture Java 1.0 existante est qu'il existe des problèmes qui pourraient être résolus par un environnement d'introspection plus dynamique - tels que des composants d'interface utilisateur chargeables, des pilotes de périphérique chargeables dans un système d'exploitation Java et des environnements d'édition configurables de manière dynamique. L '«application phare», ou le problème qui a provoqué la création de l'API Java Reflection, était le développement d'un modèle de composant objet pour Java. Ce modèle est maintenant connu sous le nom de JavaBeans.

Les composants d'interface utilisateur sont un point de conception idéal pour un système d'introspection car ils ont deux consommateurs très différents. D'une part, les objets composants sont liés entre eux pour former une interface utilisateur dans le cadre d'une application. Alternativement, il doit y avoir une interface pour les outils qui manipulent les composants utilisateur sans avoir à savoir quels sont les composants, ou, plus important encore, sans accès au code source des composants.

L'API Java Reflection est née des besoins de l'API du composant d'interface utilisateur JavaBeans.

Qu'est-ce que la réflexion?

Fondamentalement, l'API Reflection se compose de deux composants: des objets qui représentent les différentes parties d'un fichier de classe et un moyen pour extraire ces objets de manière sûre et sécurisée. Ce dernier est très important, car Java fournit de nombreuses sauvegardes de sécurité, et il ne serait pas logique de fournir un ensemble de classes qui invalident ces sauvegardes.

Le premier composant de l'API Reflection est le mécanisme utilisé pour récupérer des informations sur une classe. Ce mécanisme est intégré à la classe nommée Class. La classe spéciale Classest le type universel pour les méta-informations qui décrivent les objets dans le système Java. Les chargeurs de classe du système Java renvoient des objets de type Class. Jusqu'à présent, les trois méthodes les plus intéressantes de cette classe étaient:

  • forName, qui chargerait une classe d'un nom donné, en utilisant le chargeur de classe actuel

  • getName, qui renverrait le nom de la classe en tant Stringqu'objet, ce qui était utile pour identifier les références d'objet par leur nom de classe

  • newInstance, qui invoquerait le constructeur nul sur la classe (s'il existe) et vous renverrait une instance d'objet de cette classe d'objet

À ces trois méthodes utiles, l'API Reflection ajoute des méthodes supplémentaires à la classe Class. Ce sont les suivants:

  • getConstructor, getConstructors,getDeclaredConstructor
  • getMethod, getMethods,getDeclaredMethods
  • getField, getFields,getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

En plus de ces méthodes, de nombreuses nouvelles classes ont été ajoutées pour représenter les objets que ces méthodes renverraient. Les nouvelles classes sont pour la plupart partie du java.lang.reflectpaquet, mais quelques - unes des nouvelles classes de type de base ( Void, Byte, etc.) sont dans le java.langpaquet. La décision a été prise de placer les nouvelles classes là où elles se trouvent en plaçant des classes qui représentaient des méta-données dans le package de réflexion et des classes qui représentaient des types dans le package de langage.

Ainsi, l'API Reflection représente un certain nombre de modifications apportées à la classe Classqui vous permettent de poser des questions sur les éléments internes de la classe, et un ensemble de classes qui représentent les réponses que ces nouvelles méthodes vous donnent.

Comment utiliser l'API Reflection?

La question "Comment utiliser l'API?" est peut-être la question la plus intéressante que "Qu'est-ce que la réflexion?"

L'API Reflection est symétrique , ce qui signifie que si vous tenez un Classobjet, vous pouvez poser des questions sur ses internes, et si vous avez l'un des internes, vous pouvez lui demander quelle classe l'a déclaré. Ainsi, vous pouvez aller et venir de classe en méthode en paramètre en classe en méthode, et ainsi de suite. Une utilisation intéressante de cette technologie est de découvrir la plupart des interdépendances entre une classe donnée et le reste du système.

Un exemple de travail

Sur un plan plus pratique, cependant, vous pouvez utiliser l'API Reflection pour vider une classe, tout comme ma dumpclassclasse l'a fait dans la colonne du mois dernier.

To demonstrate the Reflection API, I wrote a class called ReflectClass that would take a class known to the Java run time (meaning it is in your class path somewhere) and, through the Reflection API, dump out its structure to the terminal window. To experiment with this class, you will need to have a 1.1 version of the JDK available.

Note: Do not try to use a 1.0 run time as it gets all confused, usually resulting in an incompatible class change exception.

The class ReflectClass begins as follows:

import java.lang.reflect.*; import java.util.*; public class ReflectClass { 

As you can see above, the first thing the code does is import the Reflection API classes. Next, it jumps right into the main method, which starts out as shown below.

 public static void main(String args[]) { Constructor cn[]; Class cc[]; Method mm[]; Field ff[]; Class c = null; Class supClass; String x, y, s1, s2, s3; Hashtable classRef = new Hashtable(); if (args.length == 0) { System.out.println("Please specify a class name on the command line."); System.exit(1); } try { c = Class.forName(args[0]); } catch (ClassNotFoundException ee) { System.out.println("Couldn't find class '"+args[0]+"'"); System.exit(1); } 

The method main declares arrays of constructors, fields, and methods. If you recall, these are three of the four fundamental parts of the class file. The fourth part is the attributes, which the Reflection API unfortunately does not give you access to. After the arrays, I've done some command-line processing. If the user has typed a class name, the code attempts to load it using the forName method of class Class. The forName method takes Java class names, not file names, so to look inside the java.math.BigInteger class, you simply type "java ReflectClass java.math.BigInteger," rather than point out where the class file actually is stored.

Identifying the class's package

Assuming the class file is found, the code proceeds into Step 0, which is shown below.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

 ff = c.getDeclaredFields(); for (int i = 0; i < ff.length; i++) { x = tName(ff[i].getType().getName(), classRef); } 

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

 cn = c.getDeclaredConstructors(); for (int i = 0; i  0) { for (int j = 0; j < cx.length; j++) { x = tName(cx[j].getName(), classRef); } } } 

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

La dernière étape, illustrée ci-dessous, consiste à collecter les références de toutes les méthodes. Ce code doit obtenir des références à la fois du type de la méthode (similaire aux champs ci-dessus) et des paramètres (similaires aux constructeurs ci-dessus).

mm = c.getDeclaredMethods (); for (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}}

Dans le code ci-dessus, il y a deux appels à tName- un pour collecter le type de retour et un pour collecter le type de chaque paramètre.