Programmation Java/Réflexion
La réflexion (reflection en anglais) permet l'introspection des classes, c'est-à-dire de charger une classe, d'en créer une instance et d'accéder aux membres statiques ou non (appel de méthodes, lire et écrire les attributs) sans connaître la classe par avance.
Java possède une API permettant la réflexion. Elle sert notamment à gérer des extensions (plug-in en anglais) pour une application.
L'API de réflexion
modifierLe package java.lang
possède trois classes utilisées pour l'utilisation dynamique des classes :
java.lang.Class
- Cette classe permet d'accéder aux caractéristiques d'une classe, à ses membres (méthodes et attributs), à la classe mère.
java.lang.ClassLoader
- Cette classe permet de gérer le chargement de classe. Il existe des sous-classes dont notamment
java.net.URLClassLoader
permettant de charger des classes en les cherchant dans une liste d'URLs (donc de fichiers JAR et répertoires également, en convertissant le chemin du fichier ou répertoire en URL). java.lang.Package
- Cette classe permet d'accéder aux informations d'un package (informations de version, annotations, ...).
Les autres classes utiles sont définies dans le package java.lang.reflect
et permettent d'accéder aux détails d'une classe.
Les principales classes sont les suivantes :
java.lang.reflect.Constructor
- Référence à un constructeur d'une classe.
java.lang.reflect.Method
- Référence à une méthode d'une classe.
java.lang.reflect.Field
- Référence à un champ d'une classe.
java.lang.reflect.Modifier
- Attributs et méthodes statiques pour décoder les modificateurs des membres (public, private, protected, static, abstract, final, native, ...).
Les classes représentant des membres d'une classe (Constructor
, Method
, Field
) implémentent toutes l'interface java.lang.reflect.Member
comportant les méthodes suivantes :
Class getDeclaringClass()
- Retourne la classe définissant ce membre.
String getName()
- Retourne le nom.
int getModifiers()
- Retourne les modificateurs (public, protected, private, static, final, ...).
boolean isSynthetic()
- Teste si ce membre a été généré par le compilateur.
Charger une classe dynamiquement
modifierLa classe java.lang.Class
possède deux méthodes statiques pour obtenir une classe (après chargement si nécessaire) :
static Class forName(String name)
- Cette méthode équivaut à appeler la seconde méthode avec pour paramètres :
(name, true, this.getClass().getClassLoader())
. static Class forName(String name, boolean initialize, ClassLoader loader)
- Charge la classe dont le nom complet (incluant les packages) est spécifié, en utilisant l'instance du chargeur de classe fourni. Le paramètre
initialize
vauttrue
pour initialiser la classe (appeler le bloc d'initialisation statique), oufalse
pour ne pas l'initialiser.
Il est également possible d'obtenir une java.lang.Class
de manière statique :
- à partir d'un objet en appelant la méthode
getClass()
, - à partir d'une référence à la classe en utilisant le champ
class
.
Exemple :
package org.wikibooks.fr;
public class Exemple
{
String nom;
public String getNom()
{ return nom; }
}
...
Class c = Class.forName("org.wikibooks.fr.Exemple"); // sans référence statique à la classe
// ou
Class c = org.wikibooks.fr.Exemple.class; // référence statique à la classe
// ou
Class c = new Exemple().getClass(); // référence statique à la classe
Liste des membres d'une classe
modifierLes méthodes suivantes permettent de lister les membres d'une classe :
getConstructors()
- Cette méthode retourne un tableau de
java.lang.reflect.Constructor
contenant tous les constructeurs définis par la classe. getMethods()
- Cette méthode retourne un tableau de
java.lang.reflect.Method
contenant toutes les méthodes définies par la classe. getFields()
- Cette méthode retourne un tableau de
java.lang.reflect.Field
contenant tous les attributs définis dans la classe.
Les méthodes ci-dessus retournent les membres publics de la classe, comprenant également ceux hérités des classes mères. Il existe une variante "Declared" de ces méthodes retournant tous les membres (publics, protégés, privés) déclarés par la classe uniquement (les membres hérités sont exclus).
Au lieu de lister tous les membres, puis en rechercher un en particulier, il est possible d'utiliser les méthodes spécifiques de recherche d'un membre précis d'une classe (publics et hérités, ou bien "Declared" pour tous ceux déclarés par la classe seule) :
getConstructor(Class... parameterTypes)
getDeclaredConstructor(Class... parameterTypes)
- Cette méthode retourne le constructeur déclaré avec les paramètres dont les types sont spécifiés.
getMethod(String name, Class... parameterTypes)
getDeclaredMethod(String name, Class... parameterTypes)
- Cette méthode retourne la méthode portant de nom spécifié et déclarée avec les paramètres dont les types sont spécifiés.
getField(String name)
getDeclaredField(String name)
- Cette méthode retourne l'attribut portant de nom spécifié.
Les membres retournés par toutes ces méthodes peuvent être d'instance ou statiques.
La méthode getAnnotations()
retourne un tableau de java.lang.Annotation
contenant toutes les annotations associées à la classe.
Instancier une classe et appel à un constructeur
modifierLa méthode newInstance()
de la classe java.lang.Class
permet de créer une nouvelle instance de la classe, en appelant le constructeur sans paramètre de la classe (qui doit donc en posséder un) :
Class c = Class.forName("org.wikibooks.fr.Exemple");
Object o = c.newInstance(); // équivaut à new org.wikibooks.fr.Exemple();
Une classe comme celle ci-dessous peut ne pas avoir de constructeur sans paramètres :
package org.wikibooks.fr;
public class Livre
{
String titre;
int nb_pages;
public Livre(String titre, int nb_pages)
{
this.titre = titre;
this.nb_pages = nb_pages;
}
}
Dans ce cas, il faut d'abord obtenir le constructeur, puis l'appeler :
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);
Pour les versions de Java antérieures à 5.0 où l'auto-boxing n'existe pas, et où il faut explicitement utiliser des tableaux :
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(new Class[]{ String.class, Integer.TYPE }); // Obtenir le constructeur (String, int)
Object o = constr.newInstance(new Object{ "Programmation Java", Integer.valueOf(120) }); // -> new Livre("Programmation Java", 120);
Appel à une méthode
modifierL'appel à une méthode de la classe est basé sur le même principe que l'appel à un constructeur vu juste avant.
Cependant, pour obtenir la référence à une méthode, il faut spécifier le nom.
Lors de l'invocation de la méthode, il faut spécifier l'instance (l'objet) auquel s'applique la méthode (null
pour une méthode statique).
Exemple :
package org.wikibooks.fr;
public class Livre
{
String titre;
int nb_pages;
public Livre(String _titre, int _nb_pages) {
this.titre = _titre;
this.nb_pages = _nb_pages;
}
public int getNombreDeFeuilles(int pages_par_feuille)
{
return (nb_pages+pages_par_feuille-1)/pages_par_feuille;
}
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);
Method method = c.getMethod("getNombreDeFeuilles", int.class); // Obtenir la méthode getNombreDeFeuilles(int)
int nb_feuilles = (int)method.invoke(o, 2); // -> o.getNombreDeFeuilles(2);
Accès à un attribut public
modifierL'accès à un attribut public se fait en appelant les méthodes sur l'instance de java.lang.reflect.Field
obtenu auprès de la classe.
Exemple :
package org.wikibooks.fr;
public class Livre
{
public String titre;
public int nb_pages;
public Livre(String _titre, int _nb_pages) {
this.titre = _titre;
this.nb_pages = _nb_pages;
}
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);
Field f_titre = c.getField("titre"); // Obtenir l'attribut titre
String titre_du_livre = (String)f_titre.get(o); // -> o.titre
f_titre.set(o, "Java"); // -> o.titre = "Java";
Accès à un attribut privé
modifierIl est également possible d'avoir accès aux champs privés grâce à la méthode setAccessible de la classe Field. Cela est cependant fortement déconseillé puisque modifier les valeurs d'un champs privée revient à violer le principe d'encapsulation.
Exemple :
package org.wikibooks.fr;
public class Livre
{
private String titre;
private int nb_pages;
public Livre(String _titre, int _nb_pages) {
this.titre = _titre;
this.nb_pages = _nb_pages;
}
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);
Field f_titre = c.getField("titre"); // Erreur: titre est privé
Field fields[] = c.getDeclaredFields();
fields[0].setAccessible(true); // titre désormais équivalent à un attribut publique
Field f_titre = fields[0]; // Obtenir l'attribut titre
String titre_du_livre = (String)f_titre.get(o); // -> o.titre
f_titre.set(o, "Java"); // -> o.titre = "Java";
Exemple concret : un gestionnaire d'extensions
modifierL'extension d'une application Java peut se faire en utilisant la réflexion pour charger dynamiquement une classe. Le but de cet exemple est de permettre d'ajouter des fonctionnalités dynamiquement à une application, sans avoir à effectuer de changement autre que l'ajout d'une nouvelle archive JAR dans un répertoire.
Par défaut, une application est composée de plusieurs classes dont le chargement est effectué par la JVM.
Cependant, il faut que les chemins des répertoires de fichiers *.class
ou des archives *.jar
soit renseignés d'avance avant le lancement de l'application dans le classpath.
Chargeur de classes
modifierSupposons que l'application doive être capable de charger des extensions fournies sous forme de fichiers *.jar
situés dans un sous-répertoire nommé « ext ».
Quand l'application est lancée, le classpath est déjà configuré pour les classes de l'application, mais ne contient pas le répertoire ext.
Mais comme les classes ne sont pas directement dans le répertoire, la JVM a besoin du chemin de chaque fichier *.jar
dans le classpath.
L'application doit donc lister les fichiers *.jar
du répertoire ext pour construire un classpath passé au constructeur d'un nouveau chargeur de classes.
public static ClassLoader creerChargeurArchives(File dir) throws IOException
{
if (!dir.isDirectory()) throw new FileNotFoundException("Répertoire non trouvé");
File[] fichiers = dir.listFiles();
ArrayList<URL> urls_fichiers = new ArrayList<>();
// Ajouter les fichiers *.jar
for(File f : fichiers)
{
if (f.isFile() && f.getName().toLowerCase().endsWith(".jar"))
urls_fichiers.add(f.toURI().toURL());
}
// Créer le chargeur de classes pour les URLS de fichiers *.jar
return new URLClassLoader(urls_fichiers.toArray(new URL[urls_fichiers.size()]));
}
Il est possible de créer un chargeur de classes par fichier *.jar
.
Cependant, chaque chargeur de classe a un chargeur de classe parent, permettant aux classes chargées d'utiliser des classes gérées par le chargeur parent.
En l’occurrence il s'agit de celui des classes de l'application elle-même.
Cela signifie que les classes d'extensions peuvent utiliser les classes de l'application (classes de base, interfaces d'API, ...).
En utilisant un chargeur par fichier *.jar
, chaque extension ne peut donc pas utiliser les classes d'une autre extension.
En utilisant un seul chargeur pour tous les fichiers *.jar
, les extensions peuvent utiliser des classes d'autres extensions.
Cette dernière solution permet de créer des extensions utiles à d'autres extensions.
Toutefois l'inconvénient est que toutes les extensions sont chargées en mémoire même si elles ne sont pas utilisées. De plus, le nom complet des classes doit être unique.
Chargement des classes d'extension
modifierLe chargement des classes d'extension doit utiliser le chargeur de classes des fichiers *.jar
trouvés dans le répertoire ext.
Chaque extension doit utiliser un nom de paquetage unique afin que deux classes n'aient pas le même nom absolu.
Il faut aussi connaître la liste de noms des classes à charger.
Cette liste peut être fixe en calculant un nom de paquetage unique à partir du nom du fichier archive JAR (donc une seule classe chargée), ou elle peut être lue à partir d'un fichier inclut dans le fichier JAR (par exemple, un fichier nommé extension.ini
à la racine du fichier JAR).
public static void chargerClasses(ClassLoader cl, File f_jar) throws IOException
{
JarFile jf = new JarFile(f_jar);
try
{
JarEntry je = jf.getJarEntry("extension.ini");
if (je != null)
{
InputStream in = jf.getInputStream(je);
try
{
// Lecture du fichier texte
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = br.readLine()) != null)
{
// Enlever l'éventuel commentaire de fin de ligne
// commençant par le caractère #
int i = line.indexOf('#');
if (i>=0) line = line.substring(0,i);
line = line.trim(); // Sans les espaces autour
// Ne pas traiter les lignes vides
if (line.length()==0) continue;
// Traitement de la ligne
// Il peut s'agir simplement de la classe à instancier immédiatement :
// Class<? extends Object> c = cl.loadClass(line);
// c.newInstance();
// Ou d'une classe instanciée lorsqu'elle est utilisée dans un script ou
// une commande de l'application en utilisant un nom associé à la classe :
i = line.indexOf('='); // <nom> = <classe>
if (i>=0)
{
String nom = line.substring(0,i).trim();
String nom_classe = line.substring(i+1).trim();
// Associer le nom à la classe instanciée plus tard
Class<? extends Object> c = cl.loadClass(nom_classe);
map_extensions.put(nom, c);
}
}
}
finally{ in.close(); }
}
}
finally{ jf.close(); }
}
public static void chargerExtensions(File dir) throws IOException
{
// Créer le chargeur de classes
ClassLoader cl = creerChargeurArchives(dir);
File[] fichiers = dir.listFiles();
// Charger les classes de chaque fichier *.jar
for(File f : fichiers)
{
if (f.isFile() && f.getName().toLowerCase().endsWith(".jar"))
chargerClasses(cl, f);
}
}