[JAVA] [Übersetzung] Byte Buddy Tutorial

https://bytebuddy.net/#/tutorial Google Übersetzung des Bytebuddy-Tutorials

Why runtime code generation?

Die Java-Sprache kommt mit einem relativ strengen Typsystem. In Java müssen alle Variablen und Objekte von einem bestimmten Typ sein. Wenn Sie versuchen, einen inkompatiblen Typ zuzuweisen, wird immer eine Fehlermeldung angezeigt. Diese Fehler werden normalerweise vom Java-Compiler oder zumindest von der Java-Laufzeit ausgegeben, wenn Sie einen Typ falsch umwandeln. Eine derart strenge Eingabe ist häufig wünschenswert, beispielsweise beim Schreiben von Geschäftsanwendungen. Geschäftsdomänen können normalerweise explizit so beschrieben werden, dass jeder Domäneneintrag seinen eigenen Typ darstellt. Auf diese Weise können Sie mit Java sehr lesbare und robuste Anwendungen erstellen, bei denen Fehler in der Nähe der Quelle erkannt werden. Insbesondere das Typensystem von Java ist für die Popularität von Java in der Unternehmensprogrammierung verantwortlich.

Durch die Durchsetzung dieses strengen Typsystems legt Java jedoch Einschränkungen fest, die den Umfang der Sprache in anderen Domänen einschränken. Wenn Sie beispielsweise eine generische Bibliothek schreiben, die von anderen Java-Anwendungen verwendet werden soll, kennen wir diese Typen nicht, wenn unsere Bibliothek kompiliert wird, also Ihre Anwendung Der in definierte Typ kann nicht referenziert werden. Die Java-Klassenbibliothek verfügt über eine Reflection-API zum Aufrufen von Methoden oder Zugriffsfeldern im unbekannten Code des Benutzers. Mit der Reflection-API können Sie unbekannte Typen, Aufrufmethoden und Zugriffsfelder überprüfen. Leider hat die Verwendung der Reflection-API zwei Hauptnachteile.

Hier kann uns die Generierung von Laufzeitcode helfen. Auf diese Weise können Sie einige Funktionen emulieren, auf die normalerweise nur beim Programmieren in einer dynamischen Sprache zugegriffen werden kann, ohne die statische Typprüfung von Java zu unterbrechen. Auf diese Weise können Sie das Beste aus beiden Welten maximieren und die Laufzeitleistung weiter verbessern. Schauen wir uns zum besseren Verständnis dieses Problems das oben erwähnte Beispiel für die Implementierung der Sicherheitsbibliothek auf Methodenebene an.

Writing a security library

Geschäftsanwendungen können wachsen und es kann schwierig sein, sich einen Überblick über den Aufrufstapel innerhalb der Anwendung zu verschaffen. Dies kann ein Problem sein, wenn Ihre Anwendung wichtige Methoden enthält, die nur unter bestimmten Bedingungen aufgerufen werden sollten. Stellen Sie sich eine Geschäftsanwendung vor, die eine Rücksetzfunktion implementiert, mit der Sie alles aus der Datenbank Ihrer Anwendung löschen können.

class Service {
  void deleteEverything() {
    // delete everything ...
  }
}

Solche Zurücksetzungen sollten natürlich nur vom Administrator und niemals vom durchschnittlichen Benutzer unserer Anwendung durchgeführt werden. Durch die Analyse unseres Quellcodes können wir natürlich sicher sein, dass dies nicht geschieht. Wir können jedoch davon ausgehen, dass unsere Anwendung in Zukunft wächst und sich ändert. Daher müssen wir ein strengeres Sicherheitsmodell implementieren, bei dem Methodenaufrufe durch explizite Überprüfungen des aktuellen Benutzers der Anwendung geschützt sind. Verwenden Sie im Allgemeinen ein Sicherheitsframework, um zu verhindern, dass diese Methode von einer anderen Person als dem Administrator aufgerufen wird.

Angenommen, Sie verwenden zu diesem Zweck ein Sicherheitsframework mit einer öffentlichen API wie folgt:

@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
  String user();
}
 
class UserHolder {
  static String user;
}
 
interface Framework {
  <T> T secure(Class<T> type);
}

Für dieses Framework müssen Sie die Annotation "Gesichert" verwenden, um Methoden zu markieren, auf die nur bestimmte Benutzer zugreifen können. Mit "UserHolder" wird global definiert, welcher Benutzer derzeit bei der Anwendung angemeldet ist. Die Framework -Schnittstelle ermöglicht die Erstellung geschützter Instanzen durch Aufrufen des Standardkonstruktors eines bestimmten Typs. Natürlich ist dieses Framework sehr einfach, aber im Prinzip funktioniert ein Sicherheitsframework wie das beliebte Spring Security so. Ein Merkmal dieses Sicherheitsframeworks ist, dass der Benutzertyp beibehalten wird. Durch die Kontraktion unserer "Framework" -Schnittstelle versprechen wir, eine Instanz eines beliebigen Typs "T" zurückzugeben, den der Benutzer erhält. Auf diese Weise kann der Benutzer mit seinem eigenen Typ interagieren, als ob das Sicherheitsframework nicht vorhanden wäre. In einer Testumgebung können Benutzer sogar ungeschützte Instanzen ihres Typs erstellen und diese Instanzen sogar anstelle geschützter Instanzen verwenden. Sie werden zustimmen, dass dies wirklich nützlich ist! Es ist bekannt, dass solche Frameworks mit POJOs (gewöhnlichen Java-Objekten) interagieren. Dies ist ein Begriff, der geprägt wurde, um ein nicht aufdringliches Framework zu beschreiben, das dem Benutzer keinen eigenen Typ auferlegt.

Im Moment wissen wir, dass der an Framework übergebene Typ nur T = Service ist und dass die deleteEverything -Methode mit@Secured ("ADMIN")versehen ist. Vorstellen. Auf diese Weise können Sie eine geschützte Version dieses bestimmten Typs einfach implementieren, indem Sie sie einfach in Unterklassen unterteilen.

class SecuredService extends Service {
  @Override
  void deleteEverything() {
    if(UserHolder.user.equals("ADMIN")) {
      super.deleteEverything();
    } else {
      throw new IllegalStateException("Not authorized");
    }
  }
}

Mit dieser zusätzlichen Klasse können Sie das Framework wie folgt implementieren:

class HardcodedFrameworkImpl implements Framework {
  @Override
  public <T> T secure(Class<T> type) {
    if(type == Service.class) {
      return (T) new SecuredService();
    } else {
      throw new IllegalArgumentException("Unknown: " + type);
    }
  }
}

Natürlich ist diese Implementierung nicht sehr nützlich. Die Signatur der sicheren Methode deutete darauf hin, dass diese Methode jede Art von Sicherheit bieten könnte. In Wirklichkeit löst sie jedoch eine Ausnahme aus, wenn etwas anderes als ein bekannter Dienst auftritt. Dazu muss unsere Sicherheitsbibliothek auch über diesen bestimmten Diensttyp informiert sein, wenn die Bibliothek kompiliert wird. Dies ist offensichtlich keine praktikable Lösung für die Implementierung des Frameworks. Wie können wir dieses Problem lösen? Dies ist ein Tutorial zur Codegenerierungsbibliothek, damit Sie die Antwort erraten können. Erstellen Sie bei Bedarf zur Laufzeit eine Unterklasse, wenn die Klasse "Service" für das Sicherheitsframework sichtbar wird, indem Sie die Methode "Secure" aufrufen. Bei der Codegenerierung können Sie einen bestimmten Typ verwenden, ihn zur Laufzeit in Unterklassen unterteilen und die zu schützende Methode überschreiben. In diesem Fall überschreiben wir alle mit "@ Secured" annotierten Methoden und lesen den erforderlichen Benutzer aus der "user" -Eigenschaft der Annotation. Viele gängige Java-Frameworks werden auf ähnliche Weise implementiert.

General information

Verwenden Sie die Codegenerierung sorgfältig, bevor Sie sich mit der Codegenerierung und Byte Buddy vertraut machen. Java-Typen sind für JVMs etwas ganz Besonderes und werden häufig nicht mit Müll gesammelt. Verwenden Sie die Codegenerierung daher nicht zu häufig und verwenden Sie generierten Code nur, wenn der generierte Code die einzige Lösung zur Lösung des Problems ist. Wenn Sie jedoch einen unbekannten Typ wie im vorherigen Beispiel erweitern müssen, ist die Codegenerierung wahrscheinlich die einzige Option. Frameworks für Sicherheit, Transaktionsverwaltung, objektrelationale Zuordnung oder Verspottung sind typische Benutzer von Codegenerierungsbibliotheken.

Natürlich ist Byte Buddy nicht die erste Bibliothek zum Generieren von Code auf einer JVM. Byte Buddy glaubt jedoch, einige Tricks zu kennen, die in anderen Frameworks nicht anwendbar sind. Der allgemeine Zweck von Byte Buddy besteht darin, deklarativ zu arbeiten, indem sowohl die domänenspezifische Sprache als auch die Verwendung von Anmerkungen im Mittelpunkt stehen. Andere Codegenerierungsbibliotheken für JVMs, von denen wir wissen, dass sie nicht auf diese Weise funktionieren. Sie sollten sich jedoch einige andere Frameworks für die Codegenerierung ansehen, um herauszufinden, welches für Sie am besten geeignet ist. Insbesondere die folgenden Bibliotheken sind im Java-Bereich weit verbreitet.

Bewerten Sie das Framework selbst, aber wir glauben, dass Byte Buddy die Funktionen und Annehmlichkeiten bietet, die Sie sonst vergeblich finden würden. Byte Buddy wird mit einer ausdrucksstarken domänenspezifischen Sprache geliefert, mit der Sie hochgradig benutzerdefinierte Laufzeitklassen erstellen können, indem Sie einfachen Java-Code schreiben oder Ihren eigenen Code stark eingeben. tun. Gleichzeitig ist Byte Buddy in hohem Maße anpassbar und schränkt die sofort einsatzbereiten Funktionen nicht ein. Bei Bedarf können Sie auch benutzerdefinierte Bytecodes für die implementierten Methoden definieren. Sie können jedoch viel tun, ohne sich in das Framework zu vertiefen, ohne zu wissen, was der Bytecode ist und wie er funktioniert. Hast du zum Beispiel Hello World gesehen? Ein Beispiel: Byte Buddy ist so einfach zu bedienen.

Natürlich sind bei der Auswahl einer Codegenerierungsbibliothek nicht nur komfortable APIs zu berücksichtigen. Für viele Anwendungen bestimmen wahrscheinlich die Laufzeitmerkmale des generierten Codes die beste Wahl. Die Ausführungszeit zum Erstellen einer dynamischen Klasse kann auch ein Problem sein, das über die Ausführungszeit des generierten Codes selbst hinausgeht. Wir behaupten, der Schnellste zu sein. Es ist so einfach wie schwierig, eine gültige Metrik für die Bibliotheksgeschwindigkeit bereitzustellen. Trotzdem möchte ich eine solche Metrik als Grundorientierung angeben. Beachten Sie jedoch, dass diese Ergebnisse nicht unbedingt zu bestimmten Anwendungsfällen führen, in denen einzelne Metriken implementiert werden müssen.

Bevor wir über Metriken sprechen, werfen wir einen Blick auf die Rohdaten. Die folgende Tabelle zeigt die durchschnittliche Ausführungszeit in Nanosekunden für Operationen, deren Standardabweichungen in geschweiften Klammern angegeben sind.

baseline Byte Buddy cglib Javassist Java proxy
trivial class creation 0.003 (0.001) 142.772 (1.390) 515.174 (26.753) 193.733 (4.430) 70.712 (0.645)
interface implementation 0.004 (0.001) 1'126.364 (10.328) 960.527 (11.788) 1'070.766 (59.865) 1'060.766 (12.231)
stub method invocation 0.002 (0.001) 0.002 (0.001) 0.003 (0.001) 0.011 (0.001) 0.008 (0.001)
class extension 0.004 (0.001) 885.983 (7.901) 5'408.329 (52.437) 1'632.730 (52.737) 683.478 (6.735)
super method invocation 0.004 (0.001) 0.004 (0.001) 0.004 (0.001) 0.021 (0.001) 0.025 (0.001) -

Wie statische Compiler stehen auch die Codegenerierungsbibliotheken vor einem Kompromiss zwischen schneller Codegenerierung und schneller Codegenerierung. Bei der Auswahl zwischen diesen widersprüchlichen Zielen liegt der Schwerpunkt von Byte Buddy auf der Generierung von Code mit minimaler Ausführungszeit. Das Erstellen und Bearbeiten von Typen ist normalerweise in keinem Programm üblich und hat keinen wesentlichen Einfluss auf Anwendungen mit langer Laufzeit. Insbesondere Ladeklassen und Instrumentierungsklassen sind die zeitaufwändigsten und unvermeidbarsten Schritte bei der Ausführung eines solchen Codes.

Der erste Benchmark in der obigen Tabelle misst die Ausführungszeit der Bibliothek für die Unterklasse "Objekt", ohne die Methode zu implementieren oder zu überschreiben. Dies vermittelt den Eindruck eines allgemeinen Overheads einer Bibliothek bei der Codegenerierung. In diesem Benchmark bietet der Java-Proxy eine bessere Leistung als andere Bibliotheken. Optimierungen sind nur möglich, wenn die Schnittstelle immer erweitert wird. Byte Buddy überprüft auch generische Typen und Anmerkungsklassen und löst zusätzliche Laufzeiten aus. Dieser Leistungsaufwand findet sich auch in anderen Benchmarks zum Erstellen von Klassen. Der Benchmark (2a) zeigt die gemessene Laufzeit zum Erstellen (und Laden) einer Klasse, die eine einzelne Schnittstelle mit 18 Methoden implementiert, und (2b) zeigt die für diese Klasse generierten Methoden. Zeigt die Ausführungszeit an. In ähnlicher Weise zeigt (3a) einen Benchmark zum Erweitern einer Klasse mit denselben 18 implementierten Methoden. Byte Buddy bietet zwei Benchmarks. Dies ist auf mögliche Optimierungen für Interceptors zurückzuführen, die immer Supermethoden ausführen. Auf Kosten der Zeit während der Klassenerstellung erreicht die Ausführungszeit einer mit Byte Buddy erstellten Klasse normalerweise die Basislinie. Das heißt, die Instrumentierung verursacht keinen Overhead. Beachten Sie, dass Byte Buddy beim Erstellen von Klassen besser ist als andere Codegenerierungsbibliotheken, wenn die Metadatenverarbeitung deaktiviert ist. Da die Ausführungszeit der Codegenerierung im Vergleich zur Gesamtausführungszeit des Programms jedoch sehr kurz ist, ist ein solches Opt-out nicht verfügbar, da es auf Kosten der Komplikation des Bibliothekscodes nur eine geringe Leistung bietet.

