Статьи

Генерация Java-кода с помощью JAnnocessor

В этой статье я покажу вам, как сгенерировать код с помощью фреймворка JAnnocessor, созданного Николче Михайловским . Впервые я познакомился с JAnnocessor на конференции GeeCON 2012 во время выступления Николча: «Инновационная и прагматичная генерация исходного кода Java» (слайды) . Впоследствии я успешно использовал его в одном из моих проектов. Об этой среде почти нет ресурсов, поэтому я надеюсь, что моя статья будет полезна для тех, кто заинтересован в ее использовании или просто ищет новую игрушку для своего проекта.

Фон

Каждый Java-разработчик ежедневно использует какой-то инструмент генерации кода . Сеттеры, геттеры, тривиальные конструкторы, toString — все это просто шаблонный код. Обычно мы генерируем его с помощью нашей любимой IDE. Я не могу себе представить, что это можно сделать вручную, и поскольку Java является статическим языком, мы никогда не сможем пропустить этот процесс.

Эти тривиальные примеры генерации кода, предоставляемые всеми современными IDE, — не единственные ситуации, когда генерация кода полезна. Существует множество современных фреймворков, которые генерируют некоторый код, чтобы помочь нам писать более надежный код и делать это быстрее. Я думаю, что наиболее известными примерами являются QueryDSL и JPA2 Metamodel Generator, который создает объекты, используемые для выполнения безопасных типов запросов к базе данных.

Есть и другие ситуации — не очень хорошо поддерживаемые IDE — где мы могли бы использовать генерацию кода. Обычно это может быть полезно при генерации:

  • строитель
  • фасад
  • DTO и преобразователи из доменных объектов в DTO

Это только примеры. Для некоторых проектов может быть что-то специфическое для проекта, где мы не можем использовать какой-либо существующий инструмент генерации кода, и мы должны написать свой собственный. Как это сделать? С Java APT — инструмент для обработки аннотаций .

Несколько слов о Java APT

Инструмент обработки аннотаций был представлен в Java 1.5 и предоставляет низкоуровневый API для обработки аннотированных классов. Это основа для большинства (может быть, всех?) Существующих сред генерации кода, и из-за очень низкого уровня API я не рекомендую использовать простой Java APT, если вы просто хотите сгенерировать некоторые классы. Как страшно генерация кода с APT, посмотрите на пример на странице apt-jelly (еще один инструмент генерации кода).

Вместо использования простого Java APT — вы можете использовать одну из существующих платформ. Недавно я присутствовал на конференции и рассказал об интересном JAnnocessor, который, кажется, отлично справляется со своей задачей.

Здравствуйте, JAnnocessor

JAnnocessor — это совершенно новый фреймворк, созданный Николчем Михайловским. Чем он отличается от APT? Вот как автор говорит то, что говорит автор:

JAnnocessor построен на основе Java APT, инкапсулируя модель исходного кода Java в богатую и удобную модель домена высокого уровня, которая служит хорошей целью для выразительного сопоставления и преобразования.

JAnnocessor поставляется с несколькими встроенными процессорами: builder , dto , фасад и картограф . Если вам нужен пользовательский функционал, вы можете легко написать его самостоятельно.

Есть один большой недостаток — очень и очень плохая документация. На самом деле документации почти нет вообще. В вики вы не найдете слишком много, а что еще хуже — вы не найдете Javadocs и в каркасных классах. Хотя есть Руководство по началу работы, написанное автором, я упускаю некоторые моменты, поэтому я пошагово проведу вас по основам.  

Настройка Maven

Мы будем использовать JAnnocessor только на этапе компиляции, поэтому нет необходимости добавлять его в наш пакет приложения — мы устанавливаем область действия

1
2
3
4
5
6
<dependency>
    <groupId>com.googlecode.jannocessor</groupId>
    <artifactId>jannocessor</artifactId>
    <version>0.7.2</version>
    <scope>provided</scope>
</dependency>

Следующая часть — плагин для обработки аннотаций. Хотя руководство по началу работы рекомендует использовать плагин jannocessor-maven-plugin, я был вынужден сделать это «по старинке» с плагином maven-processor-plugin, потому что я пропустил некоторые параметры конфигурации.

В раздел сборки / плагины добавляем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<plugin>
    <groupId>org.bsc.maven</groupId>
    <artifactId>maven-processor-plugin</artifactId>
    <version>2.0.4</version>
    <executions>
        <execution>
            <id>generate-code</id>
            <goals>
                <goal>process</goal>
            </goals>
            <phase>compile</phase>
 
            <configuration>
                <processors>
                    <processor>org.jannocessor.processor.JannocessorProcessor</processor>
                </processors>
 
                <systemProperties>
                    <logback.configurationFile>${project.basedir}/etc/jannocessor-logback.xml</logback.configurationFile>
                </systemProperties>
                <options>
                    <templates.path>${project.basedir}/src/main/resources</templates.path>
                </options>
                <defaultOutputDirectory>${project.basedir}/target/generated-sources/</defaultOutputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>
  • процессоры — сообщает плагину-процессору, какой класс выполняет обработку аннотаций — не меняйте его
  • configuration / logback.configurationFile — это необязательно. JAnnocessor использует внутренний logback для ведения журнала, и если вы сделаете это также в своем проекте и у вас есть logback.xml в classpath, он будет использоваться JAnnocessor. Я рекомендую написать отдельную конфигурацию ведения журнала для JAnnocessor, чтобы избежать возможных проблем (например, если вы используете условные выражения janino ). jannocessor-logback.xml может быть простым:
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
     
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%-30(%date) %-5level %logger{0} %msg%n</pattern>
            </encoder>
        </appender>
     
        <root level="error">
            <appender-ref ref="console"/>
        </root>
     
    </configuration>
  • options / templates.path — возможно, вы будете использовать пользовательские шаблоны JAnnocessor. Эта строка может быть удалена, если вы не будете
  • defaultOutputDirectory — это важно — по умолчанию сгенерированные классы добавляются в src / main / java — что, на мой взгляд, очень плохая идея. Помните, что все сгенерированные классы воссоздаются при каждой сборке Maven, и все сделанные вручную изменения теряются . Вот почему сгенерированные классы должны быть помещены в /target/generated-sources/ или /target/generated-test-sources/

