La programmation dynamique

images/blackbelt.png

Java étant un langage compilé, les codes que l’on écrit opèrent sur des classes connues au compile-time. Il est pourtant possible d'écrire des programmes qui opèrent sur des classes "inconnues" du compilateur:

Prenons un exemple (théorique) de mise en oeuvre de la programmation dynamique:

Ces objets sont conformes à un canevas de code:

public abstract class CritereChoix implements Serializable {
    protected final String catégorie ;

   /**
    les construteurs des sous-classes doivent initialiser ce champ
    mais être eux-mêmes sans paramètres
   */
    protected CritereChoix(String catégorie) {
        this.catégorie = catégorie;
    }

    public String getCatégorie(){
        return this.catégorie ;
    }

    public abstract boolean validation() ;
}

et le service de Catalogue :

public interface Catalogue {
   // les catégories d'articles
    List<String> listCatégories() ;
   // le critère pour une catégorie
    CritereChoix getCritères(String catégorie) ;
   // on fournit des articles en fonction des critères
    Flow.Publisher<? extends Article>  recherche(CritereChoix critères) ;
}

La classe Class et ses méthodes fondamentales de découverte

Obtention de l’objet Class

Pour pouvoir opérer à un niveau interprétatif sur un objet on a besoin d’une instance de la classe java.lang.Class représentant la classe de l’objet.

[Note]

Dans beaucoup d’exemples qui suivent on n’a pa mentionné le type-paramètre de Class (pour simplifier les exemples).

Par ailleurs les méthodes présentées sont généralement CallerSensitive il faudra donc faire attention à la portée des codes (par exemple avec un accès à des packages qui sont marqués opens).

Une instance de Class peut s’obtenir par :

  • Chargement dynamique d’une classe au travers d’un ClassLoader. On doit alors fournir une chaîne donnant le nom canonique de la classe (hiérarchie de package + nom de classe). Par ex.:

    Class laClasse = Class.forName(nom) ;

    On notera que cette opération a pour effet d’exécuter tous les blocs static de la classe correspondante. (on peut en profiter pour vérifier si toutes les conditions de déploiement de la classe sont remplies).

  • obtention de la classe d’une instance (obtenue par différents moyens, par exemple un transfert distant):

    Class laClasse = instance.getClass() ;
  • obtention dans un contexte static en employant un pseudo-champ statique nommé lui même class :

    Class maClasse = MaClasse.class ;
    Class tabCharClass = char[].class ;
    // Class<char[]> tabCharClasse = char[].class ;

    (ces classes représentant des primitifs servent à repérer par ex. les types des arguments d’une méthode: on ne peut pas faire des opérations "objet" avec)

    Class typeInt = int.class ;
    Class typeInt = Integer.TYPE ;

    Dans l’exemple du Catalogue nous aurions:

Catalogue catalogue = // trouvé par SrviceLoader
CritereChoix critereChoix= catalogue.getCritères("Livre");
Class<? extends CritereChoix> clazz = critereChoix.getClass();

La classe ainsi trouvée serait d’un type a priori inconnu de l’IHM.

public class CritereChoixLivre extends CritereChoix {
    String titre ;
    String auteur ;
    double prixMax;

    public CritereChoixLivre() {
        super("Livre");
    }

   //ici mettre annotation pour clef de traduction ...
   // Clef(nom="title")
    public String getTitre() {
        return titre;
    }

    public void setTitre(String titre) {
        this.titre = titre;
    }

    public String getAuteur() {
        return auteur;
    }

    public void setAuteur(String auteur) {
        this.auteur = auteur;
    }

    public double getPrixMax() {
        return prixMax;
    }

    public void setPrixMax(double max) {
        this.prixMax = max ;
    }

    @Override
    public boolean validation() {
        //code de vérification de l'état de l'instance
    }

L’objectif serait alors de créer des instances de cette classe, d’invoquer des méthodes get/set puis la méthode validation et ensuite de passer une telle instance au Catalogue pour obtenir des Livres correspondants aux critères.

Obtention des informations sur la classe

A partir d’une instance de Class (qui ne répond pas à la condition isPrimitive()) on peut obtenir des informations sur les constituants.

Par exemple :

  • quels sont les champs déclarés dans la classe? :

    Field[] champs = maClasse.getDeclaredFields() ;

    si la classe représente une tableau (isArray() répond true), il est possible de connaître le type des composants :

    Class typeComposants = classeTableau.getComponentType();
  • quelles sont les méthodes définies dans la classe?

