Hallo an alle. Heute möchten wir Ihnen eine Übersetzung eines Artikels mitteilen, der speziell für Studenten des
Java Developer- Kurses erstellt wurde.
In meinem Artikel über Spezifikationsmuster habe ich die zugrunde liegende Komponente, die bei der Implementierung sehr hilfreich war, nicht ausdrücklich erwähnt. Hier werde ich mehr über die
JavaBeanUtil- Klasse sprechen, mit der ich den Wert eines Objektfelds abgerufen habe. In diesem Beispiel war es
FxTransaction .

Natürlich werden Sie sagen, dass Sie
Apache Commons BeanUtils oder eine seiner Alternativen verwenden können, um das gleiche Ergebnis zu
erzielen . Aber ich war daran interessiert, mich damit zu beschäftigen, und was ich gelernt habe, funktioniert viel schneller als jede Bibliothek, die auf der Basis der bekannten
Java Reflection erstellt wurde .
Eine Technologie, die eine sehr langsame Reflexion vermeidet, ist der
invokedynamic
Bytecode-
invokedynamic
. Kurz gesagt, die Manifestation von
invokedynamic
(oder „indy“) war die schwerwiegendste Neuerung in Java 7, die den Weg für die Implementierung dynamischer Sprachen über die JVM mithilfe dynamischer Methodenaufrufe ebnete. Später in Java 8 ermöglichte dies auch
Lambda-Ausdrücke und Methodenreferenzen sowie eine verbesserte Verkettung von Zeichenfolgen in Java 9.
Kurz gesagt, die Technik, die ich unten beschreiben werde, verwendet
LambdaMetafactory und
MethodHandle, um dynamisch eine Implementierung der
Funktionsschnittstelle zu erstellen. Die Funktion ist die
einzige Methode , die einen Aufruf an die eigentliche Zielmethode delegiert, wobei der Code im Lambda definiert ist.
In diesem Fall ist die Zielmethode ein Getter, der direkten Zugriff auf das Feld hat, das wir lesen möchten. Außerdem muss ich sagen, dass Sie die folgenden Code-Schnipsel recht einfach finden, wenn Sie mit den Innovationen in Java 8 vertraut sind. Andernfalls kann der Code auf den ersten Blick kompliziert erscheinen.
Schauen Sie sich das provisorische JavaBeanUtil an
Die folgende
getFieldValue
Methode ist eine Dienstprogrammmethode zum Lesen von Werten aus einem JavaBean-Feld. Es benötigt ein JavaBean-Objekt und einen Feldnamen. Der Feldname kann einfach (z. B.
fieldA
) oder verschachtelt und durch
fieldA
getrennt sein (z. B.
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; }
Um die Leistung zu verbessern,
fieldName
ich eine dynamisch erstellte Funktion zwischen, die tatsächlich einen Wert aus einem Feld mit dem Namen
fieldName
. Wie Sie sehen, gibt es in der Methode
getCachedFunction
einen "schnellen" Pfad, der
ClassValue zum Zwischenspeichern verwendet, und einen "langsamen" Pfad "
createAndCacheFunction
", der ausgeführt wird, wenn kein Wert im Cache gefunden wird.
Die Methode
createFunctions
ruft eine Methode auf, die eine Liste von Funktionen zurückgibt, die mit
Function::andThen
verkettet werden. Das Verknüpfen von Funktionen in einer Kette kann als verschachtelte Aufrufe dargestellt werden, ähnlich wie
getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA()
. Danach
cacheAndGetFunction
wir die Funktion einfach in den Cache ein, indem
cacheAndGetFunction
Methode
cacheAndGetFunction
aufrufen.
Wenn Sie sich die Erstellung der Funktion genau ansehen, müssen wir die Felder im
path
wie folgt durchgehen:
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); }
Die obige Methode
createFunctions
für jedes Feld
fieldName
und die Klasse, in der es deklariert ist, ruft die Methode
createFunction
, die mit
javaBeanClass.getDeclaredMethods()
nach dem gewünschten Getter
javaBeanClass.getDeclaredMethods()
. Sobald der Getter gefunden wurde, wird er in ein Tupel-Tupel (Tupel aus der
Vavr- Bibliothek)
konvertiert , das den vom Getter zurückgegebenen Typ und eine dynamisch erstellte Funktion enthält, die sich so verhält, als wäre sie selbst ein Getter.
Ein Tupel wird mit der Methode
createTupleWithReturnTypeAndGetter
in Kombination mit der Methode
createCallSite
wie folgt erstellt:
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()); }
In den beiden oben genannten Methoden verwende ich eine Konstante namens
LOOKUP
, die nur auf
MethodHandles.Lookup verweist . Damit kann ich eine
direkte Verknüpfung zu einer Methode (direktes Methodenhandle) erstellen, die auf einem zuvor gefundenen Getter basiert. Schließlich wird das erstellte
MethodHandle an die Methode
createCallSite
, in der der Lambda-Body für die Funktion mit
LambdaMetafactory erstellt wird . Von dort können wir letztendlich eine Instanz von
CallSite erhalten , die der „Verwalter“ der Funktion ist.
Beachten Sie, dass Sie für Setter einen ähnlichen Ansatz verwenden können, indem Sie
BiFunction anstelle von
Function verwenden .
Benchmark
Um die Leistung zu messen, habe ich das wunderbare JMH-Tool (
Java Microbenchmark Harness ) verwendet, das wahrscheinlich Teil von JDK 12 ist (
Anmerkung des
Übersetzers: Ja, jmh ist in Java 9 enthalten ). Wie Sie wahrscheinlich wissen, ist das Ergebnis plattformabhängig. Als Referenz: Ich verwende
1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9
.
Zum Vergleich habe ich die
Apache Commons BeanUtils-Bibliothek ausgewählt , die den meisten Java-Entwicklern weithin bekannt ist, und eine ihrer Alternativen namens
Jodd BeanUtil , die angeblich
fast 20% schneller ist .
Der Benchmark-Code lautet wie folgt:
@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); } }
Der Benchmark definiert vier Szenarien für verschiedene Verschachtelungsebenen des Feldes. Für jedes Feld führt JMH 5 Iterationen von 3 Sekunden zum Aufwärmen und dann 5 Iterationen von 1 Sekunde für die eigentliche Messung durch. Jedes Szenario wird dreimal wiederholt, um bessere Messungen zu erhalten.
Ergebnisse
Beginnen wir mit den für das JDK
8u191
kompilierten Ergebnissen:
Oracle JDK 8u191Das Worst-Case-Szenario mit dem
invokedynamic
Ansatz ist viel schneller als das schnellste der beiden anderen Bibliotheken. Dies ist ein großer Unterschied, und wenn Sie an den Ergebnissen zweifeln, können Sie den
Quellcode jederzeit herunterladen und damit spielen, wie Sie möchten.
Nun wollen wir sehen, wie der gleiche Test mit
GraalVM EE 1.0.0-rc9.
GraalVM EE 1.0.0-rc9Die vollständigen Ergebnisse können
hier mit dem wunderschönen JMH Visualizer angezeigt werden.
Beobachtungen
Ein so großer Unterschied war auf die Tatsache zurückzuführen, dass der JIT-Compiler
CallSite
und
MethodHandle
gut kennt und sie im Gegensatz zum Reflection-Ansatz
MethodHandle
kann. Außerdem können Sie sehen, wie vielversprechend
GraalVM ist . Sein Compiler leistet wirklich großartige Arbeit, die die Reflexionsleistung erheblich verbessern kann.
Wenn Sie neugierig sind und tiefer eintauchen möchten, empfehlen wir Ihnen, den Code aus meinem
Github- Repository zu holen. Denken Sie daran, ich rate Ihnen nicht, ein selbst erstelltes
JavaBeanUtil
zu
JavaBeanUtil
, um es in der Produktion zu verwenden. Mein Ziel ist es einfach, mein Experiment und die Möglichkeiten zu zeigen, die wir durch
invokedynamic
.
Die Übersetzung ist beendet und wir laden alle zu einem
kostenlosen Webinar am 13. Juni ein, in dem wir untersuchen, wie der Docker für einen Java-Entwickler nützlich sein kann: wie man ein Docker-Image mit einer Java-Anwendung erstellt und wie man damit interagiert.