Schließlich misst unsere Metrik die Leistung von Java-Code, der zuvor vom Just-in-Time-Compiler der JVM (http://en.wikipedia.org/wiki/Just-in-time_compilation) optimiert wurde. Bitte beachten Sie insbesondere. Wenn der Code nur gelegentlich ausgeführt wird, ist die Leistung schlechter als in den obigen Metriken angegeben. In diesem Fall ist die Codeleistung jedoch weniger wichtig. Der Code für diese Metrik wird mit Byte Buddy verteilt, sodass Sie diese Metrik auf Ihrem eigenen Computer ausführen und die oben genannten Zahlen abhängig von der Verarbeitungsleistung Ihres Computers anpassen können. Betrachten Sie aus diesem Grund die obigen Zahlen als relatives Maß für den Vergleich verschiedener Bibliotheken und nicht als absolute Interpretation. Während Sie Ihren Byte Buddy weiterentwickeln, sollten Sie diese Metriken überwachen, um Leistungseinbußen beim Hinzufügen neuer Funktionen zu vermeiden.

Im nächsten Tutorial werden wir Sie durch die Funktionen von Byte Buddy führen. Beginnen Sie mit den allgemeineren Funktionen, die die meisten Benutzer am wahrscheinlichsten verwenden. Danach werden wir uns mit immer weiter fortgeschrittenen Themen befassen und eine kurze Einführung in Java-Bytecode und Klassendateiformate geben. Und lassen Sie sich nicht entmutigen, wenn Sie schnell auf das folgende Material vorspulen. Mit der Standard-API von Byte Buddy können Sie fast alles tun, ohne die Details einer JVM verstehen zu müssen. Lesen Sie weiter, um mehr über die Standard-API zu erfahren.

Creating a class

Die von Byte Buddy erstellten Typen werden von einer Instanz der ByteBuddy-Klasse ausgegeben. Rufen Sie einfach "new ByteBuddy ()" auf, um eine neue Instanz zu erstellen, und schon können Sie loslegen. Hoffentlich verwenden Sie eine Entwicklungsumgebung, in der Sie Vorschläge zu den Methoden erhalten, die Sie für ein bestimmtes Objekt aufrufen können. Auf diese Weise können Sie die IDE verwenden, um den Prozess zu steuern, ohne die API der Klasse in Javadoc von Byte Buddy manuell zu durchsuchen. Wie bereits erwähnt, bietet Byte Buddy eine domänenspezifische Sprache, die so gut wie möglich lesbar sein soll. Mit IDE-Tipps können Sie daher fast immer in die richtige Richtung gehen. Aber das reicht, lassen Sie uns die erste Klasse erstellen, wenn Sie ein Java-Programm ausführen.

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

Das obige Codebeispiel erstellt offensichtlich eine neue Klasse, die den Objekttyp erweitert. Dieser dynamisch erstellte Typ entspricht einer Java-Klasse, die einfach "Object" erweitert, ohne explizit eine Methode, ein Feld oder einen Konstruktor zu implementieren. Möglicherweise haben Sie bemerkt, dass Sie nicht einmal die dynamisch generierten Typen benannt haben, was normalerweise beim Definieren von Java-Klassen erforderlich ist. Natürlich können Sie Ihren Typ ganz einfach explizit benennen:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

Aber was passiert ohne expliziten Namen? Byte Buddy lebt von Beyond Customs (http://en.wikipedia.org/wiki/Convention_over_configuration) und bietet praktische Standardeinstellungen. Für Typnamen bietet die Standardeinstellung "Byte Buddy" eine "NamingStrategy", die zufällig einen Klassennamen basierend auf dem Namen der Oberklasse des dynamischen Typs erstellt. Darüber hinaus wird der Name so definiert, dass er sich im selben Paket wie die Oberklasse befindet, sodass private Methoden für direkte Superklassenpakete immer als dynamische Typen erkannt werden. Wenn Sie beispielsweise den Typ "example.Foo" unterordnen, entspricht der generierte Name etwa "example.Foo $ ByteBuddy $ 1376491271`. Hier ist die numerische Folge zufällig. Es gibt eine Ausnahme von dieser Regel, wenn ein Typ aus einem "java.lang" -Paket mit einem Typ wie "Object" untergeordnet wird. Das Java-Sicherheitsmodell erlaubt keine benutzerdefinierten Typen in diesem Namespace. Daher stellt das Standard-Namensschema solchen Typnamen "net.bytebuddy.renamed" voran.

Dieses Standardverhalten ist für Sie möglicherweise nicht geeignet. Dank der Konventionen für Konfigurationsprinzipien können Sie das Standardverhalten jederzeit nach Bedarf ändern. Hier wurde die ByteBuddy Klasse eingeführt. Erstellen Sie Standardeinstellungen, indem Sie eine neue ByteBuddy () -Instanz erstellen. Sie können es an Ihre individuellen Bedürfnisse anpassen, indem Sie Methoden mit dieser Einstellung aufrufen. Lass uns das versuchen:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    public String subclass(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

Im obigen Codebeispiel haben wir eine neue Einstellung erstellt, bei der sich die Typbenennungsstrategie von der Standardeinstellung unterscheidet. Die anonyme Klasse wird implementiert, um die Zeichenfolge "i.love.ByteBuddy" einfach mit dem einfachen Namen der Basisklasse zu verketten. Wenn Sie den Typ "Objekt" unterordnen, wird der dynamische Typ daher "i.love.ByteBuddy.Object" genannt. Seien Sie vorsichtig, wenn Sie Ihr eigenes Namensschema erstellen. Virtuelle Java-Maschinen verwenden Namen, um zwischen Typen zu unterscheiden. Daher möchten wir Namenskonflikte vermeiden. Wenn Sie das Benennungsverhalten anpassen müssen, können Sie das in Byte Buddy integrierte NamingStrategy.SuffixingRandom verwenden, um es so anzupassen, dass es Präfixe enthält, die für Ihre Anwendung aussagekräftiger sind als die Standardeinstellungen.

Domain specific language and immutability

Nachdem wir die domänenspezifische Sprache von Byte Buddy in Aktion gesehen haben, müssen wir uns kurz ansehen, wie diese Sprache implementiert ist. Ein Detail, das Sie über die Implementierung wissen müssen, ist, dass die Sprache auf unveränderlichen Objekten basiert (http://en.wikipedia.org/wiki/Immutable_object). Tatsächlich ist fast jede Klasse, die im Byte Buddy-Namespace vorhanden ist, unveränderlich, aber in einigen Fällen war es nicht möglich, den Typ unveränderlich zu machen. Wir empfehlen, dass Sie dieses Prinzip befolgen, wenn Sie benutzerdefinierte Funktionen in Byte Buddy implementieren.

Seien Sie bei der oben erwähnten Unveränderlichkeit vorsichtig, wenn Sie beispielsweise eine ByteBuddy-Instanz konfigurieren. Beispielsweise könnten Sie den folgenden Fehler machen:

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.withNamingStrategy(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

Dynamische Typen sollten mit der (wahrscheinlich) definierten benutzerdefinierten Namensstrategie "new NamingStrategy.SuffixingRandom (" Suffix ")" generiert werden. Wenn Sie die Methode "withNamingStrategy" aufrufen, anstatt die in der Variablen "byteBuddy" gespeicherte Instanz zu ändern, wird eine angepasste "ByteBuddy" -Instanz zurückgegeben, die jedoch verloren geht. Infolgedessen werden dynamische Typen mit der zuerst erstellten Standardkonfiguration erstellt.

Redefining and rebasing existing classes

Bisher haben wir Ihnen gezeigt, wie Sie mit Byte Buddy Unterklassen vorhandener Klassen erstellen. Sie können jedoch auch dieselbe API verwenden, um eine vorhandene Klasse zu erweitern. Solche Verbesserungen sind in zwei verschiedenen Geschmacksrichtungen erhältlich.

type redefinition

Wenn Sie eine Klasse neu definieren, können Sie mit Byte Buddy eine vorhandene Klasse ändern, indem Sie Felder und Methoden hinzufügen oder eine vorhandene Methodenimplementierung ersetzen. Bestehende Methodenimplementierungen gehen jedoch verloren, wenn sie durch eine andere Implementierung ersetzt werden. Zum Beispiel, wenn Sie den folgenden Typ neu definieren

class Foo {
  String bar() { return "bar"; }
}

Da die "bar" -Methode "qux" "zurückgibt, gehen die Informationen, die diese Methode ursprünglich" bar "zurückgegeben hat, insgesamt verloren.

type rebasing

Wenn Sie eine Klasse neu gründen, behält Byte Buddy alle Methodenimplementierungen der neu basierten Klasse bei. Anstatt die überschriebene Methode wie bei einer Neudefinition des Typs zu verwerfen, kopiert Byte Buddy alle diese Methodenimplementierungen in eine umbenannte private Methode mit einer kompatiblen Signatur. .. Auf diese Weise geht die Implementierung nicht verloren und die neu basierte Methode kann den ursprünglichen Code weiterhin aufrufen, indem diese umbenannten Methoden aufgerufen werden. Somit kann die obige Klasse "Foo" wie folgt umbasiert werden:

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

Die Informationen, die die "bar" -Methode ursprünglich "bar" zurückgegeben hat, werden in einer anderen Methode gespeichert und bleiben zugänglich. Beim erneuten Basieren einer Klasse behandelt Byte Buddy alle Methodendefinitionen, z. B. beim Definieren von Unterklassen. Das heißt, wenn Sie versuchen, die Supermethodenimplementierung einer Rebase-Methode aufzurufen, wird die Rebase-Methode aufgerufen. Stattdessen wird diese virtuelle Superklasse schließlich auf den oben gezeigten neu basierten Typ abgeflacht.

Das erneute Basieren, Neudefinieren oder Unterklassifizieren erfolgt mit derselben API, die von der Schnittstelle "DynamicType.Builder" definiert wird. Auf diese Weise können Sie beispielsweise eine Klasse als Unterklasse definieren und diese Definition später ändern, um stattdessen die neu basierte Klasse darzustellen. Dies kann erreicht werden, indem einfach ein Wort in der domänenspezifischen Sprache von Byte Buddy geändert wird. Diese Methode wendet einen der möglichen Ansätze an

new ByteBuddy().subclass(Foo.class)
new ByteBuddy().redefine(Foo.class)
new ByteBuddy().rebase(Foo.class)

Der Rest des im Rest dieses Tutorials beschriebenen Definitionsprozesses ist transparent. Unterklassendefinitionen sind Java-Entwicklern ein bekanntes Konzept. Daher werden alle folgenden Erklärungen und Beispiele für die domänenspezifischen Sprachen von Byte Buddy durch das Erstellen von Unterklassen angezeigt. Beachten Sie jedoch, dass alle Klassen durch Neudefinition oder Neubasierung auf dieselbe Weise definiert werden können.

Loading a class

Bisher habe ich nur dynamische Typen definiert und erstellt, aber ich habe sie nicht verwendet. Die von Byte Buddy erstellten Typen werden durch eine Instanz von "DynamicType.Unloaded" dargestellt. Wie der Name schon sagt, werden diese Typen nicht in virtuelle Java-Maschinen geladen. Stattdessen werden die von Byte Buddy erstellten Klassen im Binärformat im Java-Klassendateiformat dargestellt. Es liegt also an Ihnen, was Sie mit dem generierten Typ tun möchten. Sie können Byte Buddy beispielsweise über ein Build-Skript ausführen, das nur die Klassen generiert, die Sie erweitern möchten, bevor Sie Ihre Java-Anwendung bereitstellen. Zu diesem Zweck können Sie mit der Klasse "DynamicType.Unloaded" ein Byte-Array extrahieren, das einen dynamischen Typ darstellt. Der Einfachheit halber verfügt dieser Typ auch über eine Methode "saveIn (File)", mit der Sie die Klasse in einem bestimmten Ordner speichern können. Sie können auch "injizieren (Datei)" verwenden, um eine Klasse in eine vorhandene JAR-Datei zu injizieren.

Der direkte Zugriff auf das Binärformat einer Klasse ist einfach, aber das Laden von Typen ist leider komplizierter. In Java werden alle Klassen mit ClassLoader geladen. Ein Beispiel für einen solchen Klassenlader ist der Bootstrap-Klassenlader, der für das Laden von Klassen verantwortlich ist, die in der Java-Klassenbibliothek ausgeliefert werden. Der Systemklassenlader ist dagegen dafür verantwortlich, die Klasse in den Klassenpfad Ihrer Java-Anwendung zu laden. Offensichtlich kennt keiner dieser vorhandenen Klassenlader die von uns erstellten dynamischen Klassen. Um dies zu überwinden, müssen wir andere Möglichkeiten zum Laden von zur Laufzeit generierten Klassen finden. Byte Buddy bietet sofort Lösungen in einer Vielzahl von Ansätzen.

Leider hat der obige Ansatz beide Nachteile.

Nach dem Erstellen von "DynamicType.Unloaded" kann dieser Typ mit "ClassLoadingStrategy" geladen werden. Wenn keine solche Strategie bereitgestellt wird, leitet Byte Buddy eine solche Strategie basierend auf dem bereitgestellten Klassenladeprogramm ab, andernfalls nur für Bootstrap-Klassenladeprogramme, die keine Typen mit der Standardreflexion einfügen können. Erstellen Sie einen neuen Klassenlader für. Byte Buddy bietet verschiedene Strategien zum Laden von Klassen, die sofort einsatzbereit sind. Jede Strategie folgt einem der oben genannten Konzepte. Diese Strategien sind in ClassLoadingStrategy.Default definiert. Die Strategie "WRAPPER" erstellt einen neuen Wrapping "ClassLoader". Hier erstellt die Strategie "CHILD_FIRST" einen ähnlichen Klassenlader, wobei das Kind die erste Semantik hat. Sowohl die Strategien WRAPPER als auch CHILD_FIRST sind auch in der sogenannten Manifest-Version verfügbar, bei der die Binärform des Typs nach dem Laden der Klasse erhalten bleibt. Mit diesen alternativen Versionen können Sie über die Methode ClassLoader :: getResourceAsStream auf die binäre Darstellung der Klassen des Klassenladeprogramms zugreifen. Beachten Sie jedoch, dass diese Klassenlader dazu einen Verweis auf die vollständige binäre Darstellung der Klassen beibehalten müssen, die Speicherplatz auf dem JVM-Heap belegen. Wenn Sie tatsächlich auf das Binärformat zugreifen möchten, verwenden Sie daher nur die Manifestversion. Die "INJECTION" -Strategie funktioniert durch Reflexion und ist in der Manifestversion offensichtlich nicht verfügbar, da sie die Semantik der "ClassLoader :: getResourceAsStream" -Methode nicht ändert.

Lassen Sie uns tatsächlich das Laden einer solchen Klasse sehen.

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

Im obigen Beispiel haben wir die Klasse erstellt und geladen. Wie bereits erwähnt, haben wir in den meisten Fällen die WRAPPER-Strategie verwendet, um die entsprechende Klasse zu laden. Schließlich gibt die Methode "getLoaded" eine Instanz der Java-Klasse zurück, die die aktuell geladene dynamische Klasse darstellt.

Beim Laden einer Klasse wird eine vordefinierte Klassenladestrategie ausgeführt, indem die "ProtectionDomain" des aktuellen Ausführungskontexts angewendet wird. Alternativ bieten alle Standardstrategien eine explizite Schutzdomänenspezifikation durch Aufrufen der Methode "withProtectionDomain". Das Definieren einer expliziten Schutzdomäne ist wichtig, wenn Sie Security Manager verwenden oder mit Klassen arbeiten, die in signierten Jars definiert sind.

Reloading a class

Im vorherigen Abschnitt wurde beschrieben, wie mit Byte Buddy vorhandene Klassen neu definiert oder neu definiert werden können. Es kann jedoch nicht garantiert werden, dass eine bestimmte Klasse noch nicht geladen wurde, während das Java-Programm ausgeführt wird. (Außerdem verwendet Byte Buddy derzeit nur Ladeklassen als Argumente. In zukünftigen Versionen funktioniert dies genauso wie das Entladen von Klassen mit vorhandenen APIs.) Auch nach dem Laden. Diese Funktion wird von Byte Buddys "ClassReloadingStrategy" zugänglich gemacht. Definieren wir die Klasse Foo neu, um diese Strategie zu demonstrieren.

class Foo {
  String m() { return "foo"; }
}
 
class Bar {
  String m() { return "bar"; }
}

Mit Byte Buddy können Sie die Klasse "Foo" ganz einfach als "Bar" definieren. Mit "HotSwap" gilt diese Neudefinition auch für vorhandene Instanzen.

ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
assertThat(foo.m(), is("bar"));

Auf HotSwap kann nur mit dem sogenannten Java-Agenten zugegriffen werden. Solche Agenten können installiert werden, indem sie beim Starten einer virtuellen Java-Maschine mit dem Parameter "-javaagent" angegeben werden. Das Parameterargument ist das Byte Buddy-Agent-JAR, das Sie von der Byte Buddy Bintray-Seite herunterladen können. Wenn die Java-Anwendung jedoch von einer JDK-Installation einer virtuellen Java-Maschine ausgeführt wird, kann Byte Buddy den Java-Agenten nach dem Starten der Anwendung mit "ByteBuddyAgent.installOnOpenJDK ()" weiterhin laden. Dies ist eine sehr bequeme Methode, da die Neudefinition von Klassen hauptsächlich zum Implementieren von Tools und Tests verwendet wird. Ab Java 9 ist es auch möglich, den Agenten zur Laufzeit zu installieren, ohne das JDK zu installieren.

Eine Sache, die im obigen Beispiel möglicherweise nicht intuitiv erscheint, ist die Tatsache, dass Byte Buddy angewiesen wird, den Balkentyp neu zu definieren, wobei der Foo-Typ schließlich neu definiert wird. Virtuelle Java-Maschinen identifizieren Typen anhand des Namens und des Klassenladeprogramms. Wenn Sie Bar in Foo umbenennen und diese Definition anwenden, wird der umbenannte Balkentyp möglicherweise neu definiert. Natürlich ist es auch möglich, Foo direkt neu zu definieren, ohne verschiedene Typen umzubenennen.

Die Verwendung der HotSwap-Funktion von Java hat jedoch einen großen Nachteil. Die aktuelle Implementierung von HotSwap erfordert, dass die neu definierte Klasse vor und nach der Neudefinition der Klasse dasselbe Klassenschema anwendet. Das heißt, Sie können beim erneuten Laden einer Klasse keine Methoden oder Felder hinzufügen. Wir haben bereits gesehen, dass Byte Buddy eine Kopie der ursprünglichen Methode der Rebase-Klasse definiert, sodass die Klassen-Rebase nicht mit ClassReloadingStrategy funktioniert. Außerdem funktioniert die Neudefinition von Klassen nicht für Klassen mit expliziten Methoden zur Klasseninitialisierung (statische Blöcke innerhalb der Klasse). Dies liegt daran, dass diese Initialisierungsmethode auch in die zusätzliche Methode kopiert werden muss. Es ist jedoch geplant, "HotSwap" in Zukunft zu erweitern, und Byte Buddy ist bereit, diese Funktion zu verwenden, sobald sie funktioniert. In der Zwischenzeit kann die "HotSwap" -Unterstützung von Byte Buddy für Eckfälle verwendet werden, die Sie nützlich finden. Andernfalls kann das Verschieben und Neudefinieren von Klassen eine nützliche Funktion sein, beispielsweise wenn eine vorhandene Klasse aus einem Build-Skript erweitert wird.

Working with unloaded classes

Angesichts dieses Bewusstseins der Einschränkungen der "HotSwap" -Funktionalität von Java könnte man denken, dass die einzige sinnvolle Anwendung von Rebase- und Neudefinitionsanweisungen in der Erstellungszeit liegt. Durch Anwenden von Build-Time-Operationen können Sie sicherstellen, dass die verarbeitete Klasse nicht vor dem Laden der ersten Klasse geladen wird. Dies liegt einfach daran, dass das Laden der Klasse auf einer anderen Instanz der JVM erfolgt. Byte Buddy funktioniert genauso für Klassen, die noch nicht geladen wurden. Zu diesem Zweck abstrahiert Byte Buddy die Reflection-API von Java, sodass eine "Class" -Instanz intern dargestellt wird, beispielsweise durch eine Instanz von "TypeDescription". Tatsächlich kann Byte Buddy nur mit den Klassen umgehen, die vom Adapter bereitgestellt werden, der die TypeDescription-Schnittstelle implementiert. Der große Vorteil dieser Abstraktion besteht darin, dass die Informationen über die Klasse nicht von "ClassLoader" bereitgestellt werden müssen, sondern von jeder anderen Quelle bereitgestellt werden können.

Byte Buddy bietet eine Standardmethode zum Abrufen der TypeDescription einer Klasse mit TypePool. Natürlich ist auch eine Standardimplementierung eines solchen Pools vorgesehen. Diese TypePool.Default-Implementierung analysiert die Binärform der Klasse und stellt sie als die erforderliche TypeDescription dar. Ähnlich wie "ClassLoader" verwaltet es auch den Cache von ausdrucksfähigen Klassen. Dies ist auch anpassbar. Normalerweise wird auch die Binärform der Klasse von "ClassLoader" abgerufen, es wird jedoch nicht angewiesen, diese Klasse zu laden.

Virtuelle Java-Maschinen laden Klassen nur bei der ersten Verwendung. Infolgedessen können Sie eine Klasse sicher neu definieren, zum Beispiel:

package foo;
class Bar { }

Führen Sie es beim Programmstart aus, bevor Sie anderen Code ausführen.

class MyApplication {
  public static void main(String[] args) {
    TypePool typePool = TypePool.Default.ofClassPath();
    new ByteBuddy()
      .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class'
                ClassFileLocator.ForClassLoader.ofClassPath())
      .defineField("qux", String.class) // we learn more about defining fields later
      .make()
      .load(ClassLoader.getSystemClassLoader());
    assertThat(Bar.class.getDeclaredField("qux"), notNullValue());
  }
}

Sie können das Laden der integrierten JVM-Klasse verhindern, indem Sie die neu definierte Klasse explizit laden, bevor sie zum ersten Mal in einer Assertionsanweisung verwendet wird. Auf diese Weise wird die neu definierte Definition von "foo.Bar" geladen und während der gesamten Laufzeit der Anwendung verwendet. Wenn Sie jedoch TypePool verwenden, um eine Beschreibung bereitzustellen, verweisen Sie nicht auf die Klasse im Klassenliteral. Wenn Sie ein Klassenliteral für "foo.Bar" verwenden, ist der Neudefinitionsversuch ungültig, da die JVM diese Klasse geladen hat, bevor die Änderungen vorgenommen wurden, um sie neu zu definieren. Wenn Sie sich mit entladenen Klassen befassen, müssen Sie außerdem "ClassFileLocator" angeben, in dem Sie die Klassendateien für die Klasse finden. Im obigen Beispiel wird einfach ein Klassendateisuchprogramm erstellt, das den Klassenpfad in einer laufenden Anwendung nach solchen Dateien durchsucht.

Creating Java agents

Wenn Anwendungen wachsen und modularer werden, ist das Anwenden solcher Transformationen an bestimmten Programmpunkten natürlich eine umständliche Einschränkung bei der Implementierung. Und es gibt eine bessere Möglichkeit, eine solche Neudefinition von Klassen bei Bedarf anzuwenden. Verwenden Sie den Java-Agenten (https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html), um die Aktivitäten zum Laden von Klassen, die in Ihrer Java-Anwendung stattfinden, direkt zu steuern. Es ist möglich abzufangen. Der Java-Agent wird als einfache JAR-Datei mit den in der Manifestdatei für diese JAR-Datei angegebenen Einstiegspunkten implementiert, wie unter Verknüpfte Ressourcen beschrieben. Mit Byte Buddy ist die Implementierung eines solchen Agenten mit "AgentBuilder" einfach. Angenommen, Sie haben zuvor eine einfache Annotation mit dem Namen "ToString" definiert, implementieren Sie die "toString" -Methode für alle mit Annotationen versehenen Klassen, indem Sie einfach die "premain" -Methode des Agenten wie folgt implementieren: Ist einfach.

class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

Als Ergebnis der Anwendung von "AgentBuilder.Transformer" oben werden alle "toString" -Methoden der mit Anmerkungen versehenen Klasse jetzt transformiert zurückgegeben. Byte Buddys "DynamicType.Builder" wird in einem zukünftigen Abschnitt behandelt, aber machen Sie sich vorerst keine Sorgen um diese Klasse. Der obige Code ist natürlich eine triviale und bedeutungslose Anwendung. Die ordnungsgemäße Verwendung dieses Konzepts bietet ein leistungsstarkes Werkzeug für die einfache Implementierung einer aspektorientierten Programmierung.

Es ist auch möglich, die vom Bootstrap-Klassenlader geladenen Klassen zu instrumentieren, wenn der Agent verwendet wird. Dies erfordert jedoch einige Vorbereitungen. Zunächst wird der Bootstrap-Klassenlader durch einen Nullwert dargestellt, sodass es nicht möglich ist, eine Klasse mithilfe von Reflection in diesen Klassenlader zu laden. Dies kann jedoch erforderlich sein, um die Hilfsklasse in den Klassenladeprogramm für Messklassen zu laden und die Klassenimplementierung zu unterstützen. Um Klassen in den Bootstrap-Klassenladeprogramm zu laden, kann Byte Buddy JAR-Dateien erstellen und diese Dateien zum Ladepfad des Bootstrap-Klassenladeprogramms hinzufügen. Um dies zu ermöglichen, müssen diese Klassen auf der Festplatte gespeichert werden. Ordner für diese Klassen können mit dem Befehl "enableBootstrapInjection" angegeben werden, der auch eine Instanz der Schnittstelle "Instrumentation" abruft, um die Klasse hinzuzufügen. Alle von der Instrumentierungsklasse verwendeten Benutzerklassen müssen auch über die Instrumentierungsschnittstelle in einen möglichen Bootstrap-Suchpfad eingefügt werden.

Loading classes in Android applications

Android verwendet ein anderes Klassendateiformat und verwendet eine Dex-Datei, die nicht im Layout des Java-Klassendateiformats enthalten ist. Darüber hinaus kompiliert die ART-Laufzeit, die von der virtuellen Dalvik-Maschine erbt, Android-Anwendungen in nativen Maschinencode, bevor sie auf einem Android-Gerät installiert wird. Infolgedessen kann Byte Buddy Klassen nicht mehr neu definieren oder verschieben, es sei denn, die Anwendung wird explizit mit ihrer Java-Quelle bereitgestellt, da keine zu interpretierende Zwischencodedarstellung vorhanden ist. Byte Buddy kann jedoch neue Klassen mit DexClassLoader und dem integrierten Dex-Compiler definieren. Zu diesem Zweck bietet Byte Buddy ein Byte-Buddy-Android-Modul mit einer "AndroidClassLoadingStrategy", mit der Sie dynamisch erstellte Klassen aus Ihrer Android-Anwendung laden können. Damit es funktioniert, benötigen Sie einen Ordner zum Schreiben temporärer Dateien und kompilierter Klassendateien. Dieser Ordner ist vom Android Security Manager verboten und sollte nicht von verschiedenen Anwendungen gemeinsam genutzt werden.

Working with generic types

Byte Buddy verarbeitet generische Typen, wie sie in der Programmiersprache Java definiert sind. Generische Typen werden von der Java-Laufzeit nicht berücksichtigt, die nur das Löschen generischer Typen behandelt. Generische Typen sind jedoch weiterhin in jede Java-Klassendatei eingebettet und werden von der Java Reflection-API verfügbar gemacht. Daher ist es sinnvoll, generische Informationen in die generierte Klasse aufzunehmen, da generische Typinformationen das Verhalten anderer Bibliotheken und Frameworks beeinflussen können. Das Einbetten generischer Typinformationen ist auch wichtig, wenn die Klasse persistent ist und vom Java-Compiler als Bibliothek behandelt wird.

Wenn Sie eine Klasse unterklassifizieren, eine Schnittstelle implementieren oder ein Feld oder eine Methode deklarieren, akzeptiert Byte Buddy Java Type anstelle der gelöschten Class. Generische Typen können auch explizit mit "TypeDescription.Generic.Builder" definiert werden. Ein wichtiger Unterschied zwischen generischen Java-Typen für die Typeliminierung sind die kontextbezogenen Auswirkungen von Typvariablen. Eine Typvariable mit einem bestimmten Namen, der von einem Typ definiert wird, stellt nicht unbedingt denselben Typ dar, wenn ein anderer Typ dieselbe Typvariable mit demselben Namen deklariert. Daher bindet Byte Buddy alle generischen Typen, die Typvariablen im Kontext des generierten Typs oder der generierten Methode darstellen, neu, wenn die Instanz "Typ" an die Bibliothek übergeben wird.

Byte Buddy fügt die Bridge-Methoden (https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html) transparent ein, wenn der Typ erstellt wird. Die Bridge-Methode wird durch die Eigenschaft der ByteBuddy-Instanz MethodGraph.Compiler aufgelöst. Der Standard-Methodengraph-Compiler verhält sich wie ein Java-Compiler und verarbeitet generische Typinformationen in Klassendateien. Für andere Sprachen als Java kann ein Differential Graph Compiler eine gute Wahl sein.

Fields and methods

Die meisten im vorherigen Abschnitt erstellten Typen definieren keine Felder oder Methoden. Durch die Unterklasse "Objekt" erbt die erstellte Klasse jedoch die von ihrer Oberklasse definierten Methoden. Überprüfen Sie diese Java-Trivia und rufen Sie die toString-Methode für die dynamische Typinstanz auf. Sie können eine Instanz erhalten, indem Sie den Konstruktor der Klasse, die Sie reflektiert haben, reflektierend aufrufen.

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance() // Java reflection API
  .toString();

Die Implementierung der Methode "Object # toString" gibt eine Verkettung des vollständig qualifizierten Klassennamens der Instanz und der hexadezimalen Darstellung des Hash-Codes der Instanz zurück. Tatsächlich gibt der Aufruf der Methode "toString" für die erstellte Instanz so etwas wie "example.Type @ 340d1fa5" zurück.

Natürlich machen wir das hier nicht. Die Hauptmotivation für die Erstellung dynamischer Klassen ist die Fähigkeit, neue Logik zu definieren. Beginnen wir mit einem einfachen Beispiel, um zu zeigen, wie dies gemacht wird. Überschreibt die toString-Methode und gibt Hello World zurück! Anstelle des vorherigen Standardwerts

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .method(named("toString")).intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .toString();

Die Zeile, die ich dem Code hinzugefügt habe, enthält zwei Anweisungen in der domänenspezifischen Sprache von Byte Buddy. Die erste Anweisung ist eine Methode, mit der Sie so viele Methoden auswählen können, wie Sie überschreiben möchten. Diese Auswahl wird angewendet, indem "ElementMatcher" übergeben wird, das als Prädikat dient, um zu bestimmen, ob jede überschreibbare Methode überschrieben werden soll. Byte Buddy enthält eine Reihe vordefinierter Methoden-Matcher, die in der Klasse "ElementMatchers" zusammengefasst sind. Im Allgemeinen sollten Sie diese Klasse statisch importieren, um das Lesen des resultierenden Codes natürlicher zu gestalten. Ein solcher statischer Import wurde auch im obigen Beispiel unter Verwendung eines benannten Methodenvergleichs ins Auge gefasst, der die Methode anhand des genauen Namens auswählt. Vordefinierte Methoden-Matcher sind konfigurierbar. Auf diese Weise kann die Methodenauswahl wie folgt näher erläutert werden:

named("toString").and(returns(String.class)).and(takesArguments(0))

Dieser letztere Methoden-Matcher stimmt nur mit dieser bestimmten Methode überein, da er die toString-Methode mit einer vollständigen Java-Signatur beschreibt. In dem gegebenen Kontext wissen wir jedoch, dass es keine andere Methode namens "toString" mit einer anderen Signatur gibt, so dass unser ursprünglicher Methodenvergleich ausreichend ist.

Nach Auswahl der Methode "toString" bestimmt der zweite Befehlsabschnitt die Implementierung, die alle Methoden der angegebenen Auswahl überschreibt. Um zu wissen, wie eine Methode implementiert wird, erfordert diese Anweisung ein einzelnes Argument vom Implementierungstyp. Im obigen Beispiel wird die mit Byte Buddy gelieferte Implementierung "FixedValue" verwendet. Wie der Name dieser Klasse andeutet, implementiert die Implementierung immer eine Methode, die einen bestimmten Wert zurückgibt. Etwas später in diesem Abschnitt werden wir die Implementierung von FixedValue im Detail diskutieren. Schauen wir uns nun die Methodenauswahl genauer an.

Bisher haben wir nur eine Methode abgefangen. In einer realen Anwendung können die Dinge komplizierter werden, und Sie möchten möglicherweise unterschiedliche Regeln anwenden, um unterschiedliche Methoden zu überschreiben. Schauen wir uns ein Beispiel für ein solches Szenario an.

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}
 
Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

Im obigen Beispiel haben wir drei verschiedene Regeln zum Überschreiben von Methoden definiert. Wenn wir den Code untersuchen, können wir sehen, dass die erste Regel die durch Foo definierte Methode beinhaltet, dh alle drei Methoden in der Beispielklasse. Die zweite Regel entspricht beiden Methoden mit dem Namen "foo", die eine Teilmenge der vorherigen Auswahl sind. Und die letzte Regel entspricht nur der Methode "foo (Object)". Dies ist eine weitere Reduzierung der früheren Wahl. Aber wenn diese Auswahl dupliziert wird, wie bestimmt Byte Buddy, welche Regel für welche Methode gilt?

Byte Buddy organisiert Regeln zum Überschreiben von Methoden in einem Stapelformat. Das heißt, wenn Sie eine neue Regel registrieren, um eine Methode zu überschreiben, wird sie an den Anfang dieses Stapels verschoben und immer zuerst angewendet, bis eine neue Regel hinzugefügt wird. Im obigen Beispiel bedeutet dies:

Für diese Organisation sollten Sie immer zuletzt einen spezifischeren Methodenvergleich registrieren. Andernfalls wendet der später registrierte weniger spezifische Methodenvergleich möglicherweise nicht die zuvor definierte Regel an. Beachten Sie, dass Sie die Eigenschaft "ignoreMethod" in der Einstellung "ByteBuddy" definieren können. Eine Methode, die gut zu diesem Methoden-Matcher passt, wird niemals überschrieben. Standardmäßig überschreibt Byte Buddy keine Synthesemethoden.

In einigen Szenarien möchten Sie möglicherweise eine Supertypmethode oder eine neue Methode definieren, die die Schnittstelle nicht überschreibt. Dies ist auch mit Byte Buddy möglich. Zu diesem Zweck können Sie defineMethod aufrufen, wo immer Sie eine Signatur definieren können. Nachdem Sie die Methode definiert haben, werden Sie aufgefordert, eine Implementierung bereitzustellen, die der vom Methodenvergleich identifizierten Methode ähnelt. Methoden-Matcher, die nach der Definition einer Methode erstellt wurden, können aufgrund der zuvor beschriebenen Stapelprinzipien Vorrang vor dieser Implementierung haben.

Mit defineField kann Byte Buddy einen bestimmten Feldtyp definieren. In Java werden Felder niemals überschrieben, sondern nur im Schatten (http://en.wikipedia.org/wiki/Variable_shadowing). Daher kann keine Feldanpassung usw. verwendet werden.

Mit diesem Wissen über die Auswahl von Methoden können Sie lernen, wie Sie diese Methoden implementieren können. Schauen wir uns zu diesem Zweck die vordefinierte Implementierung "Implementierung" an, die im Lieferumfang von Byte Buddy enthalten ist. Die Definition einer benutzerdefinierten Implementierung wird in einem eigenen Abschnitt beschrieben, ist jedoch nur für Benutzer gedacht, die eine sehr benutzerdefinierte Methode implementieren müssen.

A closer look at fixed values

Die Implementierung von FixedValue läuft bereits. Wie der Name schon sagt, gibt die von "FixedValue" implementierte Methode einfach das bereitgestellte Objekt zurück. Klassen können solche Objekte auf zwei verschiedene Arten speichern.

Wenn Sie eine Methode mit "FixedValue # value (Object)" implementieren, analysiert Byte Buddy den Typ des Parameters und definiert ihn so, dass er nach Möglichkeit in einem dynamischen Klassenpool gespeichert wird. Andernfalls speichern Sie den Wert in einem statischen Feld. Wenn der Wert jedoch in einem Klassenpool gespeichert ist, kann die von der ausgewählten Methode zurückgegebene Instanz eine andere Objekt-ID haben. Daher können Sie "FixedValue # reference (Object)" verwenden, um Byte Buddy anzuweisen, Objekte immer in statischen Feldern zu speichern. Die letztere Methode ist überladen, sodass Sie den Namen des Feldes als zweites Argument angeben können. Andernfalls wird der Feldname automatisch aus dem Hashcode des Objekts abgeleitet. Die Ausnahme von diesem Verhalten ist der Wert "null". Der Wert "null" wird niemals im Feld gespeichert, sondern lediglich durch seinen Literalausdruck dargestellt.

In diesem Zusammenhang wundern Sie sich möglicherweise über die Typensicherheit. Natürlich können Sie eine Methode definieren, die einen ungültigen Wert zurückgibt.

new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0))
  .make();

Es ist schwierig, diese ungültige Implementierung durch den Compiler im Java-Typsystem zu verhindern. Stattdessen löst Byte Buddy beim Erstellen des Typs eine "IllegalArgumentException" aus, wodurch eine falsche Ganzzahlzuweisung für Methoden ermöglicht wird, die "String" zurückgeben. Byte Buddy wird alles in seiner Macht stehende tun, um sicherzustellen, dass alle erstellten Typen legitime Java-Typen sind, Ausnahmen auslösen und beim Erstellen illegaler Typen schnell fehlschlagen.

Das Zuordnungsverhalten von Byte Buddy ist anpassbar. Auch hier bietet Byte Buddy nur legitime Standardeinstellungen, die das Zuweisungsverhalten des Java-Compilers nachahmen. Infolgedessen können Sie mit Byte Buddy jedem seiner Supertypen Typen zuweisen und auch das Boxen primitiver Werte oder das Unboxing ihrer Wrapper-Darstellungen berücksichtigen. Beachten Sie jedoch, dass Byte Buddy generische Typen derzeit nicht vollständig unterstützt und nur die Eliminierung von Typen berücksichtigt. Daher kann Byte Buddy Heap Pollution verursachen (http://en.wikipedia.org/wiki/Heap_pollution). Anstatt einen vordefinierten Zuweiser zu verwenden, können Sie jederzeit einen eigenen Zuweiser implementieren, der eine Typkonvertierung ermöglicht, die nicht implizit in der Java-Programmiersprache enthalten ist. Informationen zu solchen benutzerdefinierten Implementierungen finden Sie im letzten Abschnitt dieses Lernprogramms. Im Moment erwähne ich, dass Sie einen solchen benutzerdefinierten Zuweiser definieren können, indem Sie "withAssigner" mit einer beliebigen "FixedValue" -Implementierung aufrufen.

Delegating a method call

In vielen Szenarien reicht es natürlich nicht aus, einen festen Wert von der Methode zurückzugeben. Um flexibler zu sein, bietet Byte Buddy eine MethodDelegation-Implementierung. Dies gibt Ihnen maximale Freiheit bei der Beantwortung von Methodenaufrufen. Die Methodendelegation definiert eine Methode eines dynamisch erstellten Typs, um den Aufruf an eine andere Methode weiterzuleiten, die möglicherweise außerhalb des dynamischen Typs liegt. Auf diese Weise kann die Logik dynamischer Klassen mit einfachem Java dargestellt werden, aber die Codegenerierung bietet nur eine Bindung an andere Methoden. Bevor wir uns mit den Details befassen, werfen wir einen Blick auf ein Beispiel für die Verwendung von MethodDelegation.

class Source {
  public String hello(String name) { return null; }
}
 
class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}
 
String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

In diesem Beispiel wird ein Aufruf der Methode "Source # hello (String)" an die Methode "Target" delegiert, sodass die Methode "Hello World!" Anstelle von "null" zurückgibt. Zu diesem Zweck identifiziert die Implementierung "MethodDelegation" aufrufbare Methoden vom Typ "Target" und identifiziert die beste Übereinstimmung zwischen diesen Methoden. Im obigen Beispiel definiert der Typ "Target" nur eine einzige statische Methode. Dies ist nützlich, da die Parameter, der Rückgabetyp und der Name der Methode mit denen von "Source # name (String)" identisch sind. ..

In der Praxis ist die Entscheidung, welche Methode delegiert werden soll, wahrscheinlich komplizierter. Wie entscheidet Byte Buddy, wie es geht, wenn es eine echte Wahl gibt? Nehmen wir dazu an, dass die Zielklasse wie folgt definiert ist:

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}

Wie Sie vielleicht bemerkt haben, werden alle oben genannten Methoden als Intercepts bezeichnet. Für Byte Buddy muss die Zielmethode nicht denselben Namen wie die Quellmethode haben. Wir werden dieses Problem bald im Detail untersuchen. Noch wichtiger ist, wenn Sie die Definition von "Ziel" ändern und das vorherige Beispiel ausführen, werden Sie feststellen, dass die Methode "named (String)" an "intercept (String)" gebunden ist. Aber warum? Offensichtlich kann die intercept (int) -Methode das String-Argument der Quellmethode nicht akzeptieren, daher besteht keine Chance auf eine Übereinstimmung. Dies ist jedoch nicht der Fall für bindbare "Intercept (Object)" - Methoden. Um diese Mehrdeutigkeit zu beheben, ahmt Byte Buddy den Java-Compiler erneut nach, indem es die Methodenbindung mit dem spezifischsten Parametertyp auswählt. Denken Sie daran, wie der Java-Compiler Bindungen für überladene Methoden auswählt. Da "String" spezifischer als "Objekt" ist, wird die "Intercept (String)" - Klasse schließlich aus drei Auswahlmöglichkeiten ausgewählt.

Mit den bisherigen Informationen könnten Sie denken, dass der Methodenbindungsalgorithmus eine ziemlich starre Eigenschaft ist. Aber wir haben noch nicht die ganze Geschichte erzählt. Bisher haben wir nur ein weiteres Beispiel für eine Konvention zum Festlegen von Prinzipien beobachtet, die geändert werden können, wenn die Standardeinstellungen nicht den tatsächlichen Anforderungen entsprechen. In der Praxis arbeitet die Implementierung von "MethodDelegation" mit Annotationen, die bestimmen, welchem Wert die Annotation eines Parameters zugewiesen werden soll. Wenn jedoch keine Anmerkungen gefunden werden, behandelt Byte Buddy die Parameter so, als wären sie mit "@ Argument" versehen. Diese letztere Annotation veranlasst Byte Buddy, dem annotierten Ziel das n-te Argument der Quellmethode zuzuweisen. Wenn keine Annotation explizit hinzugefügt wird, wird der Wert von "n" auf den Index des annotierten Parameters gesetzt. Nach dieser Regel behandelt Byte Buddy dies wie folgt:

void foo(Object o1, Object o2)

Als ob alle Parameter wie folgt kommentiert wären:

void foo(@Argument(0) Object o1, @Argument(1) Object o2)

Infolgedessen werden das erste und das zweite Argument der instrumentierten Methode dem Interceptor zugewiesen. Wenn die abgefangene Methode nicht mindestens zwei Parameter deklariert oder wenn der mit Anmerkungen versehene Parametertyp nicht vom Parametertyp der instrumentierten Methode zugewiesen wird, wird die betreffende abfangende Methode verworfen.

Zusätzlich zur Annotation "@ Argument" gibt es einige andere vordefinierte Annotationen, die Sie mit "MethodDelegation" verwenden können.

Mit Byte Buddy können Sie nicht nur vordefinierte Anmerkungen verwenden, sondern auch Ihre eigenen Anmerkungen definieren, indem Sie einen oder mehrere ParameterBinder registrieren. Informationen zu solchen Anpassungen finden Sie im letzten Abschnitt dieses Tutorials.

Zusätzlich zu den vier bisher beschriebenen Annotationen gibt es zwei weitere vordefinierte Annotationen, die den Zugriff auf die Superimplementierung dynamischer Methoden ermöglichen. Auf diese Weise können dynamische Typen ihren Klassen Aspekte hinzufügen, z. B. Protokollierungsmethodenaufrufe. Sie können auch die Annotation @ SuperCall verwenden, um eine Methode von außerhalb der dynamischen Klasse mit einer Superimplementierung aufzurufen, wie im folgenden Beispiel gezeigt.

class MemoryDatabase {
  public List<String> load(String info) {
    return Arrays.asList(info + ": foo", info + ": bar");
  }
}
 
class LoggerInterceptor {
  public static List<String> log(@SuperCall Callable<List<String>> zuper)
      throws Exception {
    System.out.println("Calling database");
    try {
      return zuper.call();
    } finally {
      System.out.println("Returned from database");
    }
  }
}
 
MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

Aus dem obigen Beispiel wird die Super-Methode von ihrer aufrufenden Methode aufgerufen, indem eine Instanz von "Callable" in "LoggerInterceptor" eingefügt wird, wodurch die erste nicht überschreibende Implementierung von "MemoryDatabase # load (String)" aufgerufen wird. Ist klar. Diese Hilfsklasse heißt in der Byte Buddy-Terminologie "AuxiliaryType". Hilfstypen werden bei Bedarf von Byte Buddy erstellt und können direkt über die DynamicType-Schnittstelle aufgerufen werden, nachdem die Klasse erstellt wurde. Aufgrund dieser Hilfstypen können durch manuelles Erstellen eines dynamischen Typs mehrere zusätzliche Typen erstellt werden, mit deren Hilfe die ursprüngliche Klasse implementiert werden kann. Beachten Sie schließlich, dass die Annotation "@ SuperCall" auch mit dem Typ "Runnable" verwendet werden kann, wodurch der Rückgabewert der ursprünglichen Methode entfernt wird.

Sie fragen sich möglicherweise immer noch, wie dieser Hilfstyp andere Arten von Supermethoden aufrufen kann, die normalerweise in Java verboten sind. Bei näherer Betrachtung ist dieses Verhalten sehr häufig und ähnelt dem kompilierten Code, der beim Kompilieren des folgenden Java-Quellcode-Snippets generiert wird.

class LoggingMemoryDatabase extends MemoryDatabase {
 
  private class LoadMethodSuperCall implements Callable {
 
    private final String info;
    private LoadMethodSuperCall(String info) {
      this.info = info;
    }
 
    @Override
    public Object call() throws Exception {
      return LoggingMemoryDatabase.super.load(info);
    }
  }
 
  @Override
  public List<String> load(String info) {
    return LoggerInterceptor.log(new LoadMethodSuperCall(info));
  }
}

Möglicherweise müssen Sie die Supermethode jedoch mit anderen Argumenten aufrufen als im ursprünglichen Aufruf der Methode. Dies ist auch mit Byte Buddy über die Annotation @ Super möglich. Diese Annotation löst die Erstellung eines anderen "AuxiliaryType" aus, der die betreffende Superklasse oder Schnittstelle des dynamischen Typs erweitert. Nach wie vor überschreibt der Hilfstyp alle Methoden zum Aufrufen der dynamischen Superimplementierung. Auf diese Weise können Sie das Logger Interceptor-Beispiel aus dem vorherigen Beispiel implementieren, um den tatsächlichen Aufruf zu ändern.

class ChangingLoggerInterceptor {
  public static List<String> log(String info, @Super MemoryDatabase zuper) {
    System.out.println("Calling database");
    try {
      return zuper.load(info + " (logged access)");
    } finally {
      System.out.println("Returned from database");
    }
  }
}

Die Instanz, die dem mit "@ Super" annotierten Parameter zugewiesen ist, hat eine andere ID als die tatsächliche Instanz des dynamischen Typs. Daher spiegeln die Instanzfelder, auf die der Parameter zugreift, nicht die Felder der tatsächlichen Instanz wider. Darüber hinaus behalten die nicht überdatierbaren Methoden von Hilfsinstanzen ihre ursprüngliche Implementierung bei, was zu einem absurden Verhalten führen kann, wenn sie aufgerufen werden, anstatt ihre Aufrufe zu delegieren. Wenn ein mit "@ Super" kommentierter Parameter keinen Supertyp des zugehörigen dynamischen Typs darstellt, wird die Methode schließlich nicht als Bindungsziel für diese Methode betrachtet.

Die Annotation "@ Super" ermöglicht die Verwendung beliebiger Typen. Daher müssen Sie möglicherweise Informationen darüber bereitstellen, wie dieser Typ konfiguriert werden kann. Standardmäßig versucht Byte Buddy, den Standardkonstruktor der Klasse zu verwenden. Dies funktioniert immer für Schnittstellen, die den Typ "Objekt" implizit erweitern. Wenn Sie jedoch eine dynamische Oberklasse erweitern, stellt diese Klasse möglicherweise keinen Standardkonstruktor bereit. In solchen Fällen oder wenn Sie einen bestimmten Konstruktor verwenden müssen, um solche Hilfstypen zu erstellen, verwenden Sie die Annotation "@ Super", um den Parametertyp als die Eigenschaft "constructorParameters" der Annotation festzulegen. Auf diese Weise können Sie verschiedene Konstruktoren identifizieren. Dieser Konstruktor wird aufgerufen, indem jedem Parameter der entsprechende Standardwert zugewiesen wird. Alternativ können Sie die Strategie "Super.Instantiation.UNSAFE" verwenden, um Klassen zu erstellen, die die inneren Klassen von Java verwenden, um Hilfstypen zu erstellen, ohne den Konstruktor aufzurufen. Diese Methode ist jedoch nicht immer auf Nicht-Oracle-JVMs portierbar und möglicherweise in zukünftigen JVM-Versionen nicht verfügbar. Bis heute sind die inneren Klassen, die in dieser gefährlichen Instanziierungsmethode verwendet werden, in fast jeder JVM-Implementierung zu finden.

Außerdem haben Sie möglicherweise bereits bemerkt, dass der obige "LoggerInterceptor" eine aktivierte Ausnahme deklariert. Andererseits deklariert die instrumentierte Quellmethode, die diese Methode aufruft, keine aktivierte "Ausnahme" (http://docs.oracle.com/javase/tutorial/essential/exceptions/declaring.html). Der Java-Compiler weigert sich normalerweise, solche Aufrufe zu kompilieren. Im Gegensatz zum Compiler behandelt die Java-Laufzeit geprüfte Ausnahmen jedoch nicht anders als ungeprüfte und ermöglicht diesen Aufruf. Aus diesem Grund haben wir beschlossen, die aktivierten Ausnahmen zu ignorieren und ihnen vollständige Flexibilität bei ihrer Verwendung zu geben. Beachten Sie jedoch, dass das Auslösen nicht deklarierter geprüfter Ausnahmen von dynamisch erstellten Methoden die Benutzer Ihrer Anwendung verwirren kann.

Beim Modell der Delegierung von Methoden ist noch etwas zu beachten. Statische Typisierung eignet sich hervorragend für die Implementierung von Methoden, aber strenge Typen können die Wiederverwendung von Code einschränken. Betrachten Sie das folgende Beispiel, um zu verstehen, warum.

class Loop {
  public String loop(String value) { return value; }
  public int loop(int value) { return value; }
}

Die Methoden in der obigen Klasse beschreiben zwei ähnliche Signaturen mit inkompatiblen Typen, sodass es normalerweise nicht möglich ist, beide Methoden mit einer einzigen Interceptor-Methode zu instrumentieren. Stattdessen müssen Sie zwei verschiedene Zielmethoden mit unterschiedlichen Signaturen bereitstellen, um die statische Typprüfung zu erfüllen. Um diese Einschränkung zu überwinden, können Sie mit Byte Buddy Methoden und Methodenparameter mit "@ RuntimeType" versehen.

class Interceptor {
  @RuntimeType
  public static Object intercept(@RuntimeType Object value) {
    System.out.println("Invoked method with: " + value);
    return value;
  }
}

Mit der obigen Zielmethode können Sie jetzt eine einzige Intercept-Methode für beide Quellmethoden bereitstellen. Mit Byte Buddy können Sie auch primitive Werte ein- und auspacken. Die Verwendung von "@ RunType" geht jedoch zu Lasten der Aufgabe der Typensicherheit, sodass eine Mischung inkompatibler Typen zu einer "ClassCastException" führen kann.

Als Äquivalent zu "@ SuperCall" enthält Byte Buddy eine "@ DefaultCall" -Anmerkung, mit der Sie die Standardmethode aufrufen können, anstatt die Supermethode der Methode aufzurufen. Eine Methode mit dieser Parameteranmerkung wird nur dann als Bindung betrachtet, wenn die abgefangene Methode von einer Schnittstelle, die direkt vom instrumentierten Typ implementiert wird, als Standardmethode deklariert wird. In ähnlicher Weise verhindert die Annotation "@ SuperCall" die Methodenbindung, wenn die instrumentierte Methode keine nicht abstrakte Supermethode definiert. Wenn Sie jedoch die Standardmethode für einen bestimmten Typ aufrufen möchten, können Sie die Eigenschaft targetType von @ DefaultCall für eine bestimmte Schnittstelle angeben. In dieser Spezifikation fügt Byte Buddy eine Proxy-Instanz ein, die die Standardmethode des angegebenen Schnittstellentyps aufruft, falls vorhanden. Andernfalls werden Zielmethoden mit Parameteranmerkungen nicht als Delegaten betrachtet. Offensichtlich sind die Standardmethodenaufrufe nur für Klassen verfügbar, die in Java 8 und späteren Klassendateiversionen definiert sind. In ähnlicher Weise gibt es zusätzlich zur Annotation "@ Super" eine Annotation "@ Default", die einen Proxy einfügt, um eine bestimmte Standardmethode explizit aufzurufen.

Wir haben bereits erwähnt, dass benutzerdefinierte Anmerkungen in jeder "MethodDelegation" definiert und registriert werden können. Byte Buddy ist einsatzbereit, enthält jedoch einen Hinweis, der noch explizit installiert und registriert werden muss. Sie können die Annotation @ Pipe verwenden, um einen abgefangenen Methodenaufruf an eine andere Instanz weiterzuleiten. Die Annotation "@ Pipe" ist nicht bei "MethodDelegation" vorregistriert, da die Java-Klassenbibliothek vor Java 8 keinen geeigneten Schnittstellentyp enthält, der den Funktionstyp definiert. Daher müssen Sie den Typ explizit mit einer einzelnen nicht statischen Methode angeben, die "Objekt" als Argument verwendet und als Ergebnis ein anderes "Objekt" zurückgibt. Sie können generische Typen verwenden, solange der Methodentyp an den Typ "Objekt" gebunden ist. Wenn Sie Java 8 verwenden, ist der Typ Function natürlich eine ausführbare Option. Wenn Sie eine Methode mit einem Parameterargument aufrufen, wandelt Byte Buddy den Parameter in den deklarativen Typ der Methode um und ruft die alternative Empfangsmethode mit denselben Argumenten wie der ursprüngliche Methodenaufruf auf. Bevor wir uns das Beispiel ansehen, definieren wir einen benutzerdefinierten Typ, der in Java 5 und höher verwendet werden kann.

interface Forwarder<T, S> {
  T to(S target);
}

Mit diesem Typ können Sie eine neue Lösung implementieren, die den Zugriff auf die oben genannte "MemoryDatabase" aufzeichnet, indem Sie Methodenaufrufe an eine vorhandene Instanz weiterleiten.

class ForwardingLoggerInterceptor {
 
  private final MemoryDatabase memoryDatabase; // constructor omitted
 
  public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) {
    System.out.println("Calling database");
    try {
      return pipe.to(memoryDatabase);
    } finally {
      System.out.println("Returned from database");
    }
  }
}
 
MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration()
    .withBinders(Pipe.Binder.install(Forwarder.class)))
    .to(new ForwardingLoggerInterceptor(new MemoryDatabase()))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

