Eine Anleitung für den Byte-Buddy

Eine Anleitung zu Byte Buddy

1. Überblick

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

In diesem Artikel werden wir das Framework verwenden, um vorhandene Klassen zu bearbeiten, 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 unserenpom.xml hinzufügen:


    net.bytebuddy
    byte-buddy
    1.7.1

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

compile net.bytebuddy:byte-buddy:1.7.1

Die neueste Version finden Sie unterMaven 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 klassischeHello World-Projekt ansehen.

In diesem Beispiel erstellen wir einen Typ (Class), der eine Unterklasse vonObject.class ist, und überschreiben die MethodetoString():

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

Wir haben gerade eine Instanz vonByteBuddy. erstellt. Dann haben wirsubclass() API verwendet, umObject.class zu erweitern, und wir haben dietoString() der Superklasse (Object.classausgewählt ) s) unter Verwendung vonElementMatchers.

Schließlich haben wir mit der Methodeintercept() unsere Implementierung vontoString() bereitgestellt und einen festen Wert zurückgegeben.

Die Methodemake() 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 vonDynamicType.Unloaded dargestellt, die eine binäre Form des generierten Typs ist.

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 diedynamicType instanziieren und dietoString()-Methode darauf aufrufen:

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

Beachten Sie, dass das Aufrufen vondynamicType.toString() nicht funktioniert, da dadurch nur die Implementierung vonByteBuddy.class durchtoString()aufgerufen wird.

newInstance() ist eine Java-Reflektionsmethode, die eine neue Instanz des Typs erstellt, der durch diesesByteBuddy-Objekt dargestellt wird. Ähnlich wie bei der Verwendung des Schlüsselwortsnew 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 aus dertoString()-Methode zurück.

In der Realität erfordern Anwendungen komplexere Logik. Eine effektive Methode zur Erleichterung und Bereitstellung benutzerdefinierter Logik für dynamische Typen ist die Delegierung von Methodenaufrufen.

Erstellen wir einen dynamischen Typ, derFoo.class mit der MethodesayHelloFoo() unterordnet:

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

Erstellen wir außerdem eine weitere KlasseBar mit einem statischensayHelloBar() derselben Signatur und demselben Rückgabetyp wiesayHelloFoo():

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

Lassen Sie uns nun alle Aufrufe vonsayHelloFoo() ansayHelloBar() mitByteBuddy DSL delegieren. Auf diese Weise können wir unserer neu erstellten Klasse zur Laufzeit benutzerdefinierte Logik zur Verfügung stellen, 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 dersayHelloFoo() werden diesayHelloBar() entsprechend aufgerufen.

How does ByteBuddy know which method in Bar.class to invoke? Wählt eine übereinstimmende Methode entsprechend der Methodensignatur, des Rückgabetyps, des Methodennamens und der Anmerkungen aus.

Die MethodensayHelloFoo() undsayHelloBar()haben nicht denselben Namen, aber dieselbe Methodensignatur und denselben Rückgabetyp.

Wenn es inBar.class mehr als eine aufrufbare Methode mit übereinstimmender Signatur und Rückgabetyp gibt, können wir die Annotation von@BindingPriorityverwenden, um die Mehrdeutigkeit aufzulösen.

@BindingPriority verwendet ein ganzzahliges Argument - je höher der ganzzahlige Wert, desto höher ist die Priorität beim Aufrufen der jeweiligen Implementierung. Daher werdensayHelloBar() im folgenden Codeausschnitt gegenübersayBar() bevorzugt:

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

@BindingPriority(2)
public static String sayBar() {
    return "bar";
}

5. Methoden- und Felddefinition

Es ist uns gelungen, Methoden zu ü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 NamenMyClassName erstellt, die eine Unterklasse vonObject.class ist. Wir definieren dann eine Methode,custom,, dieString zurückgibt und einen Zugriffsmodifikator vonpublichat.

Genau wie in den vorherigen Beispielen haben wir unsere Methode implementiert, indem wir Aufrufe abgefangen und anBar.class delegiert haben, die wir zuvor in diesem Lernprogramm erstellt haben.

6. Bestehende Klasse neu definieren

Obwohl wir mit dynamisch erstellten Klassen gearbeitet haben, können wir auch mit bereits geladenen Klassen arbeiten. Dies kann erreicht werden, indem vorhandene Klassen neu definiert (oder neu basiert) undByteBuddyAgent verwendet werden, um sie erneut in die JVM zu laden.

Fügen wir zunächstByteBuddyAgent zu unserenpom.xml hinzu:


    net.bytebuddy
    byte-buddy-agent
    1.7.1

Die neueste Version kannfound here sein.

Definieren wir nun die MethodesayHelloFoo()neu, die wir zuvor inFoo.classerstellt haben:

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. Fazit

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

Seinedocumentation bieten eine ausführliche Erklärung des Innenlebens und anderer Aspekte der Bibliothek.

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