:: Enseignements :: ESIPE :: E4INFO :: 2020-2021 :: 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, de record et aussi apprendre à faire des pull request/merge request et de beaux commits.
Pour cela on souhaite écrire un code qui permet d'afficher un objet au format JSON, par exemple pour le record Person:
import static java.util.Objects.requireNonNull;

public record Person(String firstName, String lastName) {
  public Person {
    requireNonNull(firstName);
    requireNonNull(lastName);
  }
}
   
On va écrire dans la classe JSONPRinter 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 """
      {
        "firstName": "%s",
        "lastName": "%s"
      }
      """.formatted(person.firstName(), person.lastName());
  }
   
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.
import static java.util.Objects.requireNonNull;
     
public record Alien(int age, String planet) {
  public Alien {
    if (age < 0) {
      throw new IllegalArgumentException("negative age");
    }
    requireNonNull(planet);
  }
}
   
public class JSONPrinter {
  public static String toJSON(Person person) {
    return """
      {
        "firstName": "%s",
        "lastName": "%s"
      }
      """.formatted(person.firstName(), person.lastName());
  }

  public static String toJSON(Alien alien) {
    return """
      {
        "age": %s,
        "planet": "%s"
      }
      """.formatted(alien.age(), alien.planet());
  }
  
  public static void main(String[] args) {
    var person = new Person("John", "Doe");
    System.out.println(toJSON(person));
    var alien = new Alien(100, "Saturn");
    System.out.println(toJSON(alien));
  }
}

Et si l'on doit dupliquer le code de toJSON à chaque fois que l'on veut transformer en JSON un nouveau record, 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 Record 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 au nouveau module pour le lab2
          mvn archetype:generate -DgroupId=fr.umlv.javainside  -DartifactId=lab2 -DinteractiveMode=false
        

    Et nettoyer le pom.xml généré car le POM de java-inside contient déjà les dépendances et le build dont on a besoin.
  2. En utilisant l'interface Web de Github, en étant logé, aller dans votre repository, puis dans Settings puis dans Manage access et ajouter votre voisin comme collaborateur. Votre voisin devra faire de même.
    A partir de maintenant et pour tout le reste du lab, à chaque fois que vous voudrez pousser du code sur github, vous aller dans un premier temps créer une branch, faire le/les commits dans cette branche puis créer un pull request sur cette branche en ajoutant votre voisin comme reviewer. Une fois que votre voisin aura fait une revue de code et vous aura dit de pouvoir intégrer/merger le code (avec un LGTM, Looks Goo To Me), vous utiliserez l'inteface de Github pour créer un seul commit (squash) que vous intégrez à la branche master. Une fois les modifications intégrées, la branche sera détruite (une branche par feature).
    Si tous ce passe bien, lorsque vous allez pousser (push) du code votre la branche si il y a une pull request associée, Travis devrait se déclencher et vérifier que votre code est valide (bien sûr, cela veut dire que vous avez des tests, sinon, rien ne sera vérifié sinon que maven compile bien le code).
  3. On souhaite, dans JSONPrinter, écrire une méthode toJSON qui prend en paramètre un Record, utilise la réflexion pour accéder à l'ensemble des components du record (Object.getClass() puis java.lang.Class.getRecordComponents), et qui pour l'instant renvoies sous forme d'une String les noms des components séparés par des virgules.
    Cette méthode toJSON va remplacer les deux méthodes existantes, donc mettez les en commentaire.
    Avant d'implanter le code, dans intelliJ créer une nouvelle branche,
    https://www.jetbrains.com/help/idea/manage-branches.html
    puis une fois le code écrit, pousser (push) le ou les commits dans cette branche. Une fois qu'il y a du code dans la branche, aller sur github.com et créer une pull request en mettant votre voisin en reviewer.
    Note: pour l'implantation, utiliser un Stream c'est plus simple.
  4. Modifier la méthode toJSON pour renvoyer le nom des components et leurs valeurs en utilisant récupérant l'accesseur du component RecordComponent.getAccessor() et en invoquant celui-ci avec Method.invoke().
    Une fois que le code marche créer la pull request correspondante.
    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 !!!
  5. Transformer les codes du main en vrai tests unitaires utilisant JUnit 5.
    Vous utiliserez la classe IncompleteJSONParser.java pour tester qu'une chaine de caractères est bien au format JSON.
    Pour cela, les records Person et Alien vont devenir des records internes (ou locales à votre convenance) de votre classe de test/vos tests.
    Créer la pull request correspondante.
  6. Le format JSON permet d'avoir des noms qui contienne des tirets (par exemple, book-title) qui n'est pas un identifiant valide en Java. Il faut donc ajouter de l'information à un component d'un record pour indiquer le nom que l'on veut utiliser au lieu d'utiliser le nom du component.
    Pour résoiudre se problème, on se propose de créer une annotation @JSONProperty et d'annoter les components dont on veut changer le nom
    Par exemple
          record Book(@JSONProperty("book-title") String title, int year) { }
        

    Tutoriel sur comment créer une annotation
    Déclarez l'annotation JSONProperty visible à l'exécution et permettant d'annoter un component d'un record, puis modifiez le code de toJSON pour utiliser le nom de l'annotation JSONProperty si un component est annoté par cette annotation.
    Enfin, ajouter un test vérifiant que votre code à la comportement attendu.
    Créer la pull request correspondante.
  7. On souhaite pourvoir utliser l'annotation sans nom et dans ce cas, le nom dans le JSON sera le nom du composant avec les undercores '_' changer en tirets '-'.
    Par exemple
          record Book(@JSONProperty String book_title, int year) { }
        

    Ecrire le code et les tests puis créer la pull request correspondante.
    Note: la valeur par défaut d'un attribut d'une annotation ne peut pas être null.
  8. En fait, l'appel à getRecordComponents est lent; regardez la signature de cette méthode dans la javadoc et expliquez pourquoi...
  9. Nous allons donc limiter les appels à getRecordComponents en stockant le résultat de getRecordComponents 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 à getRecordComponents pour une classe donnée.
    Créer la pull request correspondante.
  10. En fait, on peut cacher plus d'informations que juste les méthodes, on peut aussi pré-calculer le nom à afficherdans le JSON 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.
    Créer la pull request correspondante.
    Indication: quelle est la lettre grecque entre kappa et mu ?
  11. Ca y est, c'est fini, vous pouvez supprimer le fait que votre voisin soit un collaborateur sur le projet !