Im obigen Beispiel wird der Aufruf nur an eine andere lokal erstellte Instanz weitergeleitet. Der Vorteil gegenüber der Unterklasse eines Typs und dem Abfangen einer Methode besteht jedoch darin, dass Sie eine vorhandene Instanz auf diese Weise erweitern können. Darüber hinaus registrieren Sie normalerweise Interceptors auf Instanzebene, anstatt statische Interceptors auf Klassenebene zu registrieren.

Bisher haben wir viele "MethodDelegation" -Implementierungen gesehen. Bevor wir fortfahren, schauen wir uns genauer an, wie Byte Buddy die Zielmethode auswählt. Wir haben bereits gesehen, wie Byte Buddy die spezifischste Methode durch Vergleichen von Parametertypen löst, aber es gibt andere. Nachdem Byte Buddy eine geeignete Kandidatenmethode für die Bindung an eine bestimmte Quellmethode identifiziert hat, delegiert es die Lösung an die AmbiguityResolvers-Kette. Auch hier steht es Ihnen frei, Ihre eigene Mehrdeutigkeitslösung zu implementieren, die die Standardeinstellungen von Byte Buddy ergänzen oder ersetzen kann. Ohne solche Änderungen versucht die Mehrdeutigkeitsauflösungskette, eindeutige Zielmethoden zu identifizieren, indem die folgenden Regeln in derselben Reihenfolge angewendet werden wie:

