Bonjour à tous. Aujourd'hui, nous voulons partager avec vous une traduction d'un article préparé spécialement pour les étudiants du
cours Java Developer .
Dans mon article sur le
modèle de spécification , je n'ai pas spécifiquement mentionné le composant sous-jacent qui a beaucoup aidé à la mise en œuvre. Ici, je vais parler davantage de la classe
JavaBeanUtil que j'ai utilisée pour obtenir la valeur d'un champ d'objet. Dans cet exemple, c'était
FxTransaction .

Bien sûr, vous direz que vous pouvez utiliser
Apache Commons BeanUtils ou l'une de ses alternatives pour obtenir le même résultat. Mais j'étais intéressé à approfondir cela et ce que j'ai appris fonctionne beaucoup plus rapidement que n'importe quelle bibliothèque construite sur la base de la célèbre
Java Reflection .
Une technologie qui évite une réflexion très lente est l'
invokedynamic
bytecode
invokedynamic
. En bref, la manifestation de la
invokedynamic
(ou «indy») a été l'innovation la plus sérieuse de Java 7, qui a ouvert la voie à l'implémentation de langages dynamiques au-dessus de la JVM à l'aide d'invocations de méthodes dynamiques. Plus tard, dans Java 8, il a également permis des
expressions lambda et des références de méthode, ainsi qu'une amélioration de la concaténation des chaînes dans Java 9.
En bref, la technique que je vais décrire ci-dessous utilise
LambdaMetafactory et
MethodHandle pour créer dynamiquement une implémentation de l'interface
Function . La fonction est la
seule méthode qui délègue un appel à la méthode cible réelle avec du code défini à l'intérieur du lambda.
Dans ce cas, la méthode cible est un getter qui a un accès direct au champ que nous voulons lire. De plus, je dois dire que si vous connaissez bien les innovations apparues dans Java 8, vous trouverez les extraits de code ci-dessous assez simples. Sinon, le code peut sembler compliqué à première vue.
Jetez un œil au JavaBeanUtil de fortune
La méthode
getFieldValue
-
getFieldValue
est une méthode utilitaire utilisée pour lire les valeurs d'un champ JavaBean. Il prend un objet JavaBean et un nom de champ. Le nom du champ peut être simple (par exemple,
fieldA
) ou imbriqué, séparé par des points (par exemple,
nestedJavaBean.nestestJavaBean.fieldA
).
private static final Pattern FIELD_SEPARATOR = Pattern.compile("\\."); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static final ClassValue<Map<String, Function>> CACHE = new ClassValue<Map<String, Function>>() { @Override protected Map<String, Function> computeValue(Class<?> type) { return new ConcurrentHashMap<>(); } }; public static <T> T getFieldValue(Object javaBean, String fieldName) { return (T) getCachedFunction(javaBean.getClass(), fieldName).apply(javaBean); } private static Function getCachedFunction(Class<?> javaBeanClass, String fieldName) { final Function function = CACHE.get(javaBeanClass).get(fieldName); if (function != null) { return function; } return createAndCacheFunction(javaBeanClass, fieldName); } private static Function createAndCacheFunction(Class<?> javaBeanClass, String path) { return cacheAndGetFunction(path, javaBeanClass, createFunctions(javaBeanClass, path) .stream() .reduce(Function::andThen) .orElseThrow(IllegalStateException::new) ); } private static Function cacheAndGetFunction(String path, Class<?> javaBeanClass, Function functionToBeCached) { Function cachedFunction = CACHE.get(javaBeanClass).putIfAbsent(path, functionToBeCached); return cachedFunction != null ? cachedFunction : functionToBeCached; }
Pour améliorer les performances, je mets en cache une fonction créée dynamiquement qui lira réellement une valeur dans un champ nommé
fieldName
. Dans la méthode
getCachedFunction
, comme vous pouvez le voir, il existe un chemin «rapide» qui utilise
ClassValue pour la mise en cache et un chemin «slow»
createAndCacheFunction
, qui s'exécute si aucune valeur n'est trouvée dans le cache.
La méthode
createFunctions
appelle une méthode qui retourne une liste de fonctions qui seront chaînées à l'aide de
Function::andThen
. Les fonctions de liaison les unes aux autres dans une chaîne peuvent être représentées comme des appels imbriqués, similaires à
getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA()
. Après cela, nous mettons simplement la fonction dans le cache en appelant la méthode
cacheAndGetFunction
.
Si vous regardez de près la création de la fonction, nous devons parcourir les champs dans
path
comme suit:
private static List<Function> createFunctions(Class<?> javaBeanClass, String path) { List<Function> functions = new ArrayList<>(); Stream.of(FIELD_SEPARATOR.split(path)) .reduce(javaBeanClass, (nestedJavaBeanClass, fieldName) -> { Tuple2<? extends Class, Function> getFunction = createFunction(fieldName, nestedJavaBeanClass); functions.add(getFunction._2); return getFunction._1; }, (previousClass, nextClass) -> nextClass); return functions; } private static Tuple2<? extends Class, Function> createFunction(String fieldName, Class<?> javaBeanClass) { return Stream.of(javaBeanClass.getDeclaredMethods()) .filter(JavaBeanUtil::isGetterMethod) .filter(method -> StringUtils.endsWithIgnoreCase(method.getName(), fieldName)) .map(JavaBeanUtil::createTupleWithReturnTypeAndGetter) .findFirst() .orElseThrow(IllegalStateException::new); }
La méthode
createFunctions
ci-dessus pour chaque champ
fieldName
et la classe dans laquelle il est déclaré appelle la méthode
createFunction
, qui recherche le getter souhaité à l'aide de
javaBeanClass.getDeclaredMethods()
. Une fois le getter trouvé, il est converti en tuple
Tuple (Tuple de la bibliothèque
Vavr ), qui contient le type renvoyé par le getter, et une fonction créée dynamiquement qui se comportera comme s'il s'agissait d'un getter lui-même.
Un tuple est créé avec la méthode
createTupleWithReturnTypeAndGetter
en combinaison avec la méthode
createCallSite
comme suit:
private static Tuple2<? extends Class, Function> createTupleWithReturnTypeAndGetter(Method getterMethod) { try { return Tuple.of( getterMethod.getReturnType(), (Function) createCallSite(LOOKUP.unreflect(getterMethod)).getTarget().invokeExact() ); } catch (Throwable e) { throw new IllegalArgumentException("Lambda creation failed for getterMethod (" + getterMethod.getName() + ").", e); } } private static CallSite createCallSite(MethodHandle getterMethodHandle) throws LambdaConversionException { return LambdaMetafactory.metafactory(LOOKUP, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), getterMethodHandle, getterMethodHandle.type()); }
Dans les deux méthodes ci-dessus, j'utilise une constante appelée
LOOKUP
, qui n'est qu'une référence à
MethodHandles.Lookup . Avec lui, je peux créer un
lien direct vers une méthode (poignée de méthode directe) basée sur un getter trouvé précédemment. Enfin, le
MethodHandle créé est transmis à la méthode
createCallSite
, dans laquelle le corps lambda est créé pour la fonction à l'aide de
LambdaMetafactory . De là, en fin de compte, nous pouvons obtenir une instance de
CallSite , qui est le «gardien» de la fonction.
Notez que pour les setters, vous pouvez utiliser une approche similaire en utilisant
BiFunction au lieu de
Function .
Benchmark
Pour mesurer les performances, j'ai utilisé le merveilleux outil JMH (
Java Microbenchmark Harness ), qui fait probablement partie de JDK 12 (
Note du traducteur: oui, jmh est inclus dans java 9 ). Comme vous le savez probablement, le résultat dépend de la plate-forme, donc pour référence: j'utiliserai
1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9
.
À titre de comparaison, j'ai choisi la
bibliothèque Apache Commons BeanUtils , largement connue de la plupart des développeurs Java, et l'une de ses alternatives appelée
Jodd BeanUtil , qui serait
presque 20% plus rapide .
Le code de référence est le suivant:
@Fork(3) @Warmup(iterations = 5, time = 3) @Measurement(iterations = 5, time = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class JavaBeanUtilBenchmark { @Param({ "fieldA", "nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.nestedJavaBean.fieldA" }) String fieldName; JavaBean javaBean; @Setup public void setup() { NestedJavaBean nestedJavaBean3 = NestedJavaBean.builder().fieldA("nested-3").build(); NestedJavaBean nestedJavaBean2 = NestedJavaBean.builder().fieldA("nested-2").nestedJavaBean(nestedJavaBean3).build(); NestedJavaBean nestedJavaBean1 = NestedJavaBean.builder().fieldA("nested-1").nestedJavaBean(nestedJavaBean2).build(); javaBean = JavaBean.builder().fieldA("fieldA").nestedJavaBean(nestedJavaBean1).build(); } @Benchmark public Object invokeDynamic() { return JavaBeanUtil.getFieldValue(javaBean, fieldName); } @Benchmark public Object apacheBeanUtils() throws Exception { return PropertyUtils.getNestedProperty(javaBean, fieldName); } @Benchmark public Object joddBean() { return BeanUtil.declared.getProperty(javaBean, fieldName); } public static void main(String... args) throws IOException, RunnerException { Main.main(args); } }
Le benchmark définit quatre scénarios pour différents niveaux d'imbrication du champ. Pour chaque champ, JMH effectuera 5 itérations de 3 secondes pour s'échauffer, puis 5 itérations de 1 seconde pour la mesure réelle. Chaque scénario sera répété 3 fois pour obtenir de meilleures mesures.
Résultats
Commençons par les résultats compilés pour le JDK
8u191
:
Oracle JDK 8u191Le pire des scénarios utilisant l'approche
invokedynamic
est beaucoup plus rapide que le plus rapide des deux autres bibliothèques. C'est une énorme différence, et si vous doutez des résultats, vous pouvez toujours télécharger le
code source et jouer avec comme vous le souhaitez.
Voyons maintenant comment fonctionne le même test avec
GraalVM EE 1.0.0-rc9.
GraalVM EE 1.0.0-rc9Les résultats complets peuvent être vus
ici avec le magnifique visualiseur JMH.
Observations
Une telle différence est due au fait que le compilateur JIT connaît bien
CallSite
et
MethodHandle
et peut les
MethodHandle
, contrairement à l'approche par réflexion. De plus, vous pouvez voir à quel point
GraalVM est prometteur. Son compilateur fait un travail vraiment génial qui peut améliorer considérablement les performances de réflexion.
Si vous êtes curieux et que vous souhaitez approfondir, je vous encourage à récupérer le code de mon référentiel
Github . Gardez à l'esprit, je ne vous conseille pas de faire un
JavaBeanUtil
self-made pour l'utiliser en production. Mon objectif est simplement de montrer mon expérience et les possibilités que nous pouvons tirer de la
invokedynamic
.
La traduction est terminée, et nous invitons tout le monde à un
webinaire gratuit le 13 juin, dans lequel nous examinerons comment le Docker peut être utile pour un développeur Java: comment créer une image docker avec une application java et comment interagir avec elle.