Einführung in JavaPoet

1. Übersicht

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

JavaPoet wurde von Square entwickelt, das APIs zum Generieren von Java-Quellcode bereitstellt . Es kann primitive Typen, Referenztypen und deren Varianten (wie Klassen, Schnittstellen, 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 neueste JAR-Datei direkt herunterladen oder die folgende Abhängigkeit in unserer pom.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 die methodBuilder () -Methode der MethodSpec- Klasse auf. Wir geben den generierten Methodennamen als String- Argument der methodBuilder () -Methode an.

Mit der Methode addStatement () können wir jede einzelne logische Anweisung generieren, die mit dem Semikolon endet . In der Zwischenzeit können wir einen Kontrollfluss definieren, der mit geschweiften Klammern begrenzt ist, z. B. if-else- Block oder for- Schleife in einem Kontrollfluss.

Hier ein kurzes Beispiel: Generieren der sumOfTen () -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 erzeugt die folgende Ausgabe:

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

4. Codeblock

Wir können auch einen oder mehrere Kontrollflüsse und logische Anweisungen in einen Codeblock einschließen :

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

Was erzeugt:

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

Wir können die frühere Logik in der MethodSpec vereinfachen, indem wir addCode () aufrufen und das sumOfTenImpl- Objekt bereitstellen :

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

Ein Codeblock gilt auch für andere Spezifikationen wie Typen und Javadocs.

5. Feldspezifikation

Als nächstes wollen wir die Feldspezifikationslogik untersuchen.

Um ein Feld zu generieren, verwenden wir die builder () -Methode der FieldSpec- Klasse:

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

Dadurch wird das folgende Feld generiert:

private String name;

Sie können den Standardwert eines Felds auch durch Aufrufen der Methode initializer () initialisieren :

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

Was 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 wir addParameter () innerhalb der Kette der Funktionsaufrufe im Builder aufrufen .

Bei komplexeren Parametertypen können wir den ParameterSpec Builder verwenden:

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 / oder static:

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 wir TypeSpec verwenden, mit dem Klassen, Schnittstellen und Aufzählungstypen erstellt werden können .

7.1. Eine Klasse generieren

Um eine Klasse zu generieren, können wir die classBuilder () -Methode der TypeSpec- Klasse verwenden.

Wir können auch seine Modifikatoren angeben, zum Beispiel öffentliche und endgültige Zugriffsmodifikatoren. Zusätzlich zu Klassenmodifikatoren können wir auch Felder und Methoden mithilfe der bereits erwähnten Klassen FieldSpec und MethodSpec angeben .

Beachten Sie, dass die Methoden addField () und addMethod () 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

To generate a Java interface, we use the interfaceBuilder() method of the TypeSpec.

We can also define a default method by specifying DEFAULT modifier value in the addModifiers():

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

It will generate the following Java code:

public interface Person { private static final String DEFAULT_NAME = "Alice"; void getName(); default void getDefaultName() { return DEFAULT_NAME; } }

7.3. Generating an Enum

To generate an enumerated type, we can use the enumBuilder() method of the TypeSpec. To specify each enumerated value, we can call the addEnumConstant() method:

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

The output of the aforementioned enumBuilder() logic is:

public enum Gender { MALE, FEMALE, UNSPECIFIED }

7.4. Generating an Anonymous Inner Class

To generate an anonymous inner class, we can use the anonymousClassBuilder() method of the TypeSpec class. Note that we must specify the parent class in the addSuperinterface() method. Otherwise, it will use the default parent class, which is Object:

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

This will generate the following Java code:

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

8. Annotation Specification

To add an annotation to generated code, we can call the addAnnotation() method in a MethodSpec or FieldSpec builder class:

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

Which generates:

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

In case we need to specify the member value, we can call the addMember() method of the AnnotationSpec class:

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

This will generate the following annotation:

@ToString( exclude = "name" )

9. Generating Javadocs

Javadoc can be generated using CodeBlock, or by specifying the value directly:

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

This will generate the following Java code:

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

Let's recheck the example of the FieldSpec initializer in Section 5 which contains an escape char used to escape the “Alice” String value:

initializer("\"Alice\"")

There is also a similar example in Section 8 when we define the excluded member of an annotation:

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

It becomes unwieldy when our JavaPoet code grows and has a lot of similar String escape or String concatenation statements.

The String formatting feature in JavaPoet makes String formatting in beginControlFlow(), addStatement() or initializer() methods easier. The syntax is similar to String.format() functionality in Java. It can help to format literals, strings, types, and names.

10.1. Literal Formatting

JavaPoet replaces $L with a literal value in the output. We can specify any primitive type and String values in the 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(); }

In case we call the generateSumMethod() with the following values specified:

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

JavaPoet will generate the following output:

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

10.2. String Formatting

String formatting generates a value with the quotation mark, which refers exclusively to String type in 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(); }

In case we call the generateGetter() method and provide these values:

generateStringSupplier("getDefaultName", "Bob");

We will get the following generated Java code:

String getDefaultName() { return "Bob"; }

10.3. Type Formatting

JavaPoet replaces $T with a type in the generated Java code. JavaPoet handles the type in the import statement automatically. If we had provided the type as a literal instead, JavaPoet would not handle the import.

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

JavaPoet will generate the following output:

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

10.4. Name Formatting

In case we need to refer to a name of a variable/parameter, field or method, we can use $N in JavaPoet's String formatter.

We can add the previous getCurrentDateMethod() to the new referencing method:

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

Which generates:

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

11. Generating Lambda Expressions

We can make use of the features that we've already explored to generate a Lambda expression. For instance, a code block which prints the name field or a variable multiple times:

CodeBlock printNameMultipleTimes = CodeBlock .builder() .addStatement("$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();

That logic generates the following output:

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

12. Producing the Output Using JavaFile

The JavaFile class helps to configure and produce the output of the generated code. To generate Java code, we simply build the JavaFile, provide the package name and an instance of the TypeSpec object.

12.1. Code Indentation

By default, JavaPoet uses two spaces for indentation. To keep the consistency, all examples in this tutorial were presented with 4 spaces indentation, which is configured via indent() method:

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

12.2. Static Imports

In case we need to add a static import, we can define the type and specific method name in the JavaFile by calling the addStaticImport() method:

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

Which generates the following static import statements:

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

12.3. Output

The writeTo() method provides functionality to write the code into multiple targets, such as standard output stream (System.out) and File.

To write Java code to a standard output stream, we simply call the writeTo() method, and provide the System.out as the argument:

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

The writeTo() method also accepts java.nio.file.Path and java.io.File. We can provide the corresponding Path or File object in order to generate the Java source code file into the destination folder/path:

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

For more detailed information regarding JavaFile, please refer to the Javadoc.

13. Conclusion

Dieser Artikel war 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 Codefragmente auf GitHub verfügbar.