Sie können Methoden explizite Prioritäten zuweisen, indem Sie sie mit "@ BindingPriority" versehen. Wenn eine Methode eine höhere Priorität als eine andere hat, hat die Methode mit der höheren Priorität immer Vorrang vor der Methode mit der niedrigeren Priorität. Darüber hinaus werden mit @ IgnoreForBinding kommentierte Methoden nicht als Zielmethoden betrachtet. Wenn die Quell- und Zielmethode denselben Namen haben, hat diese Zielmethode Vorrang vor anderen Zielmethoden mit unterschiedlichen Namen. Wenn zwei Methoden "@ Argument" verwenden, um dieselben Parameter der Quellmethode zu binden, wird die Methode mit dem spezifischsten Parametertyp berücksichtigt. In dieser Hinsicht spielt es keine Rolle, ob die Annotationen explizit oder implizit bereitgestellt werden, indem die Parameter nicht annotiert werden. Der Auflösungsalgorithmus funktioniert ähnlich wie der Algorithmus des Java-Compilers zum Auflösen von Aufrufen überladener Methoden. Wenn die beiden Typen gleichermaßen identifiziert werden, wird die Methode, die mehr Argumente bindet, als Ziel betrachtet. Wenn Sie einem Parameter während dieser Auflösungsphase Argumente zuweisen müssen, ohne den Parametertyp zu berücksichtigen, können Sie dies tun, indem Sie das Attribut bindingMechanic der Annotation auf BindingMechanic.ANONYMOUS setzen. Darüber hinaus müssen Nicht-Zielparameter für jeden Indexwert jeder Zielmethode eindeutig sein, damit der Auflösungsalgorithmus funktioniert. Wenn die Zielmethode mehr Parameter als die anderen Zielmethoden hat, hat die erstere Vorrang vor der letzteren. Bisher haben wir Methodenaufrufe nur an statische Methoden delegiert, indem wir eine bestimmte Klasse benannt haben, z. B. "MethodDelegation.to (Target.class)". Es kann jedoch auch an eine Instanzmethode oder einen Konstruktor delegiert werden.

