Перейти к основному содержимому

Введение в JavaPoet

· 10 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим основные функции библиотеки JavaPoet .

JavaPoet разработан компанией Square , которая предоставляет API для генерации исходного кода Java . Он может генерировать примитивные типы, ссылочные типы и их варианты (такие как классы, интерфейсы, перечисляемые типы, анонимные внутренние классы), поля, методы, параметры, аннотации и Javadocs.

JavaPoet автоматически управляет импортом зависимых классов. Он также использует шаблон Builder, чтобы указать логику для создания кода Java.

2. Зависимость от Maven

Чтобы использовать JavaPoet, мы можем напрямую загрузить последний файл JAR или определить следующую зависимость в нашем pom.xml:

<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.10.0</version>
</dependency>

3. Спецификация метода

Во-первых, давайте пройдемся по спецификации метода. Чтобы сгенерировать метод, мы просто вызываем метод methodBuilder () класса MethodSpec . Мы указываем имя сгенерированного метода в качестве аргумента String метода methodBuilder() .

Мы можем сгенерировать любой отдельный логический оператор, заканчивающийся точкой с запятой , используя метод addStatement() . Между тем, мы можем определить один поток управления, ограниченный фигурными скобками, например блок if-else или цикл for , в потоке управления.

Вот быстрый пример — создание метода sumOfTen() , который будет вычислять сумму чисел от 0 до 10:

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

Это приведет к следующему результату:

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

4. Кодовый блок

Мы также можем обернуть один или несколько потоков управления и логических операторов в один блок кода :

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

Что генерирует:

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

Мы можем упростить предыдущую логику в MethodSpec , вызвав addCode() и предоставив объект sumOfTenImpl :

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

Блок кода также применим к другим спецификациям, таким как типы и Javadocs.

5. Спецификация поля

Далее — давайте изучим логику спецификации поля.

Чтобы сгенерировать поле, мы используем метод builder() класса FieldSpec :

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

Это создаст следующее поле:

private String name;

Мы также можем инициализировать значение поля по умолчанию, вызвав метод initializer() :

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

Что генерирует:

private static final String DEFAULT_NAME = "Alice";

6. Спецификация параметров

Давайте теперь рассмотрим логику спецификации параметров.

Если мы хотим добавить параметр в метод, мы можем вызвать addParameter() в цепочке вызовов функций в построителе.

В случае более сложных типов параметров мы можем использовать конструктор ParameterSpec :

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

Мы также можем добавить модификатор метода, например, public и/или static:

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

Вот как выглядит сгенерированный Java-код:

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

7. Спецификация типа

Изучив способы создания методов, полей и параметров, теперь давайте взглянем на спецификации типов.

Чтобы объявить тип, мы можем использовать TypeSpec , который может создавать классы, интерфейсы и перечисляемые типы .

7.1. Создание класса

Чтобы сгенерировать класс, мы можем использовать метод classBuilder () класса TypeSpec .

Мы также можем указать его модификаторы, например модификаторы доступа public и final . Помимо модификаторов класса, мы также можем указать поля и методы, используя уже упомянутые классы FieldSpec и MethodSpec .

Обратите внимание, что методы addField() и addMethod() также доступны при создании интерфейсов или анонимных внутренних классов.

Давайте взглянем на следующий пример конструктора классов:

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

А вот как выглядит сгенерированный код:

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<String> strings) {
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
}
}
}

7.2. Создание интерфейса

Чтобы сгенерировать интерфейс Java, мы используем метод interfaceBuilder() класса TypeSpec.

Мы также можем определить метод по умолчанию, указав значение модификатора DEFAULT в 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();

Он сгенерирует следующий код Java:

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

void getName();

default void getDefaultName() {
return DEFAULT_NAME;
}
}

7.3. Создание перечисления

Чтобы сгенерировать перечисляемый тип, мы можем использовать метод enumBuilder() класса TypeSpec . Чтобы указать каждое перечисляемое значение, мы можем вызвать метод addEnumConstant() :

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

Вывод вышеупомянутой логики enumBuilder() :

public enum Gender {
MALE,
FEMALE,
UNSPECIFIED
}

7.4. Создание анонимного внутреннего класса

Чтобы сгенерировать анонимный внутренний класс, мы можем использовать метод анонимного классаBuilder() класса TypeSpec . Обратите внимание, что мы должны указать родительский класс в методе addSuperinterface() . В противном случае он будет использовать родительский класс по умолчанию, которым является 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();

Это сгенерирует следующий код Java:

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

8. Спецификация аннотации

