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

Reflection, Annotation, JSON


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. Dans le répertoire java-inside, utiliser Maven pour générer les fichiers pour le lab2
          mvn archetype:generate \
            -DarchetypeArtifactId=maven-archetype-quickstart \
            -DgroupId=fr.umlv.java.inside \
            -DartifactId=lab2  \
            -DinteractiveMode=false
        

    Recopier les properties, dependencies et les plugins de build du pom.xml du lab précédent.
  2. Créer un projet Eclipse/IntelliJ pointant sur le répertoire lab2 et configurer votre IDE.
  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. Faire un commit de votre code dans le repository locale puis faite un push sur Github
  5. Modifier la méthode toJSON pour renvoyer le nom des propriétés et leurs valeurs en utilisant Method.invoke() pour executer un getter et obtenir la valeur du champ correspondant.
    Une fois que le code marche faire un commit et un push
    Note: faîtes attention à gérer correctement les exceptions lors de l'invocation de méthode invoke (surtout InvocationTargetException), pour cela lisez la javadoc de chaque exceptions qui peut être levée !!!
  6. 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.
    Faite un commit et push des tests.
  7. Modifier le fichier .travis.yml pour que Travis vérifie que votre programme compile et que les tests passent.
    Vérifier que votre projet est au vert sur l'interface web de Travis.
  8. 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.
    Faite un commit/push quand vous cela marche.
  9. 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.
    Faite un commit/push quand vous cela marche.
    Note: la valeur par défaut d'un attribut d'une annotation ne peut pas être null.
  10. En fait, l'appel à getMethods est lent; regardez la signature de cette méthode dans la javadoc et expliquez pourquoi...
    Regardez la méthode java.lang.reflect.Method.setAccessible toujours avec la javadoc et indiquez pourquoi l'appel à getMethods ne peut pas être accéléré.
  11. 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.
    Faite un commit/push quand vous cela marche.
  12. 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.
    Faite un commit/push quand vous cela marche.
    Indication: quelle est la lettre grecque entre kappa et mu ?