Sie können einen Methodenaufruf an eine beliebige Instanzmethode der Klasse "Target" delegieren, indem Sie "MethodDelegation.to (new Target ())" aufrufen. Dies schließt Methoden ein, die an einer beliebigen Stelle in der Klassenhierarchie der Instanz definiert sind, einschließlich Methoden, die in der Klasse "Object" definiert sind. Möglicherweise möchten Sie den Bereich der Kandidatenmethoden einschränken, die Sie ausführen können, indem Sie filter (ElementMatcher) on MethodDelegation aufrufen, um einen Filter auf die Methodendelegation anzuwenden. Der ElementMatcher-Typ ist derselbe wie zuvor zur Auswahl von Quellmethoden in der domänenspezifischen Sprache von Byte Buddy. Instanzen, die einer Methodendelegation unterliegen, werden in statischen Feldern gespeichert. Dies erfordert wie die Festwertdefinition die Definition "TypeInitializer". Anstatt die Delegierung in einem statischen Feld zu speichern, können Sie die Verwendung eines beliebigen Felds auch mit "MethodDelegation.toField (String)" definieren. Das Argument gibt den Feldnamen des Übertragungsziels für alle Methodendelegationen an. Stellen Sie sicher, dass Sie diesem Feld einen Wert zuweisen, bevor Sie eine Methode für eine Instanz einer solchen dynamischen Klasse aufrufen. Andernfalls lautet die Methodendelegation "NullPointerException". Mithilfe der Methodendelegierung können Sie Instanzen eines bestimmten Typs erstellen. Das Aufrufen einer abgefangenen Methode mithilfe von "MethodDelegation.toConstructor (Class)" gibt eine neue Instanz des angegebenen Zieltyps zurück. Wie Sie gerade erfahren haben, untersucht MethodDelegation die Annotationen, um ihre Bindungslogik zu optimieren. Diese Anmerkungen sind spezifisch für Byte Buddy, dies bedeutet jedoch nicht, dass die mit Anmerkungen versehene Klasse in irgendeiner Weise von Byte Buddy abhängt. Stattdessen ignoriert die Java-Laufzeit einfach Annotationstypen, die beim Laden der Klasse nicht im Klassenpfad gefunden werden. Dies bedeutet, dass Byte Buddy nach dem Erstellen der dynamischen Klasse nicht mehr benötigt wird. Dies bedeutet, dass Sie eine dynamische Klasse und den Typ, der ihre Methodenaufrufe delegiert, in einen anderen JVM-Prozess laden können, auch wenn Ihr Klassenpfad kein Byte Buddy enthält.

