Einführung in JavaPoet

Einführung in JavaPoet

1. Überblick

In diesem Tutorial werden die grundlegenden Funktionen derJavaPoet-Bibliothek erläutert.

JavaPoet wird durchSquare entwickelt, wobeiprovides APIs to generate Java source code. Es kann primitive Typen, Referenztypen und deren Varianten (wie Klassen, Interfaces, Aufzählungstypen, anonyme innere Klassen), Felder, Methoden, Parameter, Anmerkungen und Javadocs generieren.

JavaPoet verwaltet den Import der abhängigen Klassen automatisch. Außerdem wird das Builder-Muster verwendet, um die Logik zum Generieren von Java-Code anzugeben.

2. Maven-Abhängigkeit

Um JavaPoet verwenden zu können, können wir die neuestenJAR file direkt herunterladen oder die folgende Abhängigkeit in unserenpom.xml: definieren


    com.squareup
    javapoet
    1.10.0

3. Methodenspezifikation

Lassen Sie uns zunächst die Methodenspezifikation durchgehen. Um eine Methode zu generieren, rufen wir einfach diemethodBuilder()-Methode derMethodSpec-Klasse auf. Wir geben den generierten Methodennamen alsString Argument dermethodBuilder() Methode an.

Wir könnengenerate any single logical statement ending with the semi-colon mit der MethodeaddStatement(). In der Zwischenzeit können wir einen Kontrollfluss definieren, der mit geschweiften Klammern begrenzt ist, z. B.if-else Block oderfor Schleife, in einem Kontrollfluss.

Hier ein kurzes Beispiel: Generieren dersumOfTen()-Methode, mit der die Summe der Zahlen von 0 bis 10 berechnet wird:

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

Dies wird die folgende Ausgabe erzeugen:

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

4. Codeblock

Wir können auchwrap 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();

Welches erzeugt:

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

Wir können die frühere Logik inMethodSpec vereinfachen, indem wiraddCode() aufrufen und das ObjektsumOfTenImpl bereitstellen:

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

Ein Codeblock gilt auch für andere Spezifikationen, z. B. Typen und Javadocs.

5. Feldspezifikation

Weiter - Lassen Sie uns die Feldspezifikationslogik untersuchen.

Um ein Feld zu generieren, verwenden wir diebuilder()-Methode derFieldSpec-Klasse:

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

Dadurch wird das folgende Feld generiert:

private String name;

Wir können den Standardwert eines Feldes auch initialisieren, indem wir die Methodeinitializer()aufrufen:

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

Welches erzeugt:

private static final String DEFAULT_NAME = "Alice";

6. Parameterspezifikation

Lassen Sie uns nun die Parameterspezifikationslogik untersuchen.

Wenn wir der Methode einen Parameter hinzufügen möchten, können wiraddParameter() innerhalb der Kette der Funktionsaufrufe im Builder aufrufen.

Bei komplexeren Parametertypen können wir den Builder vonParameterSpecverwenden:

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

Wir können auch den Modifikator der Methode hinzufügen, z. B.public und / oderstatic:

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

So sieht der generierte Java-Code aus:

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

7. Typenspezifikation

Nachdem wir die Möglichkeiten zum Generieren von Methoden, Feldern und Parametern untersucht haben, werfen wir nun einen Blick auf die Typspezifikationen.

Um einen Typ zu deklarieren, können wirTypeSpec which can build classes, interfaces, and enumerated types verwenden.

7.1. Eine Klasse generieren

Um eine Klasse zu generieren, können wir dieclassBuilder()-Methode derTypeSpec-Klasse verwenden.

Wir können auch die Modifikatoren angeben, z. B. die Zugriffsmodifikatorenpublic andfinal. Zusätzlich zu Klassenmodifikatoren können wir auch Felder und Methoden mit den bereits erwähnten KlassenFieldSpec undMethodSpec angeben.

Beachten Sie, dass die MethodenaddField() undaddMethod() auch beim Generieren von Schnittstellen oder anonymen inneren Klassen verfügbar sind.

Schauen wir uns das folgende Beispiel für den Klassenersteller an:

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

Und so sieht der generierte Code aus:

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. Schnittstelle generieren

Um eine Java-Schnittstelle zu generieren, verwenden wir dieinterfaceBuilder()-Methode derTypeSpec.

Wir können auch eine Standardmethode definieren, indem wir den Modifikatorwert vonDEFAULTinaddModifiers()angeben:

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

Es wird der folgende Java-Code generiert:

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

    void getName();

    default void getDefaultName() {
        return DEFAULT_NAME;
    }
}

7.3. Eine Aufzählung generieren

Um einen Aufzählungstyp zu generieren, können wir dieenumBuilder()-Methode derTypeSpec verwenden. Um jeden aufgezählten Wert anzugeben, können wir die MethodeaddEnumConstant() aufrufen:

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

Die Ausgabe der oben genanntenenumBuilder()-Logik ist:

public enum Gender {
    MALE,
    FEMALE,
    UNSPECIFIED
}

7.4. Generieren einer anonymen inneren Klasse

Um eine anonyme innere Klasse zu generieren, können wir dieanonymousClassBuilder()-Methode derTypeSpec-Klasse verwenden. Beachten Sie, dasswe must specify the parent class in the addSuperinterface() method. Andernfalls wird die standardmäßige übergeordnete Klasse verwendet, nämlichObject:

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

