Een gids voor Byte Buddy

1. Overzicht

Simpel gezegd, ByteBuddy is een bibliotheek voor het dynamisch genereren van Java-klassen tijdens runtime.

In dit to-the-point artikel gaan we het raamwerk gebruiken om bestaande klassen te manipuleren, nieuwe klassen op aanvraag te maken en zelfs methodeaanroepen te onderscheppen.

2. Afhankelijkheden

Laten we eerst de afhankelijkheid aan ons project toevoegen. Voor op Maven gebaseerde projecten moeten we deze afhankelijkheid toevoegen aan onze pom.xml:

 net.bytebuddy byte-buddy 1.7.1 

Voor een op Gradle gebaseerd project moeten we hetzelfde artefact toevoegen aan ons build.gradle het dossier:

compileer net.bytebuddy: byte-buddy: 1.7.1

De nieuwste versie is te vinden op Maven Central.

3. Een Java-klasse maken tijdens runtime

Laten we beginnen met het maken van een dynamische klasse door een bestaande klasse onder te verdelen. We zullen de klassieker bekijken Hallo Wereld project.

In dit voorbeeld maken we een type (Klasse) dat is een subklasse van Object. Klasse en negeer de toString () methode:

DynamicType.Unloaded unloadedType = nieuwe ByteBuddy () .subclass (Object.class) .method (ElementMatchers.isToString ()) .intercept (FixedValue.value ("Hallo wereld ByteBuddy!")) .Make ();

Wat we zojuist hebben gedaan, was een instantie van ByteBuddy. Vervolgens hebben we de subclass () API uitbreiden Object. Klasse, en we hebben de toString () van de superklasse (Object. Klasse) gebruik makend van ElementMatchers.

Eindelijk, met de onderscheppen() methode, hebben we onze implementatie verzorgd van toString () en retourneer een vaste waarde.

De maken() methode triggert de generatie van de nieuwe klasse.

Op dit punt is onze klasse al gemaakt, maar nog niet geladen in de JVM. Het wordt vertegenwoordigd door een instantie van DynamicType.Unloaded, wat een binaire vorm is van het gegenereerde type.

Daarom moeten we de gegenereerde klasse in de JVM laden voordat we deze kunnen gebruiken:

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

Nu kunnen we het dynamicType en een beroep doen op de toString () methode erop:

assertEquals (dynamicType.newInstance (). toString (), "Hallo wereld ByteBuddy!");

Merk op dat bellen dynamicType.toString () zal niet werken omdat dat alleen de toString () invoer van ByteBuddy.class.

De newInstance () is een Java-reflectiemethode die een nieuwe instantie maakt van het type dat hierdoor wordt weergegeven ByteBuddy voorwerp; op een manier die vergelijkbaar is met het gebruik van de nieuw trefwoord met een no-arg constructor.

Tot nu toe hebben we alleen een methode in de superklasse van ons dynamische type kunnen overschrijven en onze eigen vaste waarde kunnen retourneren. In de volgende secties zullen we kijken naar het definiëren van onze methode met aangepaste logica.

4. Methodeoverdracht en aangepaste logica

In ons vorige voorbeeld retourneren we een vaste waarde van de toString () methode.

In werkelijkheid vereisen applicaties een complexere logica dan dit. Een effectieve manier om aangepaste logica voor dynamische typen te faciliteren en in te richten, is het delegeren van methodeaanroepen.

Laten we een dynamisch type maken dat subklassen Foo. Klasse die heeft de zeg halloFoo () methode:

public String sayHelloFoo () {retourneer "Hallo in Foo!"; }

Laten we bovendien nog een klas maken Bar met een statisch zeg hallobar () van dezelfde handtekening en hetzelfde retourtype als zeg halloFoo ():

openbare statische String sayHelloBar () {retourneer "Holla in Bar!"; }

Laten we nu alle aanroepen van zeg halloFoo () naar zeg hallobar () gebruik makend van ByteBuddy‘S DSL. Dit stelt ons in staat om tijdens runtime aangepaste logica, geschreven in pure Java, aan onze nieuw gemaakte klasse te leveren:

