:: Enseignements :: ESIPE :: E4INFO :: 2018-2019 :: Java Inside ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
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
-
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.
-
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.
-
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).
-
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();
}
}
-
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 ?
-
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.
-
Ecrire un test JMH qui crée un Logger que vous désactiverez dans le bloc statique
et tester sa vitesse.
-
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).
-
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.
-
Enfin, comment faire pour n'utiliser qu'un seul ClassValue qui marche à la fois
pour enable et level ?
Modifier le code en conséquence.
© Université de Marne-la-Vallée