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

Logger


Logger, MethodHandle, MutableCallSite, JUnit, JMH

Exercice 1 - Logger

Le but de cet exercice est d'implanter une serie de Loggers qui ne ne consomment aucun temps de calcul s'ils ne sont pas activés.

On se propose d'implanter différents loggers ayant la même interface Logger:
public interface Logger {
  public void log(String message);
  
  public static Logger simpleLogger(Class<?> declaringClass, Consumer<? super String> consumer) {
    MethodHandle mh = createLoggingMethodHandle(declaringClass, consumer);
    return new Logger() {
      @Override
      public void log(String message) {
        try {
          mh.invokeExact(message);
        } catch(Throwable t) {
          if (t instanceof RuntimeException) {
            throw (RuntimeException)t;
          }
          if (t instanceof Error) {
            throw (Error)t;
          }
          throw new UndeclaredThrowableException(t);
        }
      }
    };
  }
  
  private static MethodHandle createLoggingMethodHandle(Class<?> declaringClass, Consumer<? super String> consumer) {
    // TODO
    return null;
  } 
}
   
Chaque Logger utilise un method handle ce qui permet de specialiser son code à un usage spécifique

  1. Dans le répertoire EXAM
    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 EXAM pour obtenir le module fr.umlv.exam.javainside.
  2. Copier coller le code ci-dessus dans une classe Logger dans le package fr.umlv.exam.javainside.
    Créer une classe de test JUnit 5 LoggerTests et écrire quelques tests vérifiant que le code de Logger fonctionne correctement.
    Note: vous n'avez pas besoin d'implanter la classe Logger pour cela.
  3. Ecrire la méthode createLoggingMethodHandle de la classe Logger et vérifier que les tests sont Ok.
    Pour l'instant, nous n'utiliserons pas le paramètre declaringClass qui correspond à la classe dans laquelle on créé le Logger.
    Note: pour créer un method handle sur le Consumer, vous pouvez utilisez bind ou findVirtual suivi d'un insertArguments (et un asType si les types ne correspondent pas).
  4. Copier coller le code du test de perf JMH ci-dessous pour mesurer la performance de votre Logger dans le cas où le consommateur pris en paramètre est message -> { /*empty*/ }
    import java.util.concurrent.TimeUnit;
    
    import org.openjdk.jmh.annotations.Benchmark;
    import org.openjdk.jmh.annotations.BenchmarkMode;
    import org.openjdk.jmh.annotations.Fork;
    import org.openjdk.jmh.annotations.Measurement;
    import org.openjdk.jmh.annotations.Mode;
    import org.openjdk.jmh.annotations.OutputTimeUnit;
    import org.openjdk.jmh.annotations.Scope;
    import org.openjdk.jmh.annotations.State;
    import org.openjdk.jmh.annotations.Warmup;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;    
        
    @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Fork(3)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class LoggerBenchMark {
      static class SimpleLoggertTestClass {
        private static final Logger LOGGER =
          Logger.simpleLogger(SimpleLoggertTestClass.class, message -> { /* empty */ });
      }
    
      @Benchmark
      public void no_op() {
        // empty
      }
      
      @Benchmark
      public void simple_logger() {
        SimpleLoggertTestClass.LOGGER.log("should not be printed !");
      }    
      
      public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(LoggerBenchMark.class.getName()).build();
        new Runner(opt).run();
      }
    }
        
  5. On peut remarquer que l'interface Logger est une interface fonctionnelle, car elle n'a qu'une méthode abstraite, on peut donc implanter un Logger avec une lambda au lieu d'une classe abstraite comme nous avons fait pour l'instant.
    Créer une méthode statique fastLogger qui fait exactement la même chose que simpleLogger mais utilise une lambda au lieu d'une classe anonyme.
    Modifier votre test JMH pour aussi tester cette nouvelle implantation.
    Y-a-t'il une différence de performance ?
  6. On souhaite pouvoir activer ou désactiver (avec un booléen) tous les loggers ayant la même declaringClass et ce même si les loggers ont déjà été créés.
    Pour cela, on va créer un ClassValue static final dans Logger qui associe pour une classe, un MutableCallSite crée pour des fonctions qui ne prennent rien en argument en renvoie un booléen.
    L'idée est que si l'on veut activer ou non les loggers d'une classe, on va changer le method handle contenu dans le MutableCallSite pour être soit une fonction qui renvoie toujours true soit une fonction qui renvoie toujours false.
    Modifier le code de createLoggingMethodHandle pour que le method handle renvoyé utilise le MutableCallSite (techniquement la valeur de retour de l'appel à dynamicInvoker() sur le MutableCallSite) comme test d'un guardWithTest. Si la valeur est vrai, le guardWithTest exécute le méthod handle qui log et si la valeur est fausse le guardWithTest exécute un méthode handle vide (créer en utlisant MethodHandles.empty(). Comme cela, cela revient à activer ou non les loggers lorsque le MutableCallSite change de valeur.
    Ajouter une méthode statique enable(Class<?> declaringClass, boolean enable) à l'interface Logger qui permet d'activer ou désactiver tous les loggers créer sur une même declaringClass (donc tous les Logger utilisant le même MutableCallSite).
    Note: l'état d'un Logger par défaut doit être "activé" pour que les tests déjà écrits continuent de fonctionner.
  7. Ecrire un test JMH qui crée un Logger que vous désactiverez dans le bloc statique et tester sa vitesse.
  8. On souhaite ajouter la notion de Logger.Level lorsqu'on log un message.
    Modifier le code pour
    • Créer un enum Level dans Logger ayant les niveaux DEBUG, WARNING, et ERROR
    • Créer une méthode log qui prend un level et un message et faire en sorte que l'ancienne méthode log (celle avec 1 paramètre) appel maintenant log à 2 paramètres avec le niveau WARNING
    • Modifier createLoggingMethodHandle pour que le code continue à marcher (pour l'instant en ignorant le level).
  9. Puis
    • Créer une méthode statique level(Class<?> declaringClass, Logger.Level level) qui permet de changer le level d'une declaringClass. Pour cela, ajouter un autre ClassValue contenant des fonctions qui ne prennent rien en paramètre et renvoient un Logger.Level.
    • Modifier createLoggingMethodHandle pour ajouter un autre guardWithTest sur le level cette fois ci.
    • Ajouter de nouveaux test JUnit qui testent que si pour une classe est configurée avec le level ERROR alors les appels à la méthode log avec en paramètre un level DEBUG ou WARNING ne font rien.
    • Ajouter un nouveau test JMH qui test la vitesse dans le cas où l'on affiche rien à cause du level.
      Si le test montre que le logger est lent, c'est que vous ne faite pas le test sur les levels correctement.
  10. Enfin, comment faire pour n'utiliser qu'un seul ClassValue qui marche à la fois pour enable et level ?
    Modifier le code en conséquence.