Eine Anleitung zu Byte Buddy

1. Übersicht

Einfach ausgedrückt ist ByteBuddy eine Bibliothek zum dynamischen Generieren von Java-Klassen zur Laufzeit.

In diesem auf den Punkt gebrachten Artikel werden wir das Framework verwenden, um vorhandene Klassen zu manipulieren, neue Klassen bei Bedarf zu erstellen und sogar Methodenaufrufe abzufangen.

2. Abhängigkeiten

Fügen wir zunächst die Abhängigkeit zu unserem Projekt hinzu. Für Maven-basierte Projekte müssen wir diese Abhängigkeit zu unserer pom.xml hinzufügen :

 net.bytebuddy byte-buddy 1.7.1 

Für ein Gradle-basiertes Projekt müssen wir unserer build.gradle- Datei dasselbe Artefakt hinzufügen :

compile net.bytebuddy:byte-buddy:1.7.1

Die neueste Version finden Sie auf Maven Central.

3. Erstellen einer Java-Klasse zur Laufzeit

Beginnen wir mit der Erstellung einer dynamischen Klasse, indem wir eine vorhandene Klasse in Unterklassen unterteilen. Wir werden uns das klassische Hello World- Projekt ansehen .

In diesem Beispiel erstellen wir einen Typ ( Klasse ), der eine Unterklasse von Object.class ist, und überschreiben die Methode toString () :

DynamicType.Unloaded unloadedType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.isToString()) .intercept(FixedValue.value("Hello World ByteBuddy!")) .make();

Wir haben gerade eine Instanz von ByteBuddy erstellt. Dann haben wir die subclass () - API verwendet , um Object.class zu erweitern , und wir haben den toString () der Superklasse ( Object.class ) mithilfe von ElementMatchers ausgewählt .

Schließlich haben wir mit der intercept () -Methode unsere Implementierung von toString () bereitgestellt und einen festen Wert zurückgegeben.

Die Methode make () löst die Generierung der neuen Klasse aus.

Zu diesem Zeitpunkt ist unsere Klasse bereits erstellt, aber noch nicht in die JVM geladen. Es wird durch eine Instanz von DynamicType.Unloaded dargestellt , einer binären Form des generierten Typs.

Daher müssen wir die generierte Klasse in die JVM laden, bevor wir sie verwenden können:

Class dynamicType = unloadedType.load(getClass() .getClassLoader()) .getLoaded();

Jetzt können wir den dynamicType instanziieren und die toString () -Methode darauf aufrufen :

assertEquals( dynamicType.newInstance().toString(), "Hello World ByteBuddy!");

Beachten Sie, dass das Aufrufen von dynamicType.toString () nicht funktioniert, da dadurch nur die toString () -Implementierung von ByteBuddy.class aufgerufen wird .

Die newInstance () ist ein Java - Reflexionsmethode , die eine neue Instanz des Typs durch dieses repräsentiert schafft ByteBuddy Objekt; Ähnlich wie bei der Verwendung des neuen Schlüsselworts mit einem Konstruktor ohne Argumente.

Bisher konnten wir nur eine Methode in der Superklasse unseres dynamischen Typs überschreiben und einen eigenen festen Wert zurückgeben. In den nächsten Abschnitten werden wir uns mit der Definition unserer Methode mit benutzerdefinierter Logik befassen.

4. Methodendelegation und benutzerdefinierte Logik

In unserem vorherigen Beispiel geben wir einen festen Wert von der toString () -Methode zurück.

In der Realität erfordern Anwendungen eine komplexere Logik. Eine effektive Möglichkeit, benutzerdefinierte Logik für dynamische Typen zu vereinfachen und bereitzustellen, ist die Delegierung von Methodenaufrufen.

Erstellen wir einen dynamischen Typ, der die Unterklasse Foo.class mit der Methode sayHelloFoo () enthält :

public String sayHelloFoo() { return "Hello in Foo!"; }

Darüber hinaus erstellen wir eine andere Klasse Bar mit einer statischen sayHelloBar () von der gleichen Signatur und Rückgabetyp als sayHelloFoo () :

