Introduzione a JavaPoet

1. Panoramica

In questo tutorial, esploreremo le funzionalità di base della libreria JavaPoet.

JavaPoet è sviluppato da Square, che fornisce API per generare codice sorgente Java . Può generare tipi primitivi, tipi di riferimento e loro varianti (come classi, interfacce, tipi enumerati, classi interne anonime), campi, metodi, parametri, annotazioni e Javadoc.

JavaPoet gestisce automaticamente l'importazione delle classi dipendenti. Utilizza anche il pattern Builder per specificare la logica per generare il codice Java.

2. Dipendenza da Maven

Per poter utilizzare JavaPoet, possiamo scaricare direttamente il file JAR più recente o definire la seguente dipendenza nel nostro pom.xml:

 com.squareup javapoet 1.10.0 

3. Specifica del metodo

Per prima cosa, esaminiamo la specifica del metodo. Per generare un metodo, chiamiamo semplicemente il metodo methodBuilder () della classe MethodSpec . Specifichiamo il nome del metodo generato come argomento String del metodo methodBuilder () .

Possiamo generare qualsiasi singola istruzione logica che termini con il punto e virgola utilizzando il metodo addStatement () . Nel frattempo, possiamo definire un flusso di controllo delimitato da parentesi graffe, come il blocco if-else o il ciclo for , in un flusso di controllo.

Ecco un rapido esempio: generazione del metodo sumOfTen () che calcolerà la somma dei numeri da 0 a 10:

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

Questo produrrà il seguente output:

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

4. Blocco codice

Possiamo anche racchiudere uno o più flussi di controllo e istruzioni logiche in un blocco di codice :

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

Che genera:

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

Possiamo semplificare la logica precedente in MethodSpec chiamando addCode () e fornendo l' oggetto sumOfTenImpl :

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

Un blocco di codice è applicabile anche ad altre specifiche, come tipi e Javadoc.

5. Specifica del campo

Successivamente, esploriamo la logica della specifica del campo.

Per generare un campo, utilizziamo il metodo builder () della classe FieldSpec :

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

Questo genererà il seguente campo:

private String name;

Possiamo anche inizializzare il valore predefinito di un campo chiamando il metodo initializer () :

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

Che genera:

private static final String DEFAULT_NAME = "Alice";

6. Specifica dei parametri

Esploriamo ora la logica di specifica dei parametri.

Nel caso in cui vogliamo aggiungere un parametro al metodo, possiamo chiamare addParameter () all'interno della catena di chiamate di funzione nel builder.

In caso di tipi di parametri più complessi, possiamo utilizzare il generatore ParameterSpec :

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

Possiamo anche aggiungere il modificatore del metodo, come public e / o static:

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

Ecco come appare il codice Java generato:

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

7. Specifica del tipo

Dopo aver esplorato i modi per generare metodi, campi e parametri, diamo ora un'occhiata alle specifiche del tipo.

Per dichiarare un tipo, possiamo usare TypeSpec che può creare classi, interfacce e tipi enumerati .

7.1. Generazione di una classe

Per generare una classe, possiamo utilizzare il metodo classBuilder () della classe TypeSpec .

Possiamo anche specificare i suoi modificatori, ad esempio, i modificatori di accesso pubblico e finale . Oltre ai modificatori di classe, possiamo anche specificare campi e metodi utilizzando le classi FieldSpec e MethodSpec già citate .

Notare che i metodi addField () e addMethod () sono disponibili anche quando si generano interfacce o classi interne anonime.

Diamo un'occhiata al seguente esempio di creazione di classi:

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

Ed ecco come appare il codice generato:

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. Generazione di un'interfaccia

Per generare un'interfaccia Java, utilizziamo il metodo interfaceBuilder () di TypeSpec.

Possiamo anche definire un metodo predefinito specificando il valore del modificatore DEFAULT in 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();

Genererà il seguente codice Java:

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

7.3. Generazione di un enum

Per generare un tipo enumerato, possiamo utilizzare il metodo enumBuilder () di TypeSpec . Per specificare ogni valore enumerato, possiamo chiamare il metodo addEnumConstant () :

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

L'output della suddetta logica enumBuilder () è:

public enum Gender { MALE, FEMALE, UNSPECIFIED }

7.4. Generazione di una classe interna anonima

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

Questo articolo è stato un'introduzione alle funzionalità di JavaPoet, come la generazione di metodi, campi, parametri, tipi, annotazioni e Javadoc.

JavaPoet è progettato solo per la generazione di codice. Nel caso in cui volessimo fare la metaprogrammazione con Java, JavaPoet a partire dalla versione 1.10.0 non supporta la compilazione e l'esecuzione del codice.

Come sempre, gli esempi e gli snippet di codice sono disponibili su GitHub.