Строитель поколения

В этом примере я буду использовать встроенный генератор для генерации класса построителя для моего простого POJO.

Прежде всего нам нужна новая аннотация для пометки классов, для которых мы хотим сгенерировать компоновщик. Это может быть так просто, как:

1
2
3
4
package pl.maciejwalkowiak.jannocessor.domain;
 
public @interface GenerateBuilder {
}

Наш образец POJO:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package pl.maciejwalkowiak.jannocessor.domain;
 
@GenerateBuilder
public class Person {
    private String firstName;
    private String lastName;
    private Integer age;
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
}

Следующая важная часть — сообщить JAnnocessor, что должно быть сгенерировано. Для этого нам нужно создать определенный класс в определенном пакете: org.jannocessor.config.Processors . Я не мог найти способ сделать его настраиваемым, чтобы конфигурация могла быть в пакете нашего проекта.

В конфигурации мы указываем, где должны быть размещены созданные классы: pl.maciejwalkowiak.jannocessor.domain.builder и где находятся классы базовых компонентов: pl.maciejwalkowiak.jannocessor.domain . Последний параметр означает, что если мы хотим использовать режим отладки — сейчас это не важно.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package org.jannocessor.config;
 
import pl.maciejwalkowiak.jannocessor.domain.GenerateBuilder;
 
import org.jannocessor.extra.processor.BuilderGenerator;
import org.jannocessor.model.structure.JavaClass;
import org.jannocessor.processor.annotation.Annotated;
import org.jannocessor.processor.annotation.Types;
 
public class Processors {
 
    @Annotated(GenerateBuilder.class)
    @Types(JavaClass.class)
    public BuilderGenerator generateBuilder() {
        return new BuilderGenerator("pl.maciejwalkowiak.jannocessor.domain.builder", "pl.maciejwalkowiak.jannocessor.domain", false);
    }
}

Давайте запустим это

Чтобы запустить его, нам нужно просто выполнить mvn compile . В target/generated-sources/pl/maciejwalkowiak/jannocessor/domain/builder/ мы найдем класс PersonBuilder :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package pl.maciejwalkowiak.jannocessor.domain.builder;
 
import pl.maciejwalkowiak.jannocessor.domain.Person;
import javax.annotation.Generated;
 
/**
 * Generated by JAnnocessor
 */
@Generated("Easily with JAnnocessor")
public class PersonBuilder {
 
    private String firstName;
 
    private String lastName;
 
    private Integer age;
 
    public PersonBuilder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }
 
    public PersonBuilder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }
 
    public PersonBuilder age(Integer age) {
        this.age = age;
        return this;
    }
 
    public Person build() {
        Person instance = new Person();
 
        instance.setFirstName(firstName);
        instance.setLastName(lastName);
        instance.setAge(age);
 
        return instance;
    }
 
}

Благодаря классу builder у нас есть свободный интерфейс для создания объектов Person :

1
Person person = new PersonBuilder().firstName("John").lastName("Doe").age(25).build();

Создание нашего собственного генератора

Для создания пользовательских генераторов JAnnocessor предоставляет богатый API и пару примеров. К сожалению, нет руководства или учебника доступны. Для этой статьи я хотел быстро написать генератор для FEST Assert 2.x, но через некоторое время, копаясь в исходном коде JAnnocessor, я сдался. Вместо этого я просто покажу концепцию.

Для того чтобы кодировать собственный генератор, вам просто нужно создать класс, который наследуется от org.jannocessor.extra.processor.AbstractGenerator .

01
02
03
04
05
06
07
08
09
10
11
public class MyCustomGenerator extends AbstractGenerator<AbstractJavaClass> {
 
    public MyCustomGenerator(String destPackage, boolean inDebugMode) {
        super(destPackage, inDebugMode);
    }
 
    @Override
    protected void generateCodeFrom(PowerList<AbstractJavaClass> models, ProcessingContext context) {
        // ....
    }
}

PowerList<AbstractJavaClass> models представляют коллекцию всех классов, аннотированных пользовательской аннотацией. Тогда у вас есть доступ ко всем полям классов, методам, реализованным интерфейсам и т. Д. Единственное, что я пропустил, это богатый доступ к суперклассам классов. Чтобы получить поля суперкласса, мне пришлось использовать Java Reflection API.

Если вы хотите написать собственный генератор, я рекомендую вам взглянуть на примеры, такие как BuilderGenerator . Это немного, но это определенно будет полезно.  

Резюме

В этом посте я показал, как настроить и использовать JAnnocessor. Хотя я думаю, что это хорошая и, возможно, очень полезная библиотека, отсутствие документации не позволяет использовать ее в действительно серьезном проекте. Я надеюсь, что Николче напишет хорошие документы или создаст сообщество вокруг проекта, который сделает это. Я также надеюсь, что проект перейдет на github. Это как-то стало стандартом, поэтому, если автор хочет создать вокруг него сообщество — я думаю, что это единственный правильный шаг. Тем не менее я спросил его об этом, и, по крайней мере, на данный момент у него нет никаких планов сделать это.

Ссылка: генерация Java-кода с помощью JAnnocessor от нашего партнера по JCG Мачея Уоковяка в блоге « Разработка программного обеспечения» .