Java >> Java tutorial >  >> Tag >> byte

En guide til Byte Buddy

1. Oversigt

Kort sagt er ByteBuddy et bibliotek til dynamisk generering af Java-klasser under kørsel.

I denne konkrete artikel skal vi bruge rammen til at manipulere eksisterende klasser, oprette nye klasser efter behov og endda opsnappe metodekald.

2. Afhængigheder

Lad os først tilføje afhængigheden til vores projekt. For Maven-baserede projekter skal vi tilføje denne afhængighed til vores pom.xml :

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.11.20</version>
</dependency>

For et Gradle-baseret projekt skal vi tilføje den samme artefakt til vores build.gradle fil:

compile net.bytebuddy:byte-buddy:1.11.20

Den seneste version kan findes på Maven Central.

3. Oprettelse af en Java-klasse ved Runtime

Lad os starte med at oprette en dynamisk klasse ved at underklassificere en eksisterende klasse. Vi tager et kig på den klassiske Hello World projekt.

I dette eksempel opretter vi en type (Klasse ), som er en underklasse af Object.class og tilsidesæt toString() metode:

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

Det, vi lige gjorde, var at oprette en forekomst af ByteBuddy. Derefter brugte vi subclass() API'en for at udvide Object.class , og vi valgte toString() af superklassen (Object.class ) ved hjælp af ElementMatchers .

Til sidst med intercept() metode, leverede vi vores implementering af toString() og returnere en fast værdi.

make() metoden udløser genereringen af ​​den nye klasse.

På dette tidspunkt er vores klasse allerede oprettet, men endnu ikke indlæst i JVM. Det er repræsenteret af en forekomst af DynamicType.Unloaded , som er en binær form af den genererede type.

Derfor skal vi indlæse den genererede klasse i JVM, før vi kan bruge den:

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

Nu kan vi instansiere dynamicType og påkald toString() metode på det:

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

Bemærk, at kalde dynamicType.toString() vil ikke virke, da det kun vil kalde toString() implementering af ByteBuddy.class .

newInstance() er en Java-reflektionsmetode, der opretter en ny instans af typen repræsenteret af denne ByteBuddy objekt; på en måde, der ligner at bruge den nye søgeord med en no-arg-konstruktør.

Indtil videre har vi kun været i stand til at tilsidesætte en metode i superklassen af ​​vores dynamiske type og returnere vores egen faste værdi. I de næste afsnit vil vi se på at definere vores metode med tilpasset logik.

4. Metodedelegering og brugerdefineret logik

I vores tidligere eksempel returnerer vi en fast værdi fra toString() metode.

I virkeligheden kræver applikationer mere kompleks logik end dette. En effektiv måde at facilitere og klargøre tilpasset logik til dynamiske typer på er uddelegering af metodekald.

Lad os skabe en dynamisk type, der underklasser Foo.class som har sayHelloFoo() metode:

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

Desuden, lad os oprette en anden klasse Bar med en statisk sayHelloBar() af samme signatur og returtype som sayHelloFoo() :

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

Lad os nu uddelegere alle påkaldelser af sayHelloFoo() til sayHelloBar() ved hjælp af ByteBuddy 's DSL. Dette giver os mulighed for at levere tilpasset logik, skrevet i ren Java, til vores nyoprettede klasse under kørsel:

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

Påkaldelse af sayHelloFoo() vil kalde sayHelloBar() tilsvarende.

Hvordan gør ByteBuddy vide hvilken metode i Bar.class at påberåbe sig? Den vælger en matchende metode i henhold til metodesignaturen, returtypen, metodenavnet og annoteringerne.

sayHelloFoo() og sayHelloBar() metoder har ikke det samme navn, men de har samme metodesignatur og returtype.

Hvis der er mere end én påkaldelig metode i Bar.class med matchende signatur og returtype, kan vi bruge @BindingPriority anmærkning for at løse tvetydigheden.

@BindingPriority tager et heltalsargument – ​​jo højere heltalsværdien er, jo højere prioritet er det at kalde den bestemte implementering. Således sayHelloBar() vil blive foretrukket frem for sayBar() i kodestykket nedenfor:

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

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

5. Metode og feltdefinition

Vi har været i stand til at tilsidesætte metoder, der er erklæret i superklassen af ​​vores dynamiske typer. Lad os gå videre ved at tilføje en ny metode (og et felt) til vores klasse.

Vi vil bruge Java-reflektion til at fremkalde den dynamisk oprettede metode:

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .name("MyClassName")
  .defineMethod("custom", String.class, Modifier.PUBLIC)
  .intercept(MethodDelegation.to(Bar.class))
  .defineField("x", String.class, Modifier.PUBLIC)
  .make()
  .load(
    getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

Method m = type.getDeclaredMethod("custom", null);
assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar());
assertNotNull(type.getDeclaredField("x"));

Vi oprettede en klasse med navnet MyClassName det er en underklasse af Object.class . Vi definerer derefter en metode, brugerdefineret, der returnerer en streng og har en offentlig adgangsmodifikator.

Ligesom vi gjorde i tidligere eksempler, implementerede vi vores metode ved at opsnappe opkald til den og uddelegere dem til Bar.class som vi oprettede tidligere i denne øvelse.

6. Omdefinering af en eksisterende klasse

Selvom vi har arbejdet med dynamisk oprettede klasser, kan vi også arbejde med allerede indlæste klasser. Dette kan gøres ved at omdefinere (eller rebasere) eksisterende klasser og bruge ByteBuddyAgent for at genindlæse dem i JVM.

Lad os først tilføje ByteBuddyAgent til vores pom.xml :

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.7.1</version>
</dependency>

Den seneste version kan findes her.

Lad os nu omdefinere sayHelloFoo() metode, vi oprettede i Foo.class tidligere:

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

7. Konklusion

I denne omfattende vejledning har vi undersøgt mulighederne i ByteBuddy grundigt. bibliotek og hvordan man bruger det til effektiv oprettelse af dynamiske klasser.

Dens dokumentation giver en dybdegående forklaring af de indre funktioner og andre aspekter af biblioteket.

Og som altid kan de komplette kodestykker til denne øvelse findes på Github.


Java tag