Invocation dynamique
Introduction
L'invocation dynamique est une technique avancée en Java pour appeler des méthodes sans posséder directement les objets concernés.
Avantages :
- Code plus dynamique
- Code plus générique
Inconvénient :
- Concept complexe
- Beaucoup plus verbeux
Note
Contrairement aux idées reçues, l'invocation dynamique en Java et même plus rapide que l'appel classique de méthodes (Quand elle respecte les règles de visibilités). La JVM sait optimiser ce code. En revanche l'invocation par réflexion est à éviter, elle est plus lente car la JVM ne peut optimiser ce code.
Introspection
L'introspection est une technique avancée en Java permettant de parcourir la composition de la structure même d'un objet, ou d'une classe par le code.
Usage basique
package fr.zelmoacademy.model;
public class Customer{
private String email;
public Customer(){
}
// Accesseurs & Mutateurs
// ...
}
var customer = new Customer();
Field[] declaredFields = customer.getClass().getDeclaredFields();
^^^^^^^^^^^^^^^^^-----// Récupération de tous les attributs
// déclarés dans la classe 'Customer'.
System.out.println(Arrays.toString(declaredFields));
//> Sortie console:
// [private java.lang.String fr.zelmoacademy.model.Customer.email]
// On peut aussi faire l'instrospection de la classe directement
// au lieu de passer par l'instance (*.getClass()).
Method[] declaredMethods = Customer.class.getDeclaredMethods();
^^^^^^^^^^^^^^^^^^-----// Récupération de toutes les méthodes
// déclarés dans la classe 'Customer'.
System.out.println(Arrays.toString(declaredMethods));
//> Sortie console:
// [
// public void fr.zelmoacademy.model.Customer.setEmail(java.lang.String),
// public java.lang.String fr.zelmoacademy.model.Customer.getEmail()
// ]
Type superClass = customer.getClass().getGenericSuperClass();
^^^^^^^^^^^^^^^^^^^^^----// Récupérer la classe parent
System.out.println(superClass);
//> Sortie console:
// class java.lang.Object
Note
Avec l'introspection vous pouvez naviguez dans la structure d'une classe est voir toutes ses caractéristiques. cette approche permet en fonction de la structure d'une classe d'opter pour un comportement différent dans votre algorithme.
Usage avancé
Problématique :
public class Customer { /* ... */ }
public abstract class AbstractDAO<E> {
// ...
// On ne connait pas 'E'.
// On délègue ce traitement aux classes enfants.
// Cette méthode est donc abstraite.
protected abstract Class<E> getEntityClass();
}
public class CustomerDAO extends AbstractDAO<Customer> {
// ...
@Override
protected Class<Customer> getEntityClass(){
// La classe concrète résout la généricité
// Par conséquant, elle connait le type de 'E'
// Il suffit d'implémenter la méthode.
return Customer.class;
}
}
Dans le code ci-dessus, on souhaite récupérer la classe de E
.
Pour ce faire, une manière simple est de passer par l'héritage.
Cette méthode est viable, mais que se passe-t-il lorsqu'un nombre important de classe enfant de AbstractDAO
se présente ?
Est-il possible de factoriser ce code ?
Réponse
OUI
Solution :
L'introspection permet aussi de récupérer le type d'un générique.
public abstract class AbstractDAO<E> {
// ...
protected Class<E> getEntityClass(){
// Étant donné que c'est la classe parent qui possède la notation en diamant.
// On remonte donc vers cette dernière avec l'introspection.
// Il faut également transtyper la classe parent en 'ParameterizedType'.
// Cela permet de faire l'introspection sur la notation en diamant.
var type = (ParameterizedType) this.getClass().getGenericSuperclass();
// La méthode 'getActualTypeArguments' permet de récupérer un tableau de tous les génériques.
// La classe parent 'AbstractDAO' n'en possède q'un, il est à l'index zéro du tableau.
// Encore une fois, il est nécessaire de transtyper le résultat en 'Class<E>'.
// Cela permet d'avoir le bon type de retour.
var entityClass (Class<E>) type.getActualTypeArguments()[0];
// Ainsi, on peut récupérer la classe de 'E' dynamiquement.
// Les classes enfants n'ont plus besoin d'implémenter cette méthode.
return entityClass;
}
}
Note
Malheureusement le transtypage est obligatoire ici. Java polymorphe les classes ici en
java.lang.reflect.Type
. Cette interface permet à Java d'englober les notions : de variables, de primitives, de tableaux, de génériques et même de type brute.
Mise en œuvre
Principe de fonctionnement
Pour utiliser l'invocation dynamique, il est nécessaire d'observer quelques règles : * Respecter la visibilité des attributs, des méthodes, des classes. * Ne pas outrepasser cette visibilité * Être extrêmement précis avec les paramètres lors de l'invocation dynamique.
On conserve cette classe pour la démonstration.
package fr.zelmoacademy.model;
public class Customer{
private String email;
public Customer(){
}
@Override
public String toString() {
return String.format(
"Customer{email=%s}",
email
);
}
// Accesseurs & Mutateurs
// ...
}
var customer = new Customer();
System.out.println(customer);
//> Sortie console:
// Customer{email=null}
// On souhaite changer la valeur de l'attribut 'email' par invocation dynamique.
// On récupère une instance de travail pour utiliser l'invocation dynamique.
var lookup = MethodHandles.publicLookup();
// On prépare la signature de la méthode cible avec le type de retour:
// 1er paramètre: Type de retour.
// 2nd paramètre: Type du premier argument.
var setEmailSignature = MethodType.methodType(void.class, String.class);
// On récupère la méthode cible:
// 1er paramètre: Type de classe source.
// 2e paramètre: Nom exact de la méthode cible à invoquer.
// 3e paramètre: Type de retour et paramètre de la méthode cible à invoquer.
// On obtient une méthode d'instance mais rattachée à aucun objet: '.setEmail()'.
var setEmailHandler = lookup.findVirtual(
Customer.class,
"setEmail",
setEmailSignature
);
// Lier la méthode dynamique avec l'instance réelle.
// On obtient une méthode d'instance rattaché à un objet: 'customer.setEmail()'.
var setEmailBinded = setEmailHandler.bindTo(customer);
// Invocation dynamique
// On appelle la méthode avec les bons paramètres: 'customer.setEmail("john.doe@sample.com")'.
setEmailBinded.invokeExact("john.doe@sample.com");
System.out.println(customer);
//> Sortie console:
// Customer{email=john.doe@sample.com}
Cas pratique avancé
Comment générer le résultat d'une méthode toString
de manière générique pour n'importe quel type d'objet ?
À vous de coder !
Possible solution :
package fr.zelmoacademy.util;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
/**
* Utilitaire pour la génération de la méthode <b>toString</b> des objets.
*/
public final class ToString {
/**
* Constructeur interne. Pas d'instanciation.
*/
private ToString() {
throw new UnsupportedOperationException("No instance for you");
}
/**
* Générer grâce à l'invocation dynamique l'équivalent de la méthode <b>toString</b>
* pour n'importe quel type d'objet.
* Cette méthode utilise l'appel des accesseurs,
* idéalement les objets devraient suivre la convention <b>JavaBean</b>.
*
* @param o Instance quelconque
* @return Une chaîne de caractères représentant les données de cet objet.
*/
public static String generateDynamicToString(final Object o) {
var fields = new ArrayList<Field>();
var getters = new ArrayList<Method>();
var objectClass = o.getClass();
do {
// Récupération de tous les attributs de la classe.
var declaredFields = objectClass.getDeclaredFields();
Collections.addAll(fields, declaredFields);
// Récupération de tous les accesseurs.
var declaredMethods = objectClass.getDeclaredMethods();
Collections.addAll(getters, declaredMethods);
// Continuer tant que la classe parent n'est pas 'java.lang.Object'.
// Ce qui permet de récupérer les attributs et accesseurs des classes parent.
objectClass = (Class<?>) objectClass.getGenericSuperclass();
} while (!Objects.equals(objectClass, Object.class));
// Récupération de l'instance de travail pour utiliser l'invocation dynamique.
var lookup = MethodHandles.publicLookup();
// Préparation de la chaîne de caractères finale.
var text = new StringBuilder();
text.append(o.getClass().getName());
text.append("{");
try {
for (Field f : fields) {
// Récupération du le nom de l'attribut.
var name = f.getName();
// Récupération du type de l'attribut.
var type = f.getType();
// Construction du préfix de l'accesseur.
String getterPrefix;
if (!Objects.equals(type, boolean.class)) {
getterPrefix = "get";
} else {
// Attention pour un attribut de type 'boolean'
// l'accesseur commence par 'is'.
getterPrefix = "is";
}
// Construction du suffixe de l'accesseur.
// Mettre la première lettre du nom de l'attribut en majuscule.
String getterSuffix;
if (name.length() > 1) {
getterSuffix = name.substring(0, 1).toUpperCase() + name.substring(1);
} else {
getterSuffix = name.toUpperCase();
}
// Construction du nom de la méthode de l'accesseur.
var getterName = getterPrefix + getterSuffix;
// Recherche de l'accesseur parmi les méthodes existantes avec les critères suivants:
// - Nom de méthode identique
// - Type de retour: 'void'
// - Pas de paramètre
// - Méthode publique.
// L'objectif est de vérifier l’existence d'un accesseur pour un attribut donné.
// (Respect de la convention JavaBean)
var getter = getters
.stream()
.filter(m -> Objects.equals(m.getName(), getterName))
.filter(m -> !Objects.equals(m.getReturnType(), void.class))
.filter(m -> m.getParameterCount() == 0)
.filter(m -> m.getModifiers() == MethodHandles.Lookup.PUBLIC)
.findFirst();
// Si l'accesseur existe, l'invocation peut commencer
// Sinon pas d'invocation pour cet attribut.
if (getter.isPresent()) {
// On prépare la signature de la méthode cible avec le type de retour
var getterSignature = MethodType.methodType(type);
// On récupère la méthode cible
var getterHandler = lookup.findVirtual(
o.getClass(),
getterName,
getterSignature
);
// Lier la méthode dynamique avec l'instance réelle.
var getterBinded = getterHandler.bindTo(o);
// Invocation dynamique
var getterValue = getterBinded.invoke();
// Construction de la chaîne de caractères.
text.append(name);
text.append("=");
text.append(getterValue);
text.append(", ");
}
}
} catch (Throwable ex) {
// L'invocation dynamique peut lever une exception si elle est mal réalisée.
// L'exception n'est pas traitée à ce niveau.
throw new IllegalStateException(ex);
}
// Supprimer des derniers caractères superflus.
var index = text.lastIndexOf(",");
var offset = 2;
if (index >= 0 && text.length() > offset) {
text.setLength(text.length() - offset);
}
// Fin de construction de la chaîne de caractères.
text.append("}");
return text.toString();
}
}
Usage:
package fr.zelmoacademy.model
import fr.zelmoacademy.util.ToString;
public class Customer{
private String givenName;
private String familyName;
private String email;
// Constructeurs
// ...
@Override
public String toString(){
// Nouvelle implémentation.
return ToString.generateDynamicToString(this);
}
// Accesseurs & Mutateurs
// ...
public static void main(String[] args){
var customer = new Customer("John", "DOE", "john.doe@example.com");
System.out.println(customer);
//> Sortie console:
// fr.zelmoacademy.model.Customer{givenName=John, familyName=DOE, email=john.doe@example.com}
}
}