Dadurch wird der folgende Java-Code generiert:

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

8. Anmerkungsspezifikation

Um dem generierten Code eine Anmerkung hinzuzufügen, können Sie dieaddAnnotation()-Methode in einer Builder-Klasse vonMethodSpec oderFieldSpecaufrufen:

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

Welches erzeugt:

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

Falls wir den Elementwert angeben müssen, können wir die MethodeaddMember()der KlasseAnnotationSpecaufrufen:

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

Dadurch wird die folgende Anmerkung generiert:

@ToString(
    exclude = "name"
)

9. Javadocs generieren

Javadoc kann mitCodeBlock, oder durch direkte Angabe des Werts generiert werden:

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

Dadurch wird der folgende Java-Code generiert:

/**
 * 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. Formatierung

Überprüfen wir noch einmal das Beispiel des Initialisierers vonFieldSpecinSection 5, der ein Escape-Zeichen enthält, mit dem der Wert von "Alice"Stringmaskiert wird:

initializer("\"Alice\"")

Es gibt auch ein ähnliches Beispiel inSection 8, wenn wir das ausgeschlossene Mitglied einer Annotation definieren:

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

Es wird unhandlich, wenn unser JavaPoet-Code wächst und viele ähnlicheStringEscape- oderStringVerkettungsanweisungen enthält.

Die Zeichenfolgenformatierungsfunktion in JavaPoet erleichtert die Formatierung vonStringin den MethodenbeginControlFlow(),addStatement() oderinitializer(). The syntax is similar to String.format() functionality in Java. It can help to format literals, strings, types, and names.

10.1. Literale Formatierung

JavaPoet replaces $L with a literal value in the output. Wir können jeden primitiven Typ undString Werte in den Argumenten angeben:

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

Falls wirgenerateSumMethod() mit den folgenden angegebenen Werten aufrufen:

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

JavaPoet generiert die folgende Ausgabe:

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

10.2. String Formatierung

Die Formatierung vonStringgeneriert einen Wert mit dem Anführungszeichen, der sich ausschließlich auf den TypStringin Java bezieht. 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();
}

Falls wir die MethodegenerateGetter()aufrufen und folgende Werte angeben:

generateStringSupplier("getDefaultName", "Bob");

Wir erhalten folgenden generierten Java-Code:

String getDefaultName() {
    return "Bob";
}

10.3. Geben Sie Formatierung ein

JavaPoet replaces $T with a type in the generated Java code. JavaPoet behandelt den Typ in der Importanweisung automatisch. Wenn wir den Typ stattdessen als Literal angegeben hätten, würde JavaPoet den Import nicht verarbeiten.

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

JavaPoet generiert die folgende Ausgabe:

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

10.4. Namensformatierung

Falls wirrefer to a name of a variable/parameter, field or method, we can use $N imString-Formatierer von JavaPoet benötigen.

Wir können die vorherigengetCurrentDateMethod() zur neuen Referenzierungsmethode hinzufügen:

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

Welches erzeugt:

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

11. Lambda-Ausdrücke generieren

Wir können die Funktionen, die wir bereits untersucht haben, verwenden, um einen Lambda-Ausdruck zu generieren. Zum Beispiel ein Codeblock, der das Feldname oder eine Variable mehrmals druckt:

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

Diese Logik generiert die folgende Ausgabe:

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

12. Erzeugen der Ausgabe mitJavaFile

The JavaFile class helps to configure and produce the output of the generated code. Um Java-Code zu generieren, erstellen wir einfach dieJavaFile,, die den Paketnamen und eine Instanz desTypeSpec-Objekts angeben.

12.1. Codeeinrückung

Standardmäßig verwendet JavaPoet zwei Leerzeichen zum Einrücken. Um die Konsistenz zu gewährleisten, wurden alle Beispiele in diesem Lernprogramm mit einem 4-Leerzeichen-Einzug dargestellt, der über die Methodeindent()konfiguriert wird:

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

12.2. Statische Importe

Falls wir einen statischen Import hinzufügen müssen, können wir den Typ und den spezifischen Methodennamen inJavaFile definieren, indem wir die MethodeaddStaticImport() aufrufen:

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

Wodurch die folgenden statischen Importanweisungen generiert werden:

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

12.3. Ausgabe

Die MethodewriteTo() bietet Funktionen zum Schreiben des Codes in mehrere Ziele, z. B. Standardausgabestream (System.out) undFile.

Um Java-Code in einen Standardausgabestream zu schreiben, rufen wir einfach die MethodewriteTo() auf und gebenSystem.out als Argument an:

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

Die MethodewriteTo() akzeptiert auchjava.nio.file.Path undjava.io.File. Wir können das entsprechendePath- oderFile-Objekt bereitstellen, um die Java-Quellcodedatei im Zielordner / -pfad zu generieren:

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

Ausführlichere Informationen zuJavaFile finden Sie unterJavadoc.

13. Fazit

Dieser Artikel enthält eine Einführung in JavaPoet-Funktionen wie das Generieren von Methoden, Feldern, Parametern, Typen, Anmerkungen und Javadocs.

JavaPoet ist nur für die Codegenerierung vorgesehen. Für den Fall, dass wir Metaprogrammierung mit Java durchführen möchten, unterstützt JavaPoet ab Version 1.10.0 das Kompilieren und Ausführen von Code nicht.

Wie immer sind die Beispiele und Codefragmenteover on GitHub verfügbar.