String r = new ByteBuddy () .subclass (Foo.class) .method (genaamd ("sayHelloFoo"). En (isDeclaredBy (Foo.class) .and (retourneert (String.class)))). Onderscheppen (MethodDelegation.to (Bar.class)) .make () .load (getClass (). GetClassLoader ()) .getLoaded () .newInstance () .sayHelloFoo (); assertEquals (r, Bar.sayHelloBar ());

Een beroep doen op het zeg halloFoo () zal de zeg hallobar () overeenkomstig.

Hoe werkt ByteBuddy weet welke methode in Bar. Klasse aanroepen? Het kiest een overeenkomende methode op basis van de methodehandtekening, het retourneringstype, de naam van de methode en annotaties.

De zeg halloFoo () en zeg hallobar () methoden hebben niet dezelfde naam, maar ze hebben dezelfde methodehandtekening en hetzelfde retourtype.

Als er meer dan één aanroepbare methode is in Bar. Klasse met bijpassende handtekening en retourtype, kunnen we gebruiken @BindingPriority annotatie om de dubbelzinnigheid op te lossen.

@BindingPriority neemt een integer-argument - hoe hoger de integer-waarde, hoe hoger de prioriteit van het aanroepen van de specifieke implementatie. Dus, zeg hallobar () zal de voorkeur hebben boven sayBar () in het onderstaande codefragment:

@BindingPriority (3) openbare statische String sayHelloBar () {retourneer "Holla in Bar!"; } @BindingPriority (2) openbare statische String sayBar () {return "bar"; }

5. Methode en velddefinitie

We zijn erin geslaagd de methoden te overschrijven die zijn gedeclareerd in de superklasse van onze dynamische typen. Laten we verder gaan door een nieuwe methode (en een veld) aan onze klas toe te voegen.

We zullen Java-reflectie gebruiken om de dynamisch gemaakte methode aan te roepen:

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

We hebben een klas gemaakt met de naam MyClassName dat is een subklasse van Object. Klasse. We definiëren dan een methode, Op maat, dat geeft een Draad en heeft een openbaar toegangsmodificator.

Net als in eerdere voorbeelden hebben we onze methode geïmplementeerd door oproepen ernaar te onderscheppen en ze te delegeren aan Bar. Klasse die we eerder in deze tutorial hebben gemaakt.

6. Een bestaande klasse opnieuw definiëren

Hoewel we met dynamisch gemaakte klassen hebben gewerkt, kunnen we ook met reeds geladen klassen werken. Dit kan gedaan worden door bestaande klassen te herdefiniëren (of rebasen) en te gebruiken ByteBuddyAgent om ze opnieuw in de JVM te laden.

Laten we eerst toevoegen ByteBuddyAgent naar onze pom.xml:

 net.bytebuddy byte-buddy-agent 1.7.1 

De laatste versie vind je hier.

Laten we nu de zeg halloFoo () methode die we hebben gemaakt in Foo. Klasse eerder:

ByteBuddyAgent.install (); nieuwe ByteBuddy () .redefine (Foo.class) .method (genaamd ("sayHelloFoo")) .intercept (FixedValue.value ("Hello Foo Redefined")) .make () .load (Foo.class.getClassLoader (), ClassReloadingStrategy.fromInstalledAgent ()); Foo f = nieuwe Foo (); assertEquals (f.sayHelloFoo (), "Hallo Foo opnieuw gedefinieerd");

7. Conclusie

In deze uitgebreide handleiding hebben we uitgebreid gekeken naar de mogelijkheden van het ByteBuddy bibliotheek en hoe u deze kunt gebruiken voor het efficiënt maken van dynamische klassen.

De documentatie biedt een diepgaande uitleg van de interne werking en andere aspecten van de bibliotheek.

En, zoals altijd, zijn de volledige codefragmenten voor deze tutorial te vinden op Github.