    Method[] méthodes = maClasse.getDeclaredMethods() ;

    getMethods() rend toutes les méthodes dont l’objet dispose (y compris celles qui sont héritées)

  • quels sont les constructeurs définis dans la classe?

    Constructor[] constructeurs = maClasse.getDeclaredConstructors();

L’utilisation des classes correspondantes (Field, Method, Constructor,…) relève du package java.lang.reflect.

Le package java.lang.reflect

La manipulation des champs, méthodes, constructeurs peut poser des problèmes de droit d’accès. Pourtant certains mécanismes (voir par exemple la linéarisation) doivent pouvoir agir indépendament des privilèges de responsabilité (private, protected, etc.):

  • Membres et constructeurs de la classe dérivent tous de java.lang.reflect.AccessibleObject qui dispose d’une méthode setAccessible(boolean) qui permet de passer outre aux privilèges de responsabilité.
  • Bien entendu n’importe quel code ne peut réaliser ces opérations sans disposer d’une autorisation de sécurité adequate:

    java.lang.reflect.ReflectPermission "suppressAccessChecks"

Les constructeurs

A partir d’une instance de Constructor on peut obtenir :

  • les caractéristiques des paramètres

    Parameter[] parms = constructeurs[ix].getParameters() ;

    Chaque objet Parameter permet d’obtenir des informations sur chaque paramètre: son type, éventuellement son nom, ses annotations, si c’est une partie varargs, etc.). Le nom du paramètre dans le code source est accessible si le binaire a été compilé avec l’option -parameters

  • d’autres informations comme les modificateurs utilisés , les exceptions déclarées. ou les "annotations" du constructeur (Pour les "annotations" voir le chapitre correspondant).

La creátion dynamique d’une instance peut se faire par maClasse.newInstance() (s’il y a un constructeur sans paramètres) ou par l’invocation de newInstance avec un tableau de paramètres :

   try {
      constructeur.newInstance(tableauArguments) ;
      // ou newInstance(arg1, arg2, arg3,....)
   } catch (Exception xc) {
   // actions et rapports
   }

Dans notre exemple il sera possible d’obtenir une instance de la classe "critère" de la manière suivante:

// Class<? extends CritereChoix> clazz
Constructor<? extends CritereChoix> constructor =  clazz.getConstructor();
// ici pas besoin de faire un transtypage
CritereChoix crit =  constructor.newInstance() ;

Les méthodes

A partir d’une instance de Method on peut obtenir :

  • son nom:

    String nomMeth = méthodes[ix].getName() ;
  • le type de son résultat

    Class typeRes = méthodes[ix].getReturnType() ;
  • les informations sur les paramètres

    Parameter[] parms = méthodes[ix].getParameters() ;
  • d’autres informations comme les modificateurs utilisés, les exceptions déclarées ou les annotations .

On peut également invoquer la méthode dynamiquement sur cet objet en lui passant les arguments appropriés (et éventuellement en récupérant une InvocationTargetException qui chaîne une Exception provoquée par l’invocation sous-jacente).

  Class laClasse = instance.getClass() ;
  Method laMeth0 = laClasse.getDeclaredmethods() [0] ;
  .... // fixation evt. des droits d'accès
  .... // obtention informations des type des paramètres

  .....// création d'un tableau d'arguments
  //** pour chaque type primitif on passe une instance
  //** de la classe d'encapsulation correspondante
  //** par ex. un Integer pour int.TYPE

  try {
    laMeth0.invoke(instance, tableauArguments) ;
    // ou invoke(instance, arg1, arg2, arg3 ...)
  } catch (Exception xc) {
   // actions et rapports
  }

Si la méthode est statique le premier argument est ignoré (il peut être null).

Les champs

A partir d’une instance de Field on peut obtenir :

  • son nom:

    String nom = champs[ix].getName() ;
  • son type

    Class type = champs[ix].getType() ;
  • d’autres informations comme les modificateurs utilisés ou les annotations.

On peut également consulter la valeur de ce membre (ou la modifier). Les méthodes get/setXXX permettent de lire ou modifier une valeur sur une instance passée en paramètre :

  Class laClasse = instance.getClass() ;
  Field leChamp0 = laClasse.getDeclaredFields() [0] ;
  .... // obtention informations de type
  .... // fixation evt. des droits d'accès
  .....// création d'un objet de ce type: valeurObjet
  leChamp0.set(instance, valeurObjet) ;

Si le champ est statique le premier argument est ignoré (il peut être null)

Les tableaux

