Una guida a Byte Buddy

1. Panoramica

In poche parole, ByteBuddy è una libreria per la generazione dinamica di classi Java in fase di esecuzione.

In questo articolo al punto, useremo il framework per manipolare le classi esistenti, creare nuove classi su richiesta e persino intercettare le chiamate ai metodi.

2. Dipendenze

Aggiungiamo prima la dipendenza al nostro progetto. Per i progetti basati su Maven, dobbiamo aggiungere questa dipendenza al nostro pom.xml :

 net.bytebuddy byte-buddy 1.7.1 

Per un progetto basato su Gradle, dobbiamo aggiungere lo stesso artefatto al nostro file build.gradle :

compile net.bytebuddy:byte-buddy:1.7.1

L'ultima versione può essere trovata su Maven Central.

3. Creazione di una classe Java in runtime

Iniziamo creando una classe dinamica creando una sottoclasse di una classe esistente. Daremo uno sguardo al classico progetto Hello World .

In questo esempio, creiamo un tipo ( Class ) che è una sottoclasse di Object.class e sovrascriviamo il metodo toString () :

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

Quello che abbiamo appena fatto è stato creare un'istanza di ByteBuddy. Quindi, abbiamo utilizzato l' API subclass () per estendere Object.class e abbiamo selezionato toString () della super classe ( Object.class ) utilizzando ElementMatchers .

Infine, con il metodo intercept () , abbiamo fornito la nostra implementazione di toString () e restituito un valore fisso.

Il metodo make () avvia la generazione della nuova classe.

A questo punto, la nostra classe è già creata ma non ancora caricata nella JVM. È rappresentato da un'istanza di DynamicType.Unloaded , che è una forma binaria del tipo generato.

Pertanto, dobbiamo caricare la classe generata nella JVM prima di poterla utilizzare:

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

Ora possiamo istanziare il dynamicType e invocare il metodo toString () su di esso:

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

Nota che la chiamata a dynamicType.toString () non funzionerà poiché richiamerà solo l' implementazione toString () di ByteBuddy.class .

Il newInstance () è un metodo di riflessione Java che crea una nuova istanza del tipo rappresentato da questo ByteBuddy oggetto; in un modo simile all'utilizzo della nuova parola chiave con un costruttore senza argomenti.

Finora, siamo stati solo in grado di sovrascrivere un metodo nella super classe del nostro tipo dinamico e restituire un nostro valore fisso. Nelle prossime sezioni vedremo come definire il nostro metodo con una logica personalizzata.

4. Delega del metodo e logica personalizzata

Nel nostro esempio precedente, restituiamo un valore fisso dal metodo toString () .

In realtà, le applicazioni richiedono una logica più complessa di questa. Un modo efficace per facilitare e fornire la logica personalizzata ai tipi dinamici è la delega delle chiamate al metodo.

Creiamo un tipo dinamico che sottoclasse Foo.class che ha il metodo sayHelloFoo () :

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

Inoltre, creiamo un'altra classe Bar con un sayHelloBar () statico della stessa firma e tipo di ritorno di sayHelloFoo () :

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

Ora, deleghiamo tutte le invocazioni di sayHelloFoo () a sayHelloBar () usando il DSL di ByteBuddy . Questo ci consente di fornire logica personalizzata, scritta in puro Java, alla nostra classe appena creata in fase di runtime:

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());

Invocare sayHelloFoo () richiamerà di conseguenza sayHelloBar () .

Come fa ByteBuddy a sapere quale metodo in Bar.class richiamare? Sceglie un metodo di corrispondenza in base alla firma del metodo, al tipo restituito, al nome del metodo e alle annotazioni.

I metodi sayHelloFoo () e sayHelloBar () non hanno lo stesso nome, ma hanno la stessa firma del metodo e il tipo restituito.

Se è presente più di un metodo invocabile in Bar.class con firma e tipo restituito corrispondenti, è possibile utilizzare l' annotazione @BindingPriority per risolvere l'ambiguità.

@BindingPriority accetta un argomento intero: maggiore è il valore intero, maggiore è la priorità di chiamata della particolare implementazione. Pertanto, sayHelloBar () sarà preferito a sayBar () nello snippet di codice seguente:

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

5. Metodo e definizione del campo

Siamo stati in grado di sovrascrivere i metodi dichiarati nella super classe dei nostri tipi dinamici. Andiamo oltre aggiungendo un nuovo metodo (e un campo) alla nostra classe.

Useremo la riflessione Java per invocare il metodo creato dinamicamente:

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"));

Abbiamo creato una classe con il nome MyClassName che è una sottoclasse di Object.class . Definiamo quindi un metodo, personalizzato, che restituisce una stringa e ha un modificatore di accesso pubblico .

Proprio come abbiamo fatto negli esempi precedenti, abbiamo implementato il nostro metodo intercettando le chiamate ad esso e delegandole a Bar.class che abbiamo creato in precedenza in questo tutorial.

6. Ridefinire una classe esistente

Anche se abbiamo lavorato con classi create dinamicamente, possiamo lavorare anche con classi già caricate. Questo può essere fatto ridefinendo (o ribasando) le classi esistenti e utilizzando ByteBuddyAgent per ricaricarle nella JVM.

Per prima cosa, aggiungiamo ByteBuddyAgent al nostro pom.xml :

 net.bytebuddy byte-buddy-agent 1.7.1 

L'ultima versione può essere trovata qui.

Ora ridefiniamo il metodo sayHelloFoo () che abbiamo creato in Foo.class in precedenza:

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

In questa guida elaborata, abbiamo esaminato ampiamente le capacità della libreria ByteBuddy e come usarla per la creazione efficiente di classi dinamiche.

La sua documentazione offre una spiegazione approfondita del funzionamento interno e di altri aspetti della biblioteca.

E, come sempre, gli snippet di codice completi per questo tutorial possono essere trovati su Github.