Es gibt einige vordefinierte Anmerkungen, die Sie mit MethodDelegation verwenden können, aber ich werde sie kurz erklären. Wenn Sie mehr über diese Anmerkungen erfahren möchten, finden Sie weitere Informationen in der Dokumentation in Ihrem Code. Diese Notizen lauten wie folgt:

Calling a super method

Wie der Name schon sagt, kann die Implementierung "SuperMethodCall" verwendet werden, um eine Super-Implementierung einer Methode aufzurufen. Auf den ersten Blick ist ein einzelner Aufruf einer Super-Implementierung nicht sehr nützlich, da nur die vorhandene Logik dupliziert wird, anstatt die Implementierung zu ändern. Sie können jedoch die Anmerkungen der Methode und ihre Parameter ändern, indem Sie die Methode überschreiben. Dies wird im nächsten Abschnitt erläutert. Ein weiterer Grund für den Aufruf einer Supermethode in Java besteht darin, einen Konstruktor zu definieren, der immer einen anderen Konstruktor vom Supertyp oder einen eigenen Typ aufrufen muss.

Bisher haben wir einfach angenommen, dass dynamische Typkonstruktoren ihren direkten Supertypkonstruktoren immer ähnlich sind. Als Beispiel können wir anrufen

new ByteBuddy()
  .subclass(Object.class)
  .make()

Erstellen Sie eine Unterklasse von "Object" mit einem einzelnen Standardkonstruktor, der so definiert ist, dass er einfach den Standardkonstruktor seines direkten Superkonstruktors "Object" aufruft. Dieses Verhalten wird jedoch von Byte Buddy nicht angegeben. Stattdessen ist der obige Code eine Verknüpfung zum Aufrufen.

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_TYPE)
  .make()

ConstructorStrategy erstellt eine Reihe vordefinierter Konstruktoren für jede Klasse. Zusätzlich zu der obigen Strategie, die jeden sichtbaren Konstruktor einer direkten Superklasse vom dynamischen Typ kopiert, gibt es drei weitere vordefinierte Strategien. Eine Ausnahme wird ausgelöst, wenn kein solcher Konstruktor vorhanden ist und wenn es nur einen gibt, der den öffentlichen Supertyp-Konstruktor nachahmt.

Innerhalb des Java-Klassendateiformats unterscheiden sich Konstruktoren im Allgemeinen nicht von Methoden. Daher kann Byte Buddy sie so behandeln, wie sie sind. Der Konstruktor muss jedoch einen fest codierten Aufruf eines anderen Konstruktors enthalten, damit er von der Java-Laufzeit akzeptiert werden kann. Infolgedessen können die meisten vordefinierten Implementierungen außer "SuperMethodCall" keine gültige Java-Klasse erstellen, wenn sie auf einen Konstruktor angewendet werden.

Sie können jedoch Ihren eigenen Konstruktor definieren, indem Sie eine benutzerdefinierte "ConstructorStrategy" mithilfe einer benutzerdefinierten Implementierung implementieren oder einzelne Konstruktoren in der domänenspezifischen Sprache von Byte Buddy mithilfe der Methode "defineConstructor" definieren. Ich werde. Darüber hinaus planen wir, Byte Buddy um neue Funktionen zu erweitern, um komplexere Konstruktoren unverändert zu definieren.

Für die Klassenumbasierung und Klassenneudefinition hält der Konstruktor natürlich die Spezifikation "ConstructorStrategy" überflüssig. Um die Implementierungen dieser beibehaltenen Konstruktoren (und Methoden) zu kopieren, müssen Sie stattdessen "ClassFileLocator" angeben, mit dem Sie die ursprüngliche Klassendatei durchsuchen können, die diese Konstruktordefinitionen enthält. Byte Buddy bemüht sich, den Speicherort der ursprünglichen Klassendatei selbst zu ermitteln. Zum Beispiel durch Abfragen des entsprechenden "ClassLoader" oder durch Nachschlagen des Klassenpfads der Anwendung. Suchvorgänge sind jedoch möglicherweise nicht erfolgreich, wenn es sich um übliche Klassenlader handelt. Sie können dann einen benutzerdefinierten "ClassFileLocator" bereitstellen.

Calling a default method

In der Version 8 wurden in der Programmiersprache Java Standardmethoden für Schnittstellen eingeführt. In Java werden Standardmethodenaufrufe in einer Syntax dargestellt, die Supermethodenaufrufen ähnelt. Der einzige Unterschied besteht darin, dass der Standardmethodenaufruf die Schnittstelle angibt, die die Methode definiert. Dies ist erforderlich, da der Standardmethodenaufruf mehrdeutig sein kann, wenn die beiden Schnittstellen eine Methode mit derselben Signatur definieren. Daher erhält die DefaultMethodCall-Implementierung von Byte Buddy eine Liste priorisierter Schnittstellen. Beim Abfangen einer Methode ruft DefaultMethodCall die Standardmethode auf der zuerst genannten Schnittstelle auf. Angenommen, Sie möchten die folgenden zwei Schnittstellen implementieren:

interface First {
  default String qux() { return "FOO"; }
}
 
interface Second {
  default String qux() { return "BAR"; }
}

Wenn Sie eine Klasse erstellen, die beide Schnittstellen implementiert, und die Methode "qux" implementieren, um die Standardmethode aufzurufen, ruft dieser Aufruf beide die Standardmethode auf, die in der Schnittstelle "First" oder "Second" definiert ist. Ich kann es ausdrücken. Durch Überschreiben der First-Schnittstelle mit DefaultMethodCall erkennt Byte Buddy jedoch, dass die Methoden dieser letzteren Schnittstelle anstelle der alternativen Schnittstelle aufgerufen werden müssen.

new ByteBuddy(ClassFileVersion.JAVA_V8)
  .subclass(Object.class)
  .implement(First.class)
  .implement(Second.class)
  .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class))
  .make()

Java-Klassen, die in Versionen von Klassendateien vor Java 8 definiert wurden, unterstützen keine Standardmethoden. Darüber hinaus sollte beachtet werden, dass Byte Buddy im Vergleich zur Programmiersprache Java schwächere Anforderungen an die Aufrufbarkeit von Standardmethoden stellt. Byte Buddy benötigt nur eine Schnittstelle für die Standardmethoden, die von den spezifischsten Klassen in der Typhierarchie implementiert werden. Abgesehen von der Java-Programmiersprache muss diese Schnittstelle nicht die spezifischste Schnittstelle sein, die von der Oberklasse implementiert wird. Wenn Sie keine mehrdeutige Standardmethodendefinition erwarten, können Sie immer DefaultMethodCall.unambiguousOnly () verwenden, um eine Implementierung zu erhalten, die bei der Erkennung eines mehrdeutigen Standardmethodenaufrufs eine Ausnahme auslöst. Das gleiche Verhalten wird priorisiert, wenn der Standardmethodenaufruf zwischen nicht priorisierten Schnittstellen nicht eindeutig ist und keine priorisierte Schnittstelle gefunden wird, die eine Methode mit einer kompatiblen Signatur definiert. DefaultMethodCall Es wird auch als angezeigt.

Calling a specific method

In einigen Fällen reicht die obige Implementierung nicht aus, um mehr benutzerdefiniertes Verhalten zu implementieren. Beispielsweise möchten Sie möglicherweise eine benutzerdefinierte Klasse implementieren, die explizites Verhalten aufweist. Beispielsweise können Sie die folgende Java-Klasse mit einem Konstruktor implementieren, der keinen Superkonstruktor mit denselben Argumenten hat.

public class SampleClass {
  public SampleClass(int unusedValue) {
    super();
  }
}

Die Object-Klasse hat keinen Konstruktor mit int als Parameter definiert, sodass frühere Implementierungen von SuperMethodCall diese Klasse nicht implementieren konnten. Stattdessen können Sie den Superkonstruktor "Object" explizit aufrufen.

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  .defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC)
  .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
  .make()

Im obigen Code habe ich eine einfache Unterklasse von "Object" erstellt, die einen einzelnen Konstruktor definiert, der einen einzelnen "int" -Parameter verwendet, der nicht verwendet wird. Der letztere Konstruktor wird durch einen expliziten Methodenaufruf an den Superkonstruktor "Object" implementiert.

Die Implementierung von MethodCall kann auch beim Übergeben von Argumenten verwendet werden. Diese Argumente werden explizit als Werte, als Werte für Instanzfelder, die manuell festgelegt werden müssen, oder als angegebene Parameterwerte übergeben. Durch die Implementierung kann die Methode auch für eine andere Instanz als die instrumentierte Instanz aufgerufen werden. Darüber hinaus können Sie neue Instanzen aus abgefangenen Methoden erstellen. Die Dokumentation zur Klasse "MethodCall" enthält detaillierte Informationen zu diesen Funktionen.

Accessing fields

Sie können "FieldAccessor" verwenden, um Methoden zum Lesen und Schreiben von Feldwerten zu implementieren. Um mit dieser Implementierung kompatibel zu sein, muss die Methode einen der folgenden Schritte ausführen:

Das Erstellen einer solchen Implementierung ist einfach: Rufen Sie einfach "FieldAccessor.ofBeanProperty ()" auf. Wenn Sie den Feldnamen jedoch nicht vom Methodennamen ableiten möchten, können Sie den Feldnamen explizit mit FieldAccessor.ofField (String) angeben. Bei Verwendung dieser Methode definiert das einzige Argument den Namen des Feldes, auf das zugegriffen werden soll. Falls gewünscht, können Sie damit ein neues Feld definieren, auch wenn ein solches Feld noch nicht vorhanden ist. Beim Zugriff auf ein vorhandenes Feld können Sie den Typ angeben, in dem das Feld definiert ist, indem Sie die Methode "in" aufrufen. In Java ist es zulässig, Felder in einigen Klassen in der Hierarchie zu definieren. In diesem Prozess werden die Felder einer Klasse durch die Felddefinitionen ihrer Unterklassen ausgeblendet. Ohne einen expliziten Speicherort für die Klasse eines solchen Felds beginnt Byte Buddy mit der spezifischsten Klasse und folgt der Klassenhierarchie, um auf das erste gefundene Feld zuzugreifen.

Schauen wir uns eine Beispielanwendung von "FieldAccessor" an. Angenommen, Sie erhalten in diesem Beispiel einen "UserType", den Sie zur Laufzeit in eine Unterklasse umwandeln möchten. Registrieren Sie zu diesem Zweck einen Interceptor für jede von der Schnittstelle dargestellte Instanz. Auf diese Weise können wir verschiedene Implementierungen entsprechend unseren tatsächlichen Anforderungen bereitstellen. Diese letztere Implementierung kann durch Aufrufen einer Methode der InterceptionAccessor-Schnittstelle auf der entsprechenden Instanz ausgetauscht werden. Um eine Instanz dieses dynamischen Typs zu erstellen, möchte ich keine weitere Reflexion verwenden, rufe jedoch eine Methode von "InstanceCreator" auf, die als Objektfactory fungiert. Die folgenden Typen ähneln dieser Einstellung:

class UserType {
  public String doSomething() { return null; }
}
 
interface Interceptor {
  String doSomethingElse();
}
 
interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}
 
interface InstanceCreator {
  Object makeInstance();
}

Sie haben bereits gelernt, wie Sie die Methoden einer Klasse mit MethodDelegation abfangen. Mit der letzteren Implementierung können Sie die Delegierung an ein Instanzfeld definieren und diesen Feldabfangjäger benennen. Darüber hinaus implementiert es die Schnittstelle "InterceptionAccessor" und fängt alle Methoden der Schnittstelle ab, um die Zugriffsmethoden für dieses Feld zu implementieren. Durch die Definition des Eigenschaftszugriffs "Bean" erkennen wir den "Getter" von "getInterceptor" und den "Setter" von "setInterceptor".

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor"))
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE)
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();

Mit dem neuen dynamicUserType können Sie die InstanceCreator-Schnittstelle implementieren, um eine Factory für diesen dynamischen Typ zu werden. Wieder verwende ich die bekannte Methodendelegation, um den Standardkonstruktor für dynamische Typen aufzurufen.

InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.construct(dynamicUserType))
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

Beachten Sie, dass Sie den Klassenlader dynamicUserType verwenden müssen, um die Factory zu laden. Andernfalls wird dieser Typ ab Werk nicht im Werk angezeigt.

Mit diesen beiden dynamischen Typen können Sie schließlich eine neue Instanz des dynamisch erweiterten "UserType" erstellen und einen benutzerdefinierten Interceptor für diese Instanz definieren. Beenden wir dieses Beispiel, indem wir "HelloWorldInterceptor" auf die gerade erstellte Instanz anwenden. Beachten Sie, dass sowohl die Field Accessor-Schnittstelle als auch die Fabrik dies ohne die Verwendung von Reflexionen ermöglicht haben.

class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}
 
UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());

Miscellaneous

Zusätzlich zu den bisher beschriebenen Implementierungen enthält Byte Buddy mehrere andere Implementierungen.

Annotations

Sie haben gelernt, wie Byte Buddy sich auf Anmerkungen stützt, um einige seiner Funktionen bereitzustellen. Und Byte Buddy ist nicht die einzige Java-Anwendung mit einer annotationsbasierten API. Um dynamisch erstellte Typen in solche Anwendungen zu integrieren, können die erstellten Typen und ihre Mitglieder mit Byte Buddy Anmerkungen definieren. Bevor wir uns mit den Details zum Zuweisen von Annotationen zu dynamisch erstellten Typen befassen, sehen wir uns ein Beispiel für das Annotieren einer Laufzeitklasse an.

@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeDefinition { }
 
class RuntimeDefinitionImpl implements RuntimeDefinition {
  @Override
  public Class<? extends Annotation> annotationType() {
    return RuntimeDefinition.class;
  }
}
 
new ByteBuddy()
  .subclass(Object.class)
  .annotateType(new RuntimeDefinitionImpl())
  .make();

Anmerkungen werden intern als Schnittstellentypen dargestellt, wie durch das Java-Schlüsselwort "@ interface" vorgeschlagen. Infolgedessen können Anmerkungen von Java-Klassen wie eine reguläre Schnittstelle implementiert werden. Der einzige Unterschied zur Schnittstellenimplementierung besteht in der impliziten AnnotationType-Annotationsmethode, die den Annotationstyp bestimmt, den die Klasse darstellt. Die letztere Methode gibt normalerweise ein implementiertes Klassenliteral vom Annotationstyp zurück. Die anderen Annotationseigenschaften werden so implementiert, als wären sie Schnittstellenmethoden. Beachten Sie jedoch, dass für die Implementierung der Annotationsmethode der Standardwert der Annotation wiederholt werden muss.