Чтобы добавить аннотацию к сгенерированному коду, мы можем вызвать метод addAnnotation() в классе построителя MethodSpec или FieldSpec :

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

Что генерирует:

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

Если нам нужно указать значение члена, мы можем вызвать метод addMember() класса AnnotationSpec :

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

Это создаст следующую аннотацию:

@ToString(
exclude = "name"
)

9. Генерация Javadocs

Javadoc можно сгенерировать с помощью CodeBlock или путем прямого указания значения:

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

Это сгенерирует следующий код Java:

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

10. Форматирование

Давайте еще раз проверим пример инициализатора FieldSpec в разделе 5 , который содержит экранирующий символ, используемый для экранирования строкового значения «Алиса » :

initializer("\"Alice\"")

В разделе 8 также есть аналогичный пример , когда мы определяем исключенный элемент аннотации:

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

Он становится громоздким, когда наш код JavaPoet разрастается и содержит множество похожих операторов экранирования строк или конкатенации строк .

Функция форматирования строк в JavaPoet упрощает форматирование строк в методах beginControlFlow() , addStatement() или initializer() . Синтаксис похож на функциональность String.format() в Java. Это может помочь форматировать литералы, строки, типы и имена .

10.1. Буквенное форматирование

JavaPoet заменяет $L буквальным значением в выходных данных. Мы можем указать любой примитивный тип и значения String в аргументах:

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

В случае, если мы вызываем generateSumMethod() со следующими указанными значениями:

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

JavaPoet сгенерирует следующий вывод:

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

10.2. Форматирование строк

Форматирование строки генерирует значение с кавычками, которые относятся исключительно к типу String в Java. JavaPoet заменяет $S строковым значением в выводе :

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

Если мы вызовем метод generateGetter() и предоставим следующие значения:

generateStringSupplier("getDefaultName", "Bob");

Мы получим следующий сгенерированный код Java:

String getDefaultName() {
return "Bob";
}

10.3. Форматирование типа

JavaPoet заменяет $T типом в сгенерированном коде Java . JavaPoet автоматически обрабатывает тип в операторе импорта. Если бы вместо этого мы предоставили тип как литерал, JavaPoet не обработал бы импорт.

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

JavaPoet сгенерирует следующий вывод:

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

10.4. Форматирование имени

Если нам нужно сослаться на имя переменной/параметра, поля или метода, мы можем использовать $N в средстве форматирования строк JavaPoet .

Мы можем добавить предыдущий метод getCurrentDateMethod() к новому методу ссылки:

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

Что генерирует:

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

11. Генерация лямбда-выражений

Мы можем использовать функции, которые мы уже изучили, для создания лямбда-выражения. Например, блок кода, который печатает поле имени или переменную несколько раз:

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

Эта логика генерирует следующий вывод:

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

12. Создание вывода с использованием JavaFile

Класс JavaFile помогает настроить и создать вывод сгенерированного кода . Чтобы сгенерировать Java-код, мы просто создаем JavaFile, указываем имя пакета и экземпляр объекта TypeSpec .

12.1. Отступ кода

По умолчанию JavaPoet использует два пробела для отступа. Чтобы сохранить согласованность, все примеры в этом руководстве были представлены с отступом в 4 пробела, который настраивается с помощью метода indent() :

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

12.2. Статический импорт

Если нам нужно добавить статический импорт, мы можем определить тип и конкретное имя метода в JavaFile , вызвав метод addStaticImport() :

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

Что генерирует следующие статические операторы импорта:

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

12.3. Выход

Метод writeTo() предоставляет функциональные возможности для записи кода в несколько целей, таких как стандартный поток вывода ( System.out ) и File .

Чтобы записать код Java в стандартный поток вывода, мы просто вызываем метод writeTo() и предоставляем System.out в качестве аргумента:

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

javaFile.writeTo(System.out);

Метод writeTo() также принимает java.nio.file.Path и java.io.File . Мы можем предоставить соответствующий объект Path или File , чтобы сгенерировать файл исходного кода Java в папку/путь назначения:

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

Для получения более подробной информации о JavaFile обратитесь к Javadoc .

13. Заключение

Эта статья была введением в функциональные возможности JavaPoet, такие как создание методов, полей, параметров, типов, аннотаций и Javadocs.

JavaPoet предназначен только для генерации кода. В случае, если мы хотели бы выполнить метапрограммирование с помощью Java, JavaPoet версии 1.10.0 не поддерживает компиляцию и выполнение кода.

Как всегда, примеры и фрагменты кода доступны на GitHub .