La classe java.lang.reflect.Array permet des accès particuliers aux objets qui sont des tableaux (elle permet également la création de tableaux à une ou plusieurs dimensions).

Pour accéder en lecture ou en écriture les méthodes set/getXXX sont dotés d’un argument représentant l’index dans le tableau. Les méthode statiques newInstance permettent de créer des tableaux d’un type donné.

Le package java.lang.invoke

images/blackbelt.png (2° dan!)

Le package java.lang.invoke permet un accès privilégié à l’invocation du code binaire (par ex. si ce code a été généré par un autre langage que Java).

Pour accéder aux mécanismes d’une classe il faut obtenir un objet MethodHandles.Lookup.

Ces objets ont une portée qui est celle de leur code d’invocation de la méthode MethodHandles.lookup() (méthode CallerSensitive). C’est à dire que les méthodes du Lookup pourront accéder aux membres et constructeurs public des packages du même module (ou des modules "ouverts"), aux éléments public, protected et friendly du même package, et aux éléments public, private , friendly et protected (de l’instance courante) de la classe courante.

Dans l’exemple précédent nous pourrions alors avoir:

public interface Catalogue {
   ...
   // accès aux codes public du module de définition
   public MethodHandles.Lookup catLookup() ;
}

ou bien

public abstract class CritereChoix implements Serializable {
   ...
   // tout sur la classe concrète!
   public abstract MethodHandles.Lookup classLookup() ;
}

Dans les deux cas il faut explicitement invoquer le lookup dans les classes concrètes (pas de mutualisation pour une invocation de méthode CallerSensitive)

L’objet Lookup permet alors d’accéder à des MethodHandle d’une classe (et aussi à des VarHandle) qui sont des références vers des méthodes (ou des constructeurs) de cette classe.

Ici il va falloir bien comprendre que:

  • Les caractéristiques du code sont : son nom + le type des paramètres et du résultat. Cette combinaison de types est rendu par un objet descriptif MethodType.
  • Quand on invoque le code d’une méthode le premier argument est en fait l’instance sur lequel le code s’applique (c’est une vision "interne" des méthodes du binaire qui sont alors des fonctions dont le premier paramètre est l’instance).

Les combinaisons de type

Pour caractériser un constructeur ou une méthode il faut se servir d’un objet MethodType.

Il existe plusieurs fabriques qui permettent de combiner un type retour et des types de paramètres. Attention: pour un constructeur le type retour est void (le constructeur est, en interne, une procédure).

Donc:

//pour un constructeur sans paramètres
MethodType ctorType = MethodType.methodType(void.class) ;
// pour un constructeur avec paramètres rajouter aux arguments la liste des types des paramètres

// pour une méthode "setAuteur" sans résultat et un paramètre String
MethodType setAuteurType = MethodType.methodType(void.class, String.class) ;
// mais on peut avoir d'autres types retour et paramètres

Invocation de constructeurs

Reprenons notre exemple:

CritereChoix critereChoix= cat.getCritères("Livre");
Class<? extends CritereChoix> clazz = critereChoix.getClass();
...
//on obtient le constructeur
// une fois qu'on connait son MethodType
 MethodHandle ctorHandle = lookup.findConstructor(clazz,ctorType) ;
// on peut créer une instance sans arguments
CritereChoix choixEffectif = (CritereChoix) ctorHandle.invoke();

Invocation de méthodes

Sur une instance on peut trouver puis invoquer des méthodes:

// recherche d'une méthode d'instance
 MethodHandle setAuteur = lookup.findVirtual(clazz, "setAuteur", setAuteurType)  ;
//invocation: premier argument est l'instance
setAuteur.invoke( choixEffectif,"Alcofibras Nasier") ;

Notes:

  • Il existe également une version plus performante invokeExact par laquelle les types des paramètres doivent être exacts, le polymorphisme ne joue pas. (Attention: nous sommes , encore une fois, dans une exécution CallerSensitive, si le code ci-dessus n’a pas un accès direct à la classe CritereChoixLivre - mais uniquement à CritereChoix - le code ne s’exécutera pas!)
  • Les méthodes de Lookup comme findGetter/findSetter vont permettre d’accéder à des variables membres … mais ne trouveront pas les accesseurs/mutateurs correspondants

Mandataires dynamiques

images/blackbelt.png

