Introduction à JavaPoet

Introduction à JavaPoet

1. Vue d'ensemble

Dans ce didacticiel, nous allons explorer les fonctionnalités de base de la bibliothèqueJavaPoet.

JavaPoet est développé parSquare, quiprovides APIs to generate Java source code. Il peut générer des types primitifs, des types de référence et leurs variantes (telles que des classes, des interfaces, des types énumérés, des classes internes anonymes), des champs, des méthodes, des paramètres, des annotations et des Javadocs.

JavaPoet gère automatiquement l'importation des classes dépendantes. Il utilise également le modèle Builder pour spécifier la logique de génération du code Java.

2. Dépendance Maven

Pour utiliser JavaPoet, nous pouvons télécharger directement les derniersJAR file, ou définir la dépendance suivante dans nospom.xml:


    com.squareup
    javapoet
    1.10.0

3. Spécification de la méthode

Tout d'abord, passons en revue la spécification de la méthode. Pour générer une méthode, nous appelons simplement la méthodemethodBuilder() de la classeMethodSpec. Nous spécifions le nom de la méthode générée comme argumentString de la méthodemethodBuilder().

Nous pouvonsgenerate any single logical statement ending with the semi-colon en utilisant la méthodeaddStatement(). Pendant ce temps, nous pouvons définir un flux de contrôle délimité par des accolades, comme le blocif-else ou la bouclefor, dans un flux de contrôle.

Voici un exemple rapide - génération de la méthodesumOfTen() qui calculera la somme des nombres de 0 à 10:

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addStatement("int sum = 0")
  .beginControlFlow("for (int i = 0; i <= 10; i++)")
  .addStatement("sum += i")
  .endControlFlow()
  .build();

Cela produira la sortie suivante:

