:: Enseignements :: ESIPE :: E4INFO :: 2018-2019 :: Java Inside ::
[LOGO]

Reflection et Annotation


Reflection, Introspection, Annotation et ClassValue

Exercice 1 - Reflection et Annotation

Le but de cet exercice est se familiariser avec la notion de reflection, pour cela on souhaite écrire un code qui permet d'afficher un objet au format JSON, par exemple pour la classe Person:
public class Person {
  private final String firstName;
  private final String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = Objects.requireNonNull(firstName);
    this.lastName = Objects.requireNonNull(lastName);
  }
  
  public String getFirstName() {
    return firstName;
  }
  public String getLastName() {
    return lastName;
  }
}
   
On va écrire la méthode toJSON qui prend en paramètre une Person et renvoie une chaîne de caractères au format JSON:
  public static String toJSON(Person person) {
    return
        "{\n" +
        "  \"firstName\": \"" + person.getFirstName() + "\"\n" +
        "  \"lastName\": \"" + person.getLastName() + "\"\n" +
        "}\n";
  }
   
Mais on veut aussi que la méthode toJSON puisse prendre en paramètre un Alien et renvoie aussi une chaîne de caractères au format JSON, nous risqons dupliquer du code.
public class Alien {
  private final String planet;
  private final int age;

  public Alien(String planet, int age) {
    if (age <= 0) {
      throw new IllegalArgumentException("Too young...");
    }
    this.planet = Objects.requireNonNull(planet);
    this.age = age;
  }

  public String getPlanet() {
    return planet;
  }

  public int getAge() {
    return age;
  }
}
   
public class Main {
  public static String toJSON(Person person) {
    return
        "{\n" +
        "  \"firstName\": \"" + person.getFirstName() + "\"\n" +
        "  \"lastName\": \"" + person.getLastName() + "\"\n" +
        "}\n";
  }

  public static String toJSON(Alien alien) {
    return 
        "{\n" + 
        "  \"planet\": \"" + alien.getPlanet() + "\"\n" + 
        "  \"members\": \"" + alien.getAge() + "\"\n" + 
        "}\n";
  }
  
  public static void main(String[] args) {
    var person = new Person("John", "Doe");
    System.out.println(toJSON(person));
    var alien = new Alien("E.T.", 100);
    System.out.println(toJSON(alien));
  }
}

Et si l'on doit dupliquer le code de toJSON à chaque fois que l'on veut transformer en JSON une nouvelle classe, c'est embêtant ...
A kitten dies each time you duplicate a bug !
Pour éviter l'hécatombe, on se propose d'écrire une seule méthode toJSON prenant un Object en paramètre et utilisant la réflexion (reflection en anglais) pour trouver les propriétés à écrire au format JSON.

  1. Créer un répertoire lab05 dans le repository java-inside
    Ajouter la version de pro "jdk-12" à votre PATH
          export PATH=$PATH:/home/ens/forax/java-inside/pro-jdk-12
        

    Exécuter pro scaffold dans le répertoire lab05 pour obtenir les fichiers de départ.
  2. Créer un projet Eclipse pointant sur le répertoire lab05 et faite en sorte que le JDK utiliser pour ce projet (dans Property > Build Path) pointe sur Pro.
  3. Écrire une méthode toJSON qui prend en paramètre un Object, utilise la réflexion pour accéder à l'ensemble des méthodes publiques de la classe de l'objet (Object.getClass() puis java.lang.Class.getMethods), sélectionne les getters, puis renvoie le nom des propriétés (pour l'instant).
    Le nom d'une propriété peut s'obtenir à partir du nom du getter en utilisant la fonction suivante (en supposant que votre getter s'appelle bien getSomething):
         private static String propertyName(String name) {
           return Character.toLowerCase(name.charAt(3)) + name.substring(4);
         }
        

    Note: utilisé un Stream c'est plus simple.
    Note2: le résultat contient aussi la propriété class qui vient de getClass, nous verons comment la supprimer plus tard.
  4. Modifier la méthode toJSON pour renvoyé et le nom des propriétés et leur valeur en utilisant Method.invoke() pour executer un getter et obtenir la valeur du champ correspondant.
    Note: faîtes attention à gérer correctement les exceptions lors de l'invocation de méthode (surtout InvocationTargetException), pour cela lisez la javadoc !!!
  5. Transformer les codes du main en vrai tests unitaires utilisant JUnit 5. Pour cela, les classes Person et Alien vont devenir des classes internes de votre classe de test.
  6. Modifier le fichier .travis.yml pour que Travis vérifie que votre programme compile et que les tests passent.
  7. La méthode getClass qui commence aussi par le préfixe "get", donc l'idée qu'une méthode qui commence pas 'get' est forcément un getter semble pas la bonne.
    Pour éviter les cas particuliers, on va plutôt marquer les méthodes qui feront partie de l'affichage JSON. À cette fin, on se propose de créer une annotation @JSONProperty et d'annoter uniquement les getters qui nous intérresse.
    Tutoriel sur comment créer une annotation
    Déclarez l'annotation JSONProperty visible à l'exécution et permettant d'annoter des méthodes, puis modifiez le code de toJSON pour n'utiliser que les propriétés issues de méthodes marquées par l'annotation JSONProperty.
    Enfin, modifier les tests en annotant les getters de la classe Person et Alien et les résultats attendus.
  8. En fait, une propriété JSON peut contenir des caractères comme le '-' qui ne sont pas des caractères valides dans un nom de méthode Java.
    Faîtes en sorte que l'on puisse utiliser l'annotation JSONProperty sans rien, et dans ce cas le nom de la méthode sera utilisée, mais que l'on puisse aussi utiliser l'annotation JSONProperty avec un nom. Ce nom sera alors utilisé au lieu du nom de la méthode.
    Note: la valeur par défaut d'un attribut d'une annotation ne peut pas être null.
  9. En fait, l'appel à getMethods est lent; regardez la signature de cette méthode et expliquez pourquoi...
    Regardez la méthode java.lang.reflect.Method.setAccessible et indiquez pourquoi l'appel à getMethods ne peut pas être accéléré.
  10. Nous allons donc limiter les appels à getMethods en stockant le résultat de getMethods dans un cache pour éviter de faire l'appel à chaque fois qu'on utilise toJSON.
    Utilisez la classe java.lang.ClassValue (allez voir la javadoc) pour mettre en cache le résultat d'un appel à getMethods pour une classe donnée.
  11. En fait, on peut cacher plus d'informations que juste les méthodes, on peut aussi pré-calculer le nom des propriétés pour éviter d'accéder aux annotations à chaque appel.
    Écrire le code qui pré-calcule le maximum de choses pour que l'appel à toJSON soit le plus efficace possible.
    Indication: quelle est la lettre grecque entre kappa et mu ?