Un mandataire (proxy) est un objet qui s’interpose devant un autre objet pour intercepter les invocations et modifier la façon dont un service est rendu. En Java cette interposition peut se réaliser :

  • soit avec un objet d’un type qui sous-classe le type de la référence demandée et qui spécialise ses méthodes (pour evt. déléguer à un objet du type demandé : voir l’exemple "combinaison héritage/délégation" dans le chapitre sur les patterns ).

    images/introspecs/proxyObject.png

  • soit avec un objet qui implante une interface correspondante au type demandé et qui délègue éventuellement à une instance de l’objet initial.

    images/introspecs/proxyInterf.png

Dans ce dernier cas les mandataires dynamiques (dynamic proxy) permettent de générer dynamiquement des objets d’un type dont la classe effective est définie au runtime. Il peut y avoir plusieurs raisons pour opérer dynamiquement dont, en particulier, le fait que l’interposition ne s’adresse pas forcément à un objet d’un type effectivement connu au compile-time, ou le fait que le comportement rajouté dans l’interposition soit relativement générique (c.a.d s’applique à des ensembles de méthodes).

La classe Proxy de java.lang.reflect permet de générer dynamiquement des mandataires qui implantent un ensemble d’interfaces.

Proxy.newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

L’objet InvocationHandler gère les invocations en implantant un code pour:

Object  invoke (Object proxy, Method method, Object[] args)

Un exemple d’objet mandataire sont les Stubs générés par RMI (voir chapitre correspondant aux "références distantes").

Autre exemple:

  • soit un code qui permette d’invoquer une méthode avant une invocation et une méthode après. (Un tel code peut être utilisé pour tracer, mesurer des performances, etc.)

    public interface EncadrementInvocation<T> extends Cloneable{
        T avant(T objet, Method method);
        void après(T objet, Method method) ;
        EncadrementInvocation<T> clone() ;
    }
  • Soit maintenant un Processor qui reçoit des objets (nécessairement typés par une interface) et qui rend des mandataires de ces objets:

    public class InterfacesControlers<Intfc> implements Flow.Processor<Intfc, Intfc> {
        private Flow.Subscriber<? super Intfc> subscriber;
        private Flow.Subscription subscription;
        private EncadrementInvocation<Intfc> encadrement;
        private ClassLoader loader = this.getClass().getClassLoader();
    
        public InterfacesControlers(Flow.Publisher<Intfc> publisher,
                                    EncadrementInvocation<Intfc> encadrement) {
            this.encadrement = encadrement;
            publisher.subscribe(this);
    
        }
    
        @Override
        public void subscribe(Flow.Subscriber<? super Intfc> subscriber) {
            this.subscriber = subscriber;
            subscriber.onSubscribe(subscription);
        }
    
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            this.subscription = subscription;
        }
    
        @Override
        public void onNext(Intfc item) {
            Class clazz = item.getClass();
            Class[] interfaces = clazz.getInterfaces();
            Intfc mandataire = (Intfc) Proxy.newProxyInstance(loader, interfaces,
                    new InvocationHandler() {
                        Intfc resItem = item;
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            EncadrementInvocation<Intfc> cadre = encadrement.clone();
                            resItem = cadre.avant(resItem, method);
                            Object result = method.invoke(resItem, args);
                            cadre.après(resItem, method);
                            return result;
                        }
                    });
            subscriber.onNext(mandataire);
        }
    
        @Override
        public void onError(Throwable throwable) {
            subscriber.onError(throwable);
        }
    
        @Override
        public void onComplete() {
            subscriber.onComplete();
    
        }
    }

    (sur les objets rendus on pourra invoquer les méthodes de l’interface paramètre Intfc mais aussi les méthodes des autres interfaces de l’objet -après transtypage - par contre il faudra se méfier des déclenchements d’exceptions;

Utilisation de la programmation dynamique par d’autres formalismes

Il est intéressant d’encapsuler les mécanismes de la programmation dynamique dans des formalismes de plus haut niveau permettant de véritables "langages de script".

Un exemple remarquable est le langage Groovy qui vit "en symbiose" avec java. Disposant de ses propres dispositifs syntaxiques et, surtout, de paradigmes spécifiques ce langage peut faire appel directement à du code Java ou être appelé directement depuis java puisqu’il s’agit en réalité de code Java enchassé dans un formalisme de plus haut niveau.

L’intérêt majeur de ce langage est qu’il est adapté à l'écriture de D.S.L(Domain Specific Languages) c’est à dire des formalismes ad hoc destinés à décrire des ensembles données/codes adaptés à une catégorie de problèmes : par ex. description d’une interface graphique, description de la chaîne de fabrication d’un logiciel, descriptions de tests, etc….