public static String sayHelloBar() { return "Holla in Bar!"; }

Nun lassen Sie uns alle Beschwörungen delegieren sayHelloFoo () zu sayHelloBar () mit ByteBuddy ‚s DSL. Auf diese Weise können wir unserer neu erstellten Klasse zur Laufzeit benutzerdefinierte Logik bereitstellen, die in reinem Java geschrieben ist:

String r = new ByteBuddy() .subclass(Foo.class) .method(named("sayHelloFoo") .and(isDeclaredBy(Foo.class) .and(returns(String.class)))) .intercept(MethodDelegation.to(Bar.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .sayHelloFoo(); assertEquals(r, Bar.sayHelloBar());

Durch Aufrufen von sayHelloFoo () wird die sayHelloBar () entsprechend aufgerufen .

Woher weiß ByteBuddy , welche Methode in Bar.class aufgerufen werden soll? Es wählt eine übereinstimmende Methode entsprechend der Methodensignatur, des Rückgabetyps, des Methodennamens und der Anmerkungen aus.

Die Methoden sayHelloFoo () und sayHelloBar () haben nicht denselben Namen, aber dieselbe Methodensignatur und denselben Rückgabetyp.

Wenn es in Bar.class mehr als eine aufrufbare Methode mit übereinstimmender Signatur und Rückgabetyp gibt, können wir die Mehrdeutigkeit mit der Annotation @BindingPriority auflösen.

@BindingPriority verwendet ein Integer-Argument. Je höher der Integer-Wert, desto höher ist die Priorität beim Aufrufen der jeweiligen Implementierung. Daher wird sayHelloBar () im folgenden Codefragment gegenüber sayBar () bevorzugt :

@BindingPriority(3) public static String sayHelloBar() { return "Holla in Bar!"; } @BindingPriority(2) public static String sayBar() { return "bar"; }

5. Methoden- und Felddefinition

Wir konnten Methoden überschreiben, die in der Superklasse unserer dynamischen Typen deklariert sind. Gehen wir weiter, indem wir unserer Klasse eine neue Methode (und ein Feld) hinzufügen.

Wir werden Java Reflection verwenden, um die dynamisch erstellte Methode aufzurufen:

Class type = new ByteBuddy() .subclass(Object.class) .name("MyClassName") .defineMethod("custom", String.class, Modifier.PUBLIC) .intercept(MethodDelegation.to(Bar.class)) .defineField("x", String.class, Modifier.PUBLIC) .make() .load( getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); Method m = type.getDeclaredMethod("custom", null); assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar()); assertNotNull(type.getDeclaredField("x"));

Wir haben eine Klasse mit dem Namen MyClassName erstellt , die eine Unterklasse von Object.class ist . Anschließend definieren wir eine benutzerdefinierte Methode, die einen String zurückgibt und über einen Modifikator für den öffentlichen Zugriff verfügt.

Just like we did in previous examples, we implemented our method by intercepting calls to it and delegating them to Bar.class that we created earlier in this tutorial.

6. Redefining an Existing Class

Although we have been working with dynamically created classes, we can work with already loaded classes as well. This can be done by redefining (or rebasing) existing classes and using ByteBuddyAgent to reload them into the JVM.

First, let's add ByteBuddyAgent to our pom.xml:

 net.bytebuddy byte-buddy-agent 1.7.1 

The latest version can be found here.

Now, let's redefine the sayHelloFoo() method we created in Foo.class earlier:

ByteBuddyAgent.install(); new ByteBuddy() .redefine(Foo.class) .method(named("sayHelloFoo")) .intercept(FixedValue.value("Hello Foo Redefined")) .make() .load( Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); Foo f = new Foo(); assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");

7. Conclusion

In diesem ausführlichen Handbuch haben wir uns eingehend mit den Funktionen der ByteBuddy- Bibliothek und ihrer Verwendung für die effiziente Erstellung dynamischer Klassen befasst.

Die Dokumentation bietet eine ausführliche Erläuterung des Innenlebens und anderer Aspekte der Bibliothek.

Und wie immer finden Sie die vollständigen Codefragmente für dieses Tutorial auf Github.