Introdução ao JavaPoet
1. Visão geral
Neste tutorial, vamos explorar as funcionalidades básicas da bibliotecaJavaPoet.
JavaPoet é desenvolvido porSquare, queprovides APIs to generate Java source code. Ele pode gerar tipos primitivos, tipos de referência e suas variantes (como classes, interfaces, tipos enumerados, classes internas anônimas), campos, métodos, parâmetros, anotações e Javadocs.
JavaPoet gerencia a importação das classes dependentes automaticamente. Ele também usa o padrão Builder para especificar a lógica para gerar o código Java.
2. Dependência do Maven
Para usar o JavaPoet, podemos baixar diretamente osJAR file mais recentes ou definir a seguinte dependência em nossopom.xml:
com.squareup
javapoet
1.10.0
3. Especificação de Método
Primeiro, vamos examinar a especificação do método. Para gerar um método, simplesmente chamamos o métodomethodBuilder() da classeMethodSpec. Especificamos o nome do método gerado como um argumentoString do métodomethodBuilder().
Podemosgenerate any single logical statement ending with the semi-colon usando o métodoaddStatement(). Enquanto isso, podemos definir um fluxo de controle delimitado por chaves, como o blocoif-else ou o loopfor, em um fluxo de controle.
Aqui está um exemplo rápido - gerando o métodosumOfTen() que irá calcular a soma dos números de 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();
Isso produzirá a seguinte saída:
void sumOfTen() {
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
}
}
4. Bloco de código
Também podemoswrap 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();
O que gera:
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
}
Podemos simplificar a lógica anterior emMethodSpec chamandoaddCode()e fornecendo o objetosumOfTenImpl:
MethodSpec sumOfTen = MethodSpec
.methodBuilder("sumOfTen")
.addCode(sumOfTenImpl)
.build();
Um bloco de código também é aplicável a outras especificações, como tipos e Javadocs.
5. Especificação de Campo
A seguir - vamos explorar a lógica de especificação de campo.
Para gerar um campo, usamos o métodobuilder() da classeFieldSpec:
FieldSpec name = FieldSpec
.builder(String.class, "name")
.addModifiers(Modifier.PRIVATE)
.build();
Isso irá gerar o seguinte campo:
private String name;
Também podemos inicializar o valor padrão de um campo chamando o métodoinitializer():
FieldSpec defaultName = FieldSpec
.builder(String.class, "DEFAULT_NAME")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("\"Alice\"")
.build();
O que gera:
private static final String DEFAULT_NAME = "Alice";
6. Especificação de parâmetros
Vamos agora explorar a lógica de especificação de parâmetro.
Caso desejemos adicionar um parâmetro ao método, podemos chamaraddParameter() dentro da cadeia de chamadas de função no construtor.
No caso de tipos de parâmetros mais complexos, podemos usar o construtorParameterSpec:
ParameterSpec strings = ParameterSpec
.builder(
ParameterizedTypeName.get(ClassName.get(List.class), TypeName.get(String.class)),
"strings")
.build();
Também podemos adicionar o modificador do método, comopublice / oustatic:
MethodSpec sumOfTen = MethodSpec
.methodBuilder("sumOfTen")
.addParameter(int.class, "number")
.addParameter(strings)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addCode(sumOfTenImpl)
.build();
Esta é a aparência do código Java gerado:
public static void sumOfTen(int number, List strings) {
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
}
}
7. Especificação de Tipo
Depois de explorar as maneiras de gerar métodos, campos e parâmetros, agora vamos dar uma olhada nas especificações de tipo.
Para declarar um tipo, podemos usarTypeSpec which can build classes, interfaces, and enumerated types.
7.1. Gerando uma Classe
Para gerar uma classe, podemos usar o métodoclassBuilder() da classeTypeSpec.
Também podemos especificar seus modificadores, por exemplo, modificadores de acessopublic andfinal. Além dos modificadores de classe, também podemos especificar campos e métodos usando as classesFieldSpeceMethodSpec já mencionadas.
Observe que os métodosaddField()eaddMethod() também estão disponíveis ao gerar interfaces ou classes internas anônimas.
Vamos dar uma olhada no seguinte exemplo de construtor de classe:
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();
E é assim que o código gerado se parece:
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. Gerando uma Interface
Para gerar uma interface Java, usamos o métodointerfaceBuilder() doTypeSpec.
Também podemos definir um método padrão especificando o valor do modificadorDEFAULT emaddModifiers():
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();
Ele irá gerar o seguinte código Java:
public interface Person {
private static final String DEFAULT_NAME = "Alice";
void getName();
default void getDefaultName() {
return DEFAULT_NAME;
}
}
7.3. Gerando um Enum
Para gerar um tipo enumerado, podemos usar o métodoenumBuilder() deTypeSpec. Para especificar cada valor enumerado, podemos chamar o métodoaddEnumConstant():
TypeSpec gender = TypeSpec
.enumBuilder("Gender")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("MALE")
.addEnumConstant("FEMALE")
.addEnumConstant("UNSPECIFIED")
.build();
A saída da lógicaenumBuilder() acima mencionada é:
public enum Gender {
MALE,
FEMALE,
UNSPECIFIED
}
7.4. Gerando uma Classe Interna Anônima
Para gerar uma classe interna anônima, podemos usar o métodoanonymousClassBuilder() da classeTypeSpec. Observe quewe must specify the parent class in the addSuperinterface() method. Caso contrário, ele usará a classe pai padrão, que é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();
Isso irá gerar o seguinte código Java:
new Comparator() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
8. Especificação de anotação
Para adicionar uma anotação ao código gerado, podemos chamar o métodoaddAnnotation() em uma classe de construtorMethodSpec ouFieldSpec:
MethodSpec sumOfTen = MethodSpec
.methodBuilder("sumOfTen")
.addAnnotation(Override.class)
.addParameter(int.class, "number")
.addParameter(strings)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addCode(sumOfTenImpl)
.build();
O que gera:
@Override
public static void sumOfTen(int number, List strings) {
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
}
}
No caso de precisarmos especificar o valor do membro, podemos chamar o métodoaddMember() da classeAnnotationSpec:
AnnotationSpec toString = AnnotationSpec
.builder(ToString.class)
.addMember("exclude", "\"name\"")
.build();
Isso irá gerar a seguinte anotação:
@ToString(
exclude = "name"
)
9. Gerando Javadocs
Javadoc pode ser gerado usandoCodeBlock, ou especificando o valor diretamente:
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();
Isso irá gerar o seguinte código Java:
/**
* 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. Formatação
Vamos verificar novamente o exemplo do inicializadorFieldSpec emSection 5 que contém um caractere de escape usado para escapar do valorString de "Alice":
initializer("\"Alice\"")
Há também um exemplo semelhante emSection 8 quando definimos o membro excluído de uma anotação:
addMember("exclude", "\"name\"")
Torna-se difícil quando nosso código JavaPoet cresce e tem muitas instruções de escapeString ou de concatenaçãoString semelhantes.
O recurso de formatação de String em JavaPoet torna a formataçãoString nos métodosbeginControlFlow(),addStatement() ouinitializer() mais fácil. The syntax is similar to String.format() functionality in Java. It can help to format literals, strings, types, and names.
10.1. Formatação Literal
JavaPoet replaces $L with a literal value in the output. Podemos especificar qualquer tipo primitivo e valoresString no (s) argumento (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();
}
No caso de chamarmos ogenerateSumMethod() com os seguintes valores especificados:
generateSumMethod("sumOfOneHundred", 0, 100, "+");
JavaPoet irá gerar a seguinte saída:
int sumOfOneHundred() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
10.2. String Formatação
A formataçãoString gera um valor com aspas, que se refere exclusivamente ao tipoString em 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();
}
No caso de chamarmos o métodogenerateGetter() e fornecermos estes valores:
generateStringSupplier("getDefaultName", "Bob");
Obteremos o seguinte código Java gerado:
String getDefaultName() {
return "Bob";
}
10.3. Formatação de tipo
JavaPoet replaces $T with a type in the generated Java code. JavaPoet manipula o tipo na instrução de importação automaticamente. Se tivéssemos fornecido o tipo como literal, o JavaPoet não trataria da importação.
MethodSpec getCurrentDateMethod = MethodSpec
.methodBuilder("getCurrentDate")
.returns(Date.class)
.addStatement("return new $T()", Date.class)
.build();
JavaPoet irá gerar a seguinte saída:
Date getCurrentDate() {
return new Date();
}
10.4. Formatação de Nome
No caso de precisarmosrefer to a name of a variable/parameter, field or method, we can use $N no formatadorString do JavaPoet.
Podemos adicionar ogetCurrentDateMethod() anterior ao novo método de referência:
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();
O que gera:
String getCurrentDateAsString() {
DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
return formatter.format(getCurrentDate());
}
11. Gerando expressões Lambda
Podemos usar os recursos que já exploramos para gerar uma expressão Lambda. Por exemplo, um bloco de código que imprime o camponame ou uma variável várias vezes:
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();
Essa lógica gera a seguinte saída:
List names = new ArrayList<>();
IntStream.range(0, 10).forEach(i -> names.add(name));
names.forEach(System.out::println);
12. Produzindo a saída usandoJavaFile
The JavaFile class helps to configure and produce the output of the generated code. Para gerar o código Java, simplesmente construímosJavaFile,, fornecendo o nome do pacote e uma instância do objetoTypeSpec.
12.1. Recuo do código
Por padrão, o JavaPoet usa dois espaços para recuo. Para manter a consistência, todos os exemplos neste tutorial foram apresentados com recuo de 4 espaços, que é configurado via métodoindent():
JavaFile javaFile = JavaFile
.builder("com.example.javapoet.person", person)
.indent(" ")
.build();
12.2. Importações estáticas
No caso de precisarmos adicionar uma importação estática, podemos definir o tipo e o nome do método específico emJavaFile chamando o métodoaddStaticImport():
JavaFile javaFile = JavaFile
.builder("com.example.javapoet.person", person)
.indent(" ")
.addStaticImport(Date.class, "UTC")
.addStaticImport(ClassName.get("java.time", "ZonedDateTime"), "*")
.build();
O que gera as seguintes instruções de importação estática:
import static java.util.Date.UTC;
import static java.time.ZonedDateTime.*;
12.3. Resultado
O métodowriteTo() fornece funcionalidade para escrever o código em vários destinos, como fluxo de saída padrão (System.out) eFile.
Para escrever código Java em um fluxo de saída padrão, simplesmente chamamos o métodowriteTo() e fornecemosSystem.out como argumento:
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);
O métodowriteTo() também aceitajava.nio.file.Pathejava.io.File. Podemos fornecer o objetoPath ouFile correspondente para gerar o arquivo de código-fonte Java na pasta / caminho de destino:
Path path = Paths.get(destinationPath);
javaFile.writeTo(path);
Para obter informações mais detalhadas sobreJavaFile, consulteJavadoc.
13. Conclusão
Este artigo foi uma introdução às funcionalidades do JavaPoet, como gerar métodos, campos, parâmetros, tipos, anotações e Javadocs.
O JavaPoet foi projetado apenas para geração de código. No caso de desejarmos fazer metaprogramação com Java, JavaPoet a partir da versão 1.10.0 não oferece suporte para compilação e execução de código.
Como sempre, os exemplos e trechos de código estão disponíveisover on GitHub.