void sumOfTen() {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

4. Bloc de code

On peut aussiwrap one or more control flows and logical statements into one code block:

CodeBlock sumOfTenImpl = CodeBlock
  .builder()
  .addStatement("int sum = 0")
  .beginControlFlow("for (int i = 0; i <= 10; i++)")
  .addStatement("sum += i")
  .endControlFlow()
  .build();

Ce qui génère:

int sum = 0;
for (int i = 0; i <= 10; i++) {
    sum += i;
}

Nous pouvons simplifier la logique précédente dans lesMethodSpec en appelantaddCode() et en fournissant l'objetsumOfTenImpl:

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addCode(sumOfTenImpl)
  .build();

Un bloc de code est également applicable à d'autres spécifications, telles que des types et des Javadocs.

5. Spécification du champ

Ensuite, explorons la logique de spécification de champ.

Afin de générer un champ, nous utilisons la méthodebuilder() de la classeFieldSpec:

FieldSpec name = FieldSpec
  .builder(String.class, "name")
  .addModifiers(Modifier.PRIVATE)
  .build();

Cela générera le champ suivant:

private String name;

On peut également initialiser la valeur par défaut d'un champ en appelant la méthodeinitializer():

FieldSpec defaultName = FieldSpec
  .builder(String.class, "DEFAULT_NAME")
  .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
  .initializer("\"Alice\"")
  .build();

Ce qui génère:

private static final String DEFAULT_NAME = "Alice";

6. Paramètre Spécification

Explorons maintenant la logique de spécification des paramètres.

Dans le cas où nous voulons ajouter un paramètre à la méthode, nous pouvons appeler lesaddParameter() dans la chaîne des appels de fonction dans le générateur.

Dans le cas de types de paramètres plus complexes, nous pouvons utiliser le générateurParameterSpec:

ParameterSpec strings = ParameterSpec
  .builder(
    ParameterizedTypeName.get(ClassName.get(List.class), TypeName.get(String.class)),
    "strings")
  .build();

On peut aussi ajouter le modificateur de la méthode, tel quepublic et / oustatic:

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

Voici à quoi ressemble le code Java généré:

public static void sumOfTen(int number, List strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

7. Spécification de type

Après avoir exploré les moyens de générer des méthodes, des champs et des paramètres, examinons maintenant les spécifications de type.

Pour déclarer un type, nous pouvons utiliser lesTypeSpec which can build classes, interfaces, and enumerated types.

7.1. Générer une classe

Afin de générer une classe, nous pouvons utiliser la méthodeclassBuilder() de la classeTypeSpec.

Nous pouvons également spécifier ses modificateurs, par exemple, les modificateurs d'accès depublic andfinal. En plus des modificateurs de classe, nous pouvons également spécifier des champs et des méthodes en utilisant les classesFieldSpec etMethodSpec déjà mentionnées.

Notez que les méthodesaddField() etaddMethod() sont également disponibles lors de la génération d'interfaces ou de classes internes anonymes.

Jetons un coup d'œil à l'exemple de générateur de classe suivant:

TypeSpec person = TypeSpec
  .classBuilder("Person")
  .addModifiers(Modifier.PUBLIC)
  .addField(name)
  .addMethod(MethodSpec
    .methodBuilder("getName")
    .addModifiers(Modifier.PUBLIC)
    .returns(String.class)
    .addStatement("return this.name")
    .build())
  .addMethod(MethodSpec
    .methodBuilder("setName")
    .addParameter(String.class, "name")
    .addModifiers(Modifier.PUBLIC)
    .returns(String.class)
    .addStatement("this.name = name")
    .build())
  .addMethod(sumOfTen)
  .build();

Et voici à quoi ressemble le code généré:

public class Person {
    private String name;

    public String getName() {
        return this.name;
    }

    public String setName(String name) {
        this.name = name;
    }

    public static void sumOfTen(int number, List strings) {
        int sum = 0;
        for (int i = 0; i <= 10; i++) {
            sum += i;
        }
    }
}

7.2. Générer une interface

Pour générer une interface Java, nous utilisons la méthodeinterfaceBuilder() desTypeSpec.

Nous pouvons également définir une méthode par défaut en spécifiant la valeur du modificateurDEFAULT dans leaddModifiers():

TypeSpec person = TypeSpec
  .interfaceBuilder("Person")
  .addModifiers(Modifier.PUBLIC)
  .addField(defaultName)
  .addMethod(MethodSpec
    .methodBuilder("getName")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .build())
  .addMethod(MethodSpec
    .methodBuilder("getDefaultName")
    .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
    .addCode(CodeBlock
      .builder()
      .addStatement("return DEFAULT_NAME")
      .build())
    .build())
  .build();

Il générera le code Java suivant:

public interface Person {
    private static final String DEFAULT_NAME = "Alice";

    void getName();

    default void getDefaultName() {
        return DEFAULT_NAME;
    }
}

7.3. Générer une énumération

Pour générer un type énuméré, nous pouvons utiliser la méthodeenumBuilder() desTypeSpec. Pour spécifier chaque valeur énumérée, nous pouvons appeler la méthodeaddEnumConstant():

TypeSpec gender = TypeSpec
  .enumBuilder("Gender")
  .addModifiers(Modifier.PUBLIC)
  .addEnumConstant("MALE")
  .addEnumConstant("FEMALE")
  .addEnumConstant("UNSPECIFIED")
  .build();

La sortie de la logiqueenumBuilder() mentionnée ci-dessus est:

public enum Gender {
    MALE,
    FEMALE,
    UNSPECIFIED
}

7.4. Générer une classe interne anonyme

Pour générer une classe interne anonyme, nous pouvons utiliser la méthodeanonymousClassBuilder() de la classeTypeSpec. Notez quewe must specify the parent class in the addSuperinterface() method. Sinon, il utilisera la classe parent par défaut, qui estObject:

TypeSpec comparator = TypeSpec
  .anonymousClassBuilder("")
  .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
  .addMethod(MethodSpec
    .methodBuilder("compare")
    .addModifiers(Modifier.PUBLIC)
    .addParameter(String.class, "a")
    .addParameter(String.class, "b")
    .returns(int.class)
    .addStatement("return a.length() - b.length()")
    .build())
  .build();

Cela générera le code Java suivant:

new Comparator() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

8. Spécification d'annotation

Pour ajouter une annotation au code généré, nous pouvons appeler la méthodeaddAnnotation() dans une classe de générateurMethodSpec ouFieldSpec:

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addAnnotation(Override.class)
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

Ce qui génère:

@Override
public static void sumOfTen(int number, List strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

Au cas où nous aurions besoin de spécifier la valeur du membre, nous pouvons appeler la méthodeaddMember() de la classeAnnotationSpec:

AnnotationSpec toString = AnnotationSpec
  .builder(ToString.class)
  .addMember("exclude", "\"name\"")
  .build();

Cela générera l'annotation suivante:

@ToString(
    exclude = "name"
)

9. Générer des Javadocs

Javadoc peut être généré en utilisantCodeBlock, ou en spécifiant la valeur directement:

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addJavadoc(CodeBlock
    .builder()
    .add("Sum of all integers from 0 to 10")
    .build())
  .addAnnotation(Override.class)
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

Cela générera le code Java suivant:

/**
 * Sum of all integers from 0 to 10
 */
@Override
public static void sumOfTen(int number, List strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

10. Mise en page

Revérifions l'exemple de l'initialiseurFieldSpec dansSection 5 qui contient un caractère d'échappement utilisé pour échapper à la valeur «Alice»String:

initializer("\"Alice\"")

Il existe également un exemple similaire dansSection 8 lorsque nous définissons le membre exclu d'une annotation:

addMember("exclude", "\"name\"")

Cela devient compliqué lorsque notre code JavaPoet se développe et a beaucoup d'instructions d'échappement similairesString ou de concaténationString.

La fonction de formatage de chaîne de JavaPoet facilite le formatage deString dans les méthodesbeginControlFlow(),addStatement() ouinitializer(). The syntax is similar to String.format() functionality in Java. It can help to format literals, strings, types, and names.

10.1. Formatage littéral

JavaPoet replaces $L with a literal value in the output. Nous pouvons spécifier n'importe quel type primitif et les valeursString dans le (s) argument (s):

private MethodSpec generateSumMethod(String name, int from, int to, String operator) {
    return MethodSpec
      .methodBuilder(name)
      .returns(int.class)
      .addStatement("int sum = 0")
      .beginControlFlow("for (int i = $L; i <= $L; i++)", from, to)
      .addStatement("sum = sum $L i", operator)
      .endControlFlow()
      .addStatement("return sum")
      .build();
}

Dans le cas où nous appelons lesgenerateSumMethod() avec les valeurs suivantes spécifiées:

generateSumMethod("sumOfOneHundred", 0, 100, "+");

JavaPoet générera la sortie suivante:

int sumOfOneHundred() {
    int sum = 0;
    for (int i = 0; i <= 100; i++) {
        sum = sum + i;
    }
    return sum;
}

10.2. String Formatage

Le formatageString génère une valeur entre guillemets, qui se réfère exclusivement au typeString en Java. JavaPoet replaces $S with a String value in the output:

private static MethodSpec generateStringSupplier(String methodName, String fieldName) {
    return MethodSpec
      .methodBuilder(methodName)
      .returns(String.class)
      .addStatement("return $S", fieldName)
      .build();
}

Dans le cas où nous appelons la méthodegenerateGetter() et fournissons ces valeurs:

generateStringSupplier("getDefaultName", "Bob");

Nous obtiendrons le code Java généré suivant:

String getDefaultName() {
    return "Bob";
}

10.3. Formatage de type

JavaPoet replaces $T with a type in the generated Java code. JavaPoet gère automatiquement le type dans la déclaration d'importation. Si nous avions fourni le type sous la forme d'un littéral, JavaPoet ne gérerait pas l'importation.

MethodSpec getCurrentDateMethod = MethodSpec
  .methodBuilder("getCurrentDate")
  .returns(Date.class)
  .addStatement("return new $T()", Date.class)
  .build();

JavaPoet générera la sortie suivante:

Date getCurrentDate() {
    return new Date();
}

10.4. Formatage du nom

Au cas où nous aurions besoin derefer to a name of a variable/parameter, field or method, we can use $N dans le formateurString de JavaPoet.

Nous pouvons ajouter lesgetCurrentDateMethod()précédents à la nouvelle méthode de référencement:

MethodSpec dateToString = MethodSpec
  .methodBuilder("getCurrentDateAsString")
  .returns(String.class)
  .addStatement(
    "$T formatter = new $T($S)",
    DateFormat.class,
    SimpleDateFormat.class,
    "MM/dd/yyyy HH:mm:ss")
  .addStatement("return formatter.format($N())", getCurrentDateMethod)
  .build();

Ce qui génère:

String getCurrentDateAsString() {
    DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
    return formatter.format(getCurrentDate());
}

11. Générer des expressions Lambda

Nous pouvons utiliser les fonctionnalités que nous avons déjà explorées pour générer une expression Lambda. Par exemple, un bloc de code qui imprime le champname ou une variable plusieurs fois:

CodeBlock printNameMultipleTimes = CodeBlock
  .builder()
  .addStatement("$T<$T> names = new $T<>()", List.class, String.class, ArrayList.class)
  .addStatement("$T.range($L, $L).forEach(i -> names.add(name))", IntStream.class, 0, 10)
  .addStatement("names.forEach(System.out::println)")
  .build();

Cette logique génère la sortie suivante:

List names = new ArrayList<>();
IntStream.range(0, 10).forEach(i -> names.add(name));
names.forEach(System.out::println);

12. Production de la sortie en utilisantJavaFile

The JavaFile class helps to configure and produce the output of the generated code. Pour générer du code Java, nous construisons simplement lesJavaFile, fournissant le nom du package et une instance de l'objetTypeSpec.

12.1. Indentation du code

Par défaut, JavaPoet utilise deux espaces pour l'indentation. Pour garder la cohérence, tous les exemples de ce didacticiel ont été présentés avec une indentation de 4 espaces, qui est configurée via la méthodeindent():

JavaFile javaFile = JavaFile
  .builder("com.example.javapoet.person", person)
  .indent("    ")
  .build();

12.2. Importations statiques

Au cas où nous aurions besoin d'ajouter une importation statique, nous pouvons définir le type et le nom de la méthode spécifique dans leJavaFile en appelant la méthodeaddStaticImport():

JavaFile javaFile = JavaFile
  .builder("com.example.javapoet.person", person)
  .indent("    ")
  .addStaticImport(Date.class, "UTC")
  .addStaticImport(ClassName.get("java.time", "ZonedDateTime"), "*")
  .build();

Qui génère les instructions d'importation statiques suivantes:

import static java.util.Date.UTC;
import static java.time.ZonedDateTime.*;

12.3. Sortie

La méthodewriteTo() fournit des fonctionnalités pour écrire le code dans plusieurs cibles, telles que le flux de sortie standard (System.out) etFile.

Pour écrire du code Java dans un flux de sortie standard, nous appelons simplement la méthodewriteTo() et fournissons lesSystem.out comme argument:

JavaFile javaFile = JavaFile
  .builder("com.example.javapoet.person", person)
  .indent("    ")
  .addStaticImport(Date.class, "UTC")
  .addStaticImport(ClassName.get("java.time", "ZonedDateTime"), "*")
  .build();

javaFile.writeTo(System.out);

La méthodewriteTo() accepte également lesjava.nio.file.Path etjava.io.File. Nous pouvons fournir l'objetPath ouFile correspondant afin de générer le fichier de code source Java dans le dossier / chemin de destination:

Path path = Paths.get(destinationPath);
javaFile.writeTo(path);

Pour plus d'informations sur lesJavaFile, reportez-vous auxJavadoc.

13. Conclusion

Cet article a été une introduction aux fonctionnalités de JavaPoet, telles que les méthodes de génération, les champs, les paramètres, les types, les annotations et les Javadocs.

JavaPoet est conçu pour la génération de code uniquement. Au cas où nous souhaiterions effectuer une métaprogrammation avec Java, JavaPoet à partir de la version 1.10.0 ne prend pas en charge la compilation et l'exécution de code.

Comme toujours, les exemples et extraits de code sont disponiblesover on GitHub.