Un guide pour Byte Buddy

Un guide pour Byte Buddy

1. Vue d'ensemble

En termes simples,ByteBuddy est une bibliothèque pour générer dynamiquement des classes Java au moment de l'exécution.

Dans cet article précis, nous allons utiliser le framework pour manipuler les classes existantes, créer de nouvelles classes à la demande et même intercepter les appels de méthode.

2. Les dépendances

Ajoutons d'abord la dépendance à notre projet. Pour les projets basés sur Maven, nous devons ajouter cette dépendance à nospom.xml:


    net.bytebuddy
    byte-buddy
    1.7.1

Pour un projet basé sur Gradle, nous devons ajouter le même artefact à notre fichierbuild.gradle:

compile net.bytebuddy:byte-buddy:1.7.1

La dernière version peut être trouvée surMaven Central.

3. Création d'une classe Java au moment de l'exécution

Commençons par créer une classe dynamique en sous-classant une classe existante. Nous allons jeter un œil au projet classiqueHello World.

Dans cet exemple, nous créons un type (Class) qui est une sous-classe deObject.class et remplaçons la méthodetoString():

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

Ce que nous venons de faire, c'est de créer une instance deByteBuddy. Ensuite, nous avons utilisé lessubclass() API pour étendreObject.class, et nous avons sélectionné lestoString() de la super classe (Object.class) s) en utilisantElementMatchers.

Enfin, avec la méthodeintercept(), nous avons fourni notre implémentation detoString() et renvoyé une valeur fixe.

La méthodemake() déclenche la génération de la nouvelle classe.

À ce stade, notre classe est déjà créée mais pas encore chargée dans la JVM. Il est représenté par une instance deDynamicType.Unloaded, qui est une forme binaire du type généré.

Par conséquent, nous devons charger la classe générée dans la JVM avant de pouvoir l'utiliser:

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

Maintenant, nous pouvons instancier lesdynamicType et appeler la méthodetoString() dessus:

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

Notez que l'appel dedynamicType.toString() ne fonctionnera pas car cela n'appellera que l'implémentationtoString() deByteBuddy.class.

LenewInstance() est une méthode de réflexion Java qui crée une nouvelle instance du type représenté par cet objetByteBuddy; d'une manière similaire à l'utilisation du mot-clénew avec un constructeur sans argument.

Jusqu'à présent, nous n'avons pu remplacer qu'une méthode de la super-classe de notre type dynamique et renvoyer notre propre valeur fixe. Dans les prochaines sections, nous verrons comment définir notre méthode avec une logique personnalisée.

4. Délégation de méthode et logique personnalisée

Dans notre exemple précédent, nous renvoyons une valeur fixe de la méthodetoString().

En réalité, les applications nécessitent une logique plus complexe que celle-ci. La délégation des appels de méthode est un moyen efficace de faciliter et d’approvisionner la logique personnalisée en types dynamiques.

Créons un type dynamique qui sous-classeFoo.class qui a la méthodesayHelloFoo():

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

De plus, créons une autre classeBar avec unsayHelloBar() statique de la même signature et du même type de retour quesayHelloFoo():

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

Désormais, déléguons toutes les invocations desayHelloFoo() àsayHelloBar() en utilisant le DSL deByteBuddy. Cela nous permet de fournir une logique personnalisée, écrite en Java pur, à notre classe nouvellement créée au moment de l'exécution:

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

L'appel dessayHelloFoo() invoquera lessayHelloBar() en conséquence.

How does ByteBuddy know which method in Bar.class to invoke? Il sélectionne une méthode correspondante en fonction de la signature de la méthode, du type de retour, du nom de la méthode et des annotations.

Les méthodessayHelloFoo() etsayHelloBar() n'ont pas le même nom, mais elles ont la même signature de méthode et le même type de retour.

S'il y a plus d'une méthode invocable dansBar.class avec la signature et le type de retour correspondants, nous pouvons utiliser l'annotation@BindingPriority pour résoudre l'ambiguïté.

@BindingPriority prend un argument entier - plus la valeur entière est élevée, plus la priorité d'appel de l'implémentation particulière est élevée. Ainsi,sayHelloBar() sera préféré àsayBar() dans l'extrait de code ci-dessous:

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

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

5. Définition de la méthode et du champ

Nous avons pu redéfinir les méthodes déclarées dans la super classe de nos types dynamiques. Allons plus loin en ajoutant une nouvelle méthode (et un champ) à notre classe.

Nous allons utiliser la réflexion Java pour appeler la méthode créée dynamiquement:

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

Nous avons créé une classe avec le nomMyClassName qui est une sous-classe deObject.class. Nous définissons ensuite une méthode,custom, qui renvoie unString et a un modificateur d'accèspublic.

Tout comme nous l'avons fait dans les exemples précédents, nous avons implémenté notre méthode en interceptant les appels et en les déléguant àBar.class que nous avons créés plus tôt dans ce tutoriel.

6. Redéfinir une classe existante

Bien que nous ayons travaillé avec des classes créées dynamiquement, nous pouvons également travailler avec des classes déjà chargées. Cela peut être fait en redéfinissant (ou en rebasant) les classes existantes et en utilisantByteBuddyAgent pour les recharger dans la JVM.

Tout d'abord, ajoutonsByteBuddyAgent à nospom.xml:


    net.bytebuddy
    byte-buddy-agent
    1.7.1

La dernière version peut êtrefound here.

Maintenant, redéfinissons la méthodesayHelloFoo() que nous avons créée dansFoo.class plus tôt:

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

Dans ce guide élaboré, nous avons étudié en détail les fonctionnalités de la bibliothèqueByteBuddy et comment l’utiliser pour créer efficacement des classes dynamiques.

Sondocumentation offre une explication approfondie du fonctionnement interne et d'autres aspects de la bibliothèque.

Et, comme toujours, les extraits de code complets de ce didacticiel peuvent être trouvésover on Github.