Es ist besonders wichtig, dynamisch erstellte Klassenanmerkungen zu definieren, wenn eine Klasse als Unterklassen-Proxy für eine andere Klasse fungieren muss. Unterklassen-Proxys werden häufig verwendet, um Querschnittsthemen zu implementieren, wenn Unterklassen die ursprüngliche Klasse so transparent wie möglich nachahmen müssen. Solange dieses Verhalten durch die Definition der Annotation in "@ Inherited" explizit erforderlich ist, werden die Annotationen der Klasse nicht in ihren Unterklassen beibehalten. Sie können einfach einen Unterklassen-Proxy erstellen, der die Basisklassenanmerkungen enthält, indem Sie mit Byte Buddy die Attributmethoden der domänenspezifischen Sprache von Byte Buddy aufrufen. Diese Methode erwartet "TypeAttributeAppender" als Argument. Der Typattribut-Anhang bietet eine flexible Möglichkeit, Anmerkungen für dynamisch erstellte Klassen basierend auf ihrer Basisklasse zu definieren. Wenn Sie beispielsweise "TypeAttributeAppender.ForSuperType" übergeben, werden die Klassenanmerkungen in die dynamisch erstellte Unterklasse kopiert. Anmerkungen und Typattribut-Appender sind zulässig, und Sie können Anmerkungstypen für keine Klasse mehr als einmal definieren.

Methoden- und Feldanmerkungen werden auf die gleiche Weise wie die gerade beschriebenen Typanmerkungen definiert. Methodenanmerkungen können als endgültige Anweisung in der domänenspezifischen Sprache von Byte Buddy für die Implementierung der Methode definiert werden. Ebenso können Felder nach der Definition mit Anmerkungen versehen werden. Schauen wir uns das Beispiel noch einmal an.

new ByteBuddy()
  .subclass(Object.class)
    .annotateType(new RuntimeDefinitionImpl())
  .method(named("toString"))
    .intercept(SuperMethodCall.INSTANCE)
    .annotateMethod(new RuntimeDefinitionImpl())
  .defineField("foo", Object.class)
    .annotateField(new RuntimeDefinitionImpl())

Das obige Codebeispiel überschreibt die Methode "toString" und kommentiert die überschriebene Methode mit "RuntimeDefinition". Darüber hinaus definiert der erstellte Typ ein Feld "foo" mit derselben Annotation und definiert auch die letztere Annotation für den erstellten Typ selbst.

Standardmäßig definiert die ByteBuddy-Konfiguration keine Anmerkungen für dynamisch erstellte Typen oder Typelemente vor. Dieses Verhalten kann jedoch geändert werden, indem der Standardtyp "TypeAttributeAppender", "MethodAttributeAppender" oder "FieldAttributeAppender" angegeben wird. Beachten Sie, dass ein solcher Standard-Appender nicht zulässig ist und den vorherigen Wert ersetzt.

Beim Definieren einer Klasse kann es wünschenswert sein, den Annotationstyp oder seinen Eigenschaftstyp nicht zu laden. Zu diesem Zweck können Sie AnnotationDescription.Builder verwenden, das eine fließende Schnittstelle zum Definieren von Annotationen bietet, ohne das Laden von Klassen auszulösen, jedoch auf Kosten der Typensicherheit. Alle Annotationseigenschaften werden jedoch zur Laufzeit ausgewertet.

Standardmäßig enthält Byte Buddy alle Eigenschaften der Anmerkung in der Klassendatei, einschließlich der Standardeigenschaften, die implizit durch die Standardwerte angegeben werden. Dieses Verhalten kann jedoch angepasst werden, indem der Instanz "ByteBuddy" ein "AnteationFilter" bereitgestellt wird.

Type annotations

Byte Buddy veröffentlicht und schreibt Typanmerkungen, die als Teil von Java 8 eingeführt wurden. Auf Typanmerkungen kann als von der Instanz "TypeDescription.Generic" deklarierte Anmerkungen zugegriffen werden. Wenn Sie einem generischen Feld oder Methodentyp eine Typanmerkung hinzufügen müssen, können Sie mit "TypeDescription.Generic.Builder" einen Anmerkungstyp generieren.

Attribute appenders

Java-Klassendateien können beliebige benutzerdefinierte Informationen als sogenannte Attribute enthalten. Solche Attribute können mit Byte Buddy mithilfe von * AttributeAppender für einen Typ, ein Feld oder eine Methode eingefügt werden. Attribut-Appender können jedoch auch verwendet werden, um Methoden basierend auf den Informationen zu definieren, die von dem abgefangenen Typ, Feld oder der Methode bereitgestellt werden. Wenn Sie beispielsweise eine Methode in einer Unterklasse überschreiben, können Sie alle Anmerkungen der abgefangenen Methode kopieren.

class AnnotatedMethod {
  @SomeAnnotation
  void bar() { }
}

new ByteBuddy()
  .subclass(AnnotatedMethod.class)
  .method(named("bar"))
  .intercept(StubMethod.INSTANCE)
  .attribute(MethodAttributeAppender.ForInstrumentedMethod.INSTANCE)

Der obige Code überschreibt die "bar" -Methode der "AnnotatedMethod" -Klasse, kopiert jedoch alle Annotationen (einschließlich Parameter- oder Typanmerkungen) der überschriebenen Methode.

Die gleichen Regeln gelten möglicherweise nicht, wenn eine Klasse neu definiert oder neu basiert wird. Standardmäßig ist "ByteBuddy" so eingestellt, dass die neu basierten oder neu definierten Methodenanmerkungen beibehalten werden, selbst wenn die Methode wie oben beschrieben abgefangen wird. Dieses Verhalten kann jedoch so geändert werden, dass Byte Buddy vorhandene Anmerkungen verwirft, indem die Strategie "AnnotationRetention" auf "DISABLED" gesetzt wird.

Custom method implementations

Im vorherigen Abschnitt wurde die Standard-Byte-Buddy-API beschrieben. Keine der bisher beschriebenen Funktionen erfordert Kenntnisse oder eine explizite Darstellung des Java-Bytecodes. Wenn Sie jedoch benutzerdefinierten Bytecode erstellen müssen, rufen Sie die API von ASM auf, einer einfachen Bytecodebibliothek, auf der Byte Buddy basiert. Es kann durch direkten Zugriff erstellt werden. Verschiedene Versionen von ASM sind jedoch nicht mit anderen Versionen kompatibel. Daher müssen Sie Byte Buddy neu in Ihren Namespace packen, wenn Sie Ihren Code veröffentlichen. Andernfalls kann die Anwendung zu Inkompatibilitäten für andere Verwendungen von Byte Buddy führen, wenn unterschiedliche Abhängigkeiten unterschiedliche Versionen von Byte Buddy basierend auf unterschiedlichen Versionen von ASM erwarten. Weitere Informationen zum Aufrechterhalten Ihrer Abhängigkeit von Byte Buddy finden Sie auf der Startseite (https://bytebuddy.net/#dependency).

Die ASM-Bibliothek enthält eine gute Dokumentation zum Java-Bytecode und zur Bibliotheksnutzung (http://download.forge.objectweb.org/asm/asm4-guide.pdf). Lesen Sie daher dieses Dokument, wenn Sie mehr über Java-Bytecode und ASM-API erfahren möchten. Stattdessen werde ich kurz das JVM-Ausführungsmodell und die Anpassung der ASM-API durch Byte Buddy vorstellen.

Jede Java-Klassendatei besteht aus mehreren Segmenten. Die Kernsegmente können grob wie folgt klassifiziert werden.

Glücklicherweise übernimmt die ASM-Bibliothek die Verantwortung für die Einrichtung eines ordnungsgemäßen konstanten Pools beim Erstellen einer Klasse. Damit bleibt das einzige nicht triviale Element bei der Implementierung der Methode, die durch ein Array von Ausführungsanweisungen dargestellt wird, die jeweils als einzelnes Byte codiert sind. Diese Anweisungen werden von der virtuellen Stapelmaschine verarbeitet, wenn die Methode aufgerufen wird. Betrachten Sie als einfaches Beispiel eine Methode, die die Summe zweier primitiver Ganzzahlen "10" und "50" berechnet und zurückgibt. Der Java-Bytecode für diese Methode sieht folgendermaßen aus:

LDC     10  // stack contains 10
LDC     50  // stack contains 10, 50
IADD        // stack contains 60
IRETURN     // stack is empty

Die obigen Java-Bytecode-Array-Mnemoniken (http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings) beginnen damit, dass beide Zahlen mit der Anweisung LDC auf den Stapel geschoben werden. Beachten Sie, wie sich diese Ausführungsreihenfolge von der Reihenfolge unterscheidet, in der Ergänzungen im Java-Quellcode dargestellt werden, der als einleitende Notation "10 + 50" geschrieben ist. Derzeit auf dem Stapel gefundener oberster Wert Dieser Zusatz wird durch "IADD" dargestellt und verbraucht zwei oberste Stapelwerte, von denen beide als primitive Ganzzahlen erwartet werden. Dabei werden diese beiden Werte addiert und das Ergebnis an die Spitze des Stapels verschoben. Schließlich verwendet die Anweisung "IRETURN" diese Berechnung und gibt sie von der Methode zurück, wobei ein leerer Stapel verbleibt.

Wir haben bereits erwähnt, dass alle primitiven Werte, auf die innerhalb einer Methode verwiesen wird, im konstanten Pool der Klasse gespeichert werden. Dies gilt auch für die Nummern "50" und "10", auf die in der obigen Methode Bezug genommen wird. Jedem Wert im Konstantenpool wird ein 2 Byte langer Index zugewiesen. Angenommen, die Zahlen "10" und "50" sind in den Indizes "1" und "2" gespeichert. Das obige Verfahren wird wie folgt ausgedrückt, zusammen mit dem Bytewert der obigen Mnemonik, der "0x12" für "LDC", "0x60" für "IADD" und "0xAC" für "IRETURN" ist. Raw-Byte-Anweisung:

12 00 01
12 00 02
60
AC

Bei kompilierten Klassen befindet sich diese genaue Bytesequenz in der Klassendatei. Diese Beschreibung reicht jedoch noch nicht aus, um die Implementierung der Methode vollständig zu definieren. Um die Ausführungszeit von Java-Anwendungen zu verkürzen, sollte jede Methode die virtuelle Java-Maschine über die für den Ausführungsstapel erforderliche Größe informieren. Bei der obigen Methode, die ohne Verzweigung geliefert wird, haben wir bereits gesehen, dass der Stapel bis zu zwei Werte hat, sodass dies ziemlich einfach zu bestimmen ist. Auf komplexere Weise kann die Bereitstellung dieser Informationen jedoch leicht eine komplexe Aufgabe sein. Um die Sache noch schlimmer zu machen, können Stapelwerte unterschiedliche Größen haben. Sowohl der "lange" als auch der "doppelte" Wert belegen zwei Slots, während die anderen Werte einen verbrauchen. Als ob dies nicht genug wäre, benötigen Java Virtual Machines auch Informationen über die Größe aller lokalen Variablen im Methodenkörper. Alle diese Variablen innerhalb einer Methode werden in einem Array gespeichert, das auch beliebige Methodenparameter und "this" -Referenzen für nicht statische Methoden enthält. Wiederum verbrauchen die Werte "long" und "double" zwei Slots.

Das Verfolgen all dieser Informationen macht Byte Buddy offensichtlich zu einer vereinfachten Abstraktion, da das manuelle Zusammenstellen von Java-Bytecode mühsam und fehleranfällig ist. In Byte Buddy sind Stapelanweisungen in der Implementierung der StackManipulation-Schnittstelle enthalten. Alle Stapeloperationsimplementierungen kombinieren eine Anweisung zum Ändern eines bestimmten Stapels mit Informationen über die Auswirkung dieser Anweisung auf die Größe. Sie können eine beliebige Anzahl solcher Anweisungen einfach zu einer gemeinsamen Anweisung kombinieren. Um dies zu demonstrieren, implementieren wir zunächst die IADD-Anweisung StackManipulation.

enum IntegerSum implements StackManipulation {
 
  INSTANCE; // singleton
 
  @Override
  public boolean isValid() {
    return true;
  }
 
  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext) {
    methodVisitor.visitInsn(Opcodes.IADD);
    return new Size(-1, 0);
  }
}

Anhand der obigen Methode "apply" können wir erkennen, dass diese Stapeloperation die Anweisung "IADD" ausführt, indem die zugehörige Methode im ASM-Methodenbesucher aufgerufen wird. Zusätzlich stellt dieses Verfahren dar, dass der Befehl die aktuelle Stapelgröße um einen Schlitz reduziert. Das zweite Argument der erstellten Size-Instanz ist 0. Dies bedeutet, dass für diese Anweisung keine bestimmte Mindeststapelgröße erforderlich ist, um die Zwischenergebnisse zu berechnen. Darüber hinaus kann jede Stapelmanipulation als ungültig beschrieben werden. Dieses Verhalten kann für komplexere Stapeloperationen verwendet werden, z. B. für Objektzuweisungen, mit denen Typbeschränkungen aufgehoben werden können. Später in diesem Abschnitt sehen wir Beispiele für ungültige Stapeloperationen. Beachten Sie schließlich, dass die Stapeloperation als Singleton-Aufzählung (http://en.wikipedia.org/wiki/Singleton_pattern#The_Enum_way) beschrieben wird. Die Verwendung dieser unveränderlichen und funktionalen Stack-Operationsbeschreibungen hat sich als bewährte Methode für die interne Implementierung von Byte Buddy erwiesen. Wir empfehlen, dass Sie den gleichen Ansatz verfolgen.

Sie können die Methode implementieren, indem Sie die obige IntegerSum mit den vordefinierten Stapeloperationen IntegerConstant und MethodReturn kombinieren. In Byte Buddy ist die Methodenimplementierung in ByteCodeAppender enthalten. Es wird wie folgt implementiert:

enum SumMethod implements ByteCodeAppender {
 
  INSTANCE; // singleton
 
  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext,
                    MethodDescription instrumentedMethod) {
    if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) {
      throw new IllegalArgumentException(instrumentedMethod + " must return int");
    }
    StackManipulation.Size operandStackSize = new StackManipulation.Compound(
      IntegerConstant.forValue(10),
      IntegerConstant.forValue(50),
      IntegerSum.INSTANCE,
      MethodReturn.INTEGER
    ).apply(methodVisitor, implementationContext);
    return new Size(operandStackSize.getMaximalSize(),
                    instrumentedMethod.getStackSize());
  }
}

Auch hier ist der benutzerdefinierte "ByteCodeAppender" als Singleton-Aufzählung implementiert.

Stellen Sie vor der Implementierung der gewünschten Methode zunächst sicher, dass die instrumentierte Methode tatsächlich eine primitive Ganzzahl zurückgibt. Andernfalls wird die erstellte Klasse vom JVM-Validator abgelehnt. Anschließend werden die beiden Zahlen "10" und "50" in den Laufstapel geladen, die Summe dieser Werte angewendet und das Ergebnis der Berechnung zurückgegeben. Durch das Umschließen all dieser Anweisungen in eine zusammengesetzte Stapeloperation wird sichergestellt, dass Sie die Gesamtstapelgröße erhalten, die für die Ausführung dieser Reihe von Stapeloperationen erforderlich ist. Schließlich wird die Gesamtgrößenanforderung für diese Methode zurückgegeben. Das erste Argument des zurückgegebenen "ByteCodeAppender.Size" spiegelt die Größe wider, die erforderlich ist, damit der oben erwähnte Ausführungsstapel in "StackManipulation.Size" enthalten ist. Darüber hinaus spiegelt das zweite Argument die Größe wider, die für das lokale Variablenarray erforderlich ist. Dies ähnelt dieser Referenz, da wir hier nicht einfach die erforderliche Größe für die Methodenparameter und lokalen Variablen definiert haben.

Die Implementierung dieser Aggregationsmethode ist bereit, eine benutzerdefinierte Implementierung dieser Methode bereitzustellen, die für die domänenspezifische Sprache von Byte Buddy bereitgestellt werden kann.

enum SumImplementation implements Implementation {
 
  INSTANCE; // singleton
 
  @Override
  public InstrumentedType prepare(InstrumentedType instrumentedType) {
    return instrumentedType;
  }
 
  @Override
  public ByteCodeAppender appender(Target implementationTarget) {
    return SumMethod.INSTANCE;
  }
}

Jede Implementierung wird in zwei Schritten abgefragt. Erstens hat die Implementierung die Möglichkeit, die erstellte Klasse zu ändern, indem der `prepare'-Methode zusätzliche Felder oder Methoden hinzugefügt werden. Darüber hinaus können Sie mit dieser Vorbereitung den im vorherigen Abschnitt in Ihrer Implementierung erlernten "TypeInitializer" registrieren. Wenn keine solche Vorbereitung erforderlich ist, reicht es aus, den unveränderten "InstrumentedType" als Argument zurückzugeben. Implementierungen sollten im Allgemeinen keine einzelnen Instanzen eines instrumentierten Typs zurückgeben, sondern eine Appender-Methode für instrumentierte Typen mit allen Präfixen aufrufen. Nachdem die Implementierung zum Erstellen einer bestimmten Klasse vorbereitet wurde, wird die Methode "appender" aufgerufen, um den "ByteCodeAppender" abzurufen. Dieser Appender wird dann auch nach der Methode abgefragt, die von der angegebenen Implementierung zum Abfangen ausgewählt wurde, und nach der Methode, die während des Aufrufs der "prepare" -Methode durch die Implementierung registriert wurde.

Beachten Sie, dass Byte Buddy die Methoden "prepare" und "appender" jeder Implementierung während des Klassenerstellungsprozesses nur einmal aufruft. Dies ist garantiert, unabhängig davon, wie oft die Implementierung für die Erstellung der Klasse registriert ist. Auf diese Weise kann die Implementierung vermeiden, zu überprüfen, ob das Feld oder die Methode bereits definiert ist. Dabei vergleicht Byte Buddy die Instanzen von "Implementations" mit den Methoden "hashCode" und "equals". Im Allgemeinen sollten alle von Byte Buddy verwendeten Klassen eine sinnvolle Implementierung dieser Methoden bieten. Die Tatsache, dass Aufzählungen per Definition an solche Implementierungen angehängt sind, ist ein weiterer guter Grund für ihre Verwendung.

Nun wollen wir sehen, wie "SumImplementation" funktioniert.

abstract class SumExample {
  public abstract int calculate();
}
 
new ByteBuddy()
  .subclass(SumExample.class)
    .method(named("calculate"))
    .intercept(SumImplementation.INSTANCE)
  .make()

Herzliche Glückwünsche. Wir haben Byte Buddy erweitert, um eine benutzerdefinierte Methode zu implementieren, die die Summe von "10" und "50" berechnet und zurückgibt. Natürlich ist diese Beispielimplementierung nicht sehr praktisch. Sie können jedoch problemlos komplexere Implementierungen zusätzlich zu dieser Infrastruktur implementieren. Wenn Sie der Meinung sind, dass Sie etwas Nützliches erstellt haben, sollten Sie einen Beitrag zu Ihrer Implementierung leisten (https://bytebuddy.net/develop). Ich freue mich darauf, von dir zu hören.

Bevor wir mit dem Anpassen der anderen Komponenten von Byte Buddy fortfahren, müssen wir kurz die Verwendung von Sprunganweisungen und das sogenannte Java-Stack-Frame-Problem erläutern. Ab Java 6 erfordern Sprunganweisungen, die beispielsweise zum Implementieren von "if" - oder "while" -Anweisungen verwendet werden, einige zusätzliche Informationen, um den JVM-Validierungsprozess zu beschleunigen. Diese zusätzlichen Informationen werden als Stack-Map-Frame bezeichnet. Der Stapelzuordnungsrahmen enthält Informationen zu allen Werten, die im Ausführungsstapel eines Ziels der Sprunganweisung gefunden wurden. Durch die Bereitstellung dieser Informationen können JVM-Prüfer einige Arbeit sparen, aber im Moment liegt es an uns. Für komplexere Sprunganweisungen ist das Bereitstellen des richtigen Stapelzuordnungsrahmens eine ziemlich schwierige Aufgabe, und viele Codegenerierungsframeworks haben immer erhebliche Probleme beim Erstellen des richtigen Stapelzuordnungsrahmens. Wie gehen Sie mit diesem Problem um? Tatsächlich sind wir es einfach nicht. Die Philosophie von Byte Buddy lautet, dass die Codegenerierung nur als Klebstoff zwischen der unbekannten Typhierarchie beim Kompilieren und dem benutzerdefinierten Code verwendet werden sollte, der in diese Typen eingefügt werden muss. Daher sollte der tatsächlich generierte Code so eingeschränkt wie möglich bleiben. Wenn immer möglich, sollten bedingte Anweisungen in der JVM-Sprache Ihrer Wahl implementiert und kompiliert und dann mit minimaler Implementierung an eine bestimmte Methode gebunden werden. Ein guter Nebeneffekt dieses Ansatzes ist, dass Byte Buddy-Benutzer mit normalem Java-Code arbeiten oder vertraute Tools wie Debugger und IDE-Code-Navigatoren verwenden können. Dies ist mit generiertem Code ohne Quellcodedarstellung nicht möglich. Wenn Sie jedoch Sprunganweisungen zum Erstellen von Bytecodes verwenden müssen, müssen Sie ASM verwenden, um die richtigen Stack-Map-Frames hinzuzufügen, da Byte Buddy diese nicht automatisch einschließt.

Creating a custom assigner

Im vorherigen Abschnitt haben wir erklärt, dass die integrierte Implementierung von Byte Buddy auf "Assigner" beruht, um Variablen Werte zuzuweisen. In diesem Prozess kann "Assigner" die Konvertierung eines Werts in einen anderen anwenden, indem er die entsprechende "StackManipulation" ausgibt. Dabei bietet der integrierte Assistent von Byte Buddy beispielsweise das automatische Boxen primitiver Werte und ihrer Wrapper-Typen. Im häufigsten Fall kann der Wert direkt der Variablen zugewiesen werden. In einigen Fällen kann jedoch überhaupt nicht zugewiesen werden, was durch die Rückgabe einer ungültigen "StackManipulation" vom Zuweiser ausgedrückt werden kann. Eine regelmäßige Implementierung ungültiger Zuweisungen wird von der IllegalStackManipulation-Klasse von Byte Buddy bereitgestellt.

Um zu veranschaulichen, wie ein benutzerdefinierter Zuweiser verwendet wird, implementieren Sie einen Zuweiser, der nur Zeichenfolgenvariablen Werte zuweist, indem Sie die Methode "toString" für jeden empfangenen Wert aufrufen.

enum ToStringAssigner implements Assigner {
 
  INSTANCE; // singleton
 
  @Override
  public StackManipulation assign(TypeDescription.Generic source,
                                  TypeDescription.Generic target,
                                  Assigner.Typing typing) {
    if (!source.isPrimitive() && target.represents(String.class)) {
      MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class)
        .getDeclaredMethods()
        .filter(named("toString"))
        .getOnly();
      return MethodInvocation.invoke(toStringMethod).virtual(sourceType);
    } else {
      return StackManipulation.Illegal.INSTANCE;
    }
  }
}

Die obige Implementierung überprüft zuerst, dass der Eingabewert kein primitiver Typ ist und dass der Zielvariablentyp ein String-Typ ist. Wenn diese Bedingungen nicht erfüllt sind, gibt "Assigner" "IllegalStackManipulation" aus, um die versuchte Zuordnung ungültig zu machen. Andernfalls wird der Objekttyp "toString" anhand seines Namens identifiziert. Verwenden Sie dann "MethodInvocation" von Byte Buddy, um eine "StackManipulation" zu erstellen, die diese Methode virtuell nach Quelltyp aufruft. Schließlich können Sie diesen benutzerdefinierten "Assigner" beispielsweise wie folgt in die "FixedValue" -Implementierung von Byte Buddy integrieren:

new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(FixedValue.value(42)
      .withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE),
                    Assigner.Typing.STATIC))
  .make()

Wenn die Methode "toString" für eine Instanz des obigen Typs aufgerufen wird, wird der Zeichenfolgenwert "42" zurückgegeben. Dies ist nur möglich, indem Sie die Methode "toString" aufrufen und einen benutzerdefinierten Zuweiser verwenden, der den Typ "Integer" in "String" konvertiert. Weiteres Umschließen des benutzerdefinierten Zuweisers mit dem integrierten "PrimitiveTypeAwareAssigner", der das automatische Boxen des bereitgestellten Grundelements "int" in einen Umhüllungstyp durchführt, bevor die Zuweisung dieses umschlossenen Grundwerts an seinen inneren Zuweiser delegiert wird. Bitte beachten Sie. Andere integrierte Zuweiser sind "VoidAwareAssigner" und "ReferenceTypeAwareAssigner". Stellen Sie sicher, dass Sie in Ihren benutzerdefinierten Zuweisern aussagekräftige Methoden "hashCode" und "equals" implementieren. Diese Methoden werden normalerweise von den entsprechenden Methoden in "Implementierung" aufgerufen, die einen bestimmten Zuweiser verwenden. Implementieren Sie den Zuweiser auch als Singleton-Aufzählung, um dies nicht manuell zu tun.

Creating a custom parameter binder

Wir haben bereits im vorherigen Abschnitt erwähnt, dass es möglich ist, die Implementierung von "MethodDelegation" zu erweitern, um benutzerdefinierte Anmerkungen zu verarbeiten. Zu diesem Zweck müssen Sie einen benutzerdefinierten "ParameterBinder" bereitstellen, der weiß, wie mit einer bestimmten Anmerkung umgegangen wird. Als Beispiel möchte ich eine Anmerkung einfach zum Einfügen einer festen Zeichenfolge in einen mit Anmerkungen versehenen Parameter definieren. Definieren Sie zunächst eine solche "StringValue" -Anmerkung.

@Retention(RetentionPolicy.RUNTIME)
@interface StringValue {
  String value();
}

Sie müssen die entsprechende "RuntimePolicy" festlegen, damit die Anmerkungen zur Laufzeit sichtbar werden. Andernfalls wird die Anmerkung zur Laufzeit nicht beibehalten und Byte Buddy hat keine Chance, sie zu finden. Auf diese Weise enthält die obige Eigenschaft value die Zeichenfolge, die dem mit Anmerkungen versehenen Parameter als Wert zugewiesen wird.

Sie müssen einen entsprechenden "ParameterBinder" erstellen, der mithilfe einer benutzerdefinierten Annotation eine "StackManipulation" erstellen kann, die die Bindung für diesen Parameter darstellt. Dieser Parameterordner wird jedes Mal aufgerufen und die entsprechende Anmerkung wird durch MethodDelegation auf dem Parameter gefunden. In diesem Beispiel ist es einfach, einen benutzerdefinierten Parameterordner für die Anmerkungen zu implementieren.

enum StringValueBinder
    implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> {
 
  INSTANCE; // singleton
 
  @Override
  public Class<StringValue> getHandledType() {
    return StringValue.class;
  }
 
  @Override
  public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loaded<StringValue> annotation,
                                                         MethodDescription source,
                                                         ParameterDescription target,
                                                         Implementation.Target implementationTarget,
                                                         Assigner assigner,
                                                         Assigner.Typing typing) {
    if (!target.getType().asErasure().represents(String.class)) {
      throw new IllegalStateException(target + " makes illegal use of @StringValue");
    }
    StackManipulation constant = new TextConstant(annotation.loadSilent().value());
    return new MethodDelegationBinder.ParameterBinding.Anonymous(constant);
  }
}

Zunächst überprüft der Parameterordner, ob der Parameter target tatsächlich vom Typ String ist. Andernfalls wird eine Ausnahme ausgelöst, die den Benutzer über die Anmerkung zur unzulässigen Platzierung dieser Anmerkung informiert. Andernfalls erstellen Sie einfach eine "TextConstant", die das Laden der konstanten Stapelzeichenfolge in den Ausführungsstapel darstellt. Diese StackManipulation wird schließlich als anonymes ParameterBinding verpackt, das von der Methode zurückgegeben wird. Alternativ haben Sie möglicherweise die Parameterbindung "Unique" oder "Illegal" angegeben. Eindeutige Bindungen werden von jedem Objekt identifiziert, mit dem Sie diese Bindung von "AmbiguityResolver" erhalten können. In einem späteren Schritt kann ein solcher Resolver prüfen, ob die Parameterbindung mit einer eindeutigen Kennung registriert ist, und dann feststellen, ob diese Bindung besser ist als andere erfolgreich gebundene Methoden. Ich kann es schaffen Bei illegaler Bindung können Sie Byte Buddy mitteilen, dass bestimmte Paare von "Quell" - und "Ziel" -Methoden nicht kompatibel sind und nicht miteinander verbunden werden können.

Dies sind bereits alle Informationen, die Sie benötigen, um benutzerdefinierte Anmerkungen in Ihrer MethodDelegation-Implementierung zu verwenden. Stellen Sie nach Erhalt der ParameterBinding sicher, dass der Wert an den richtigen Parameter gebunden ist, oder verwerfen Sie das aktuelle Paar von Quell- und Zielmethoden als ungebunden. Darüber hinaus können "AmbiguityResolvers" eindeutige Bindungen überprüfen. Lassen Sie uns abschließend diese benutzerdefinierte Anmerkung ausführen.

class ToStringInterceptor {
  public static String makeString(@StringValue("Hello!") String value) {
    return value;
  }
}
 
new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(MethodDelegation.withDefaultConfiguration()
      .withBinders(StringValueBinder.INSTANCE)
      .to(ToStringInterceptor.class))
  .make()

Die Angabe von "StringValueBinder" als einziger Parameterordner ersetzt alle Standardwerte. Alternativ können Sie dem bereits registrierten einen Parameterordner hinzufügen. Wenn der ToStringInterceptor nur eine Zielmethode hat, ist die abgefangene toString-Methode der dynamischen Klasse an den Aufruf der letzteren Methode gebunden. Wenn die Zielmethode aufgerufen wird, weist Byte Buddy den Zeichenfolgenwert der Anmerkung als einzigen Parameter der Zielmethode zu.

Recommended Posts

[Übersetzung] Byte Buddy Tutorial
Zusammenfassung der Übersetzung des Apache Shiro-Tutorials
Trüffel Tutorial Folien Persönliches Übersetzungsprotokoll ①
[Einführung in Docker] Offizielles Tutorial (japanische Übersetzung)