Генерация кода
В нескольких проектах на разных местах работы я использовал генерацию кода. Зачем?
Чаще всего этим достигается сохранение правильности кода при изменениях. Например при добавлении еще одного типа данных в модель предметной области надо добавить класс на Яве, класс на С ++, код преобразования между ними и значение в Enum. Без генерации кода и работы ручками много, и всегда есть шанс что то из этого забыть.
Иногда reflection может быть заменой генерации кода, но даже тогда такой код будет менее читабельный чем шаблон для генерации.
Исходная информация для генерации кода может выглядеть очень по разному — это может быть Ява класс, у которого через reflection считываем свойства и генерим класс на C++, или csv файл, или XML, или еще что нибудь.
Попробую рассказать как я генерирую код в maven проектах с помощью фреймворка jamon.
Постановка задачи
Для примера возьмем задачу из сервиса для карт, который я пишу уже давно, но еще не написал. Есть точка на карте в Иерусалиме и для неё список свойств которые эта точка может иметь. Например относится она к времени второго храма или к Византии, христианская или мусульманская святыня и т.д.
Исходный файл выглядит так:
name,group
christian, religion
jewish, religion
muslim, religion
ancient, time
firsttemple, time
secondtemple, time
hasmonean, time
hordus, time
roma, time
byzantee, time
arab, time
ottoman, time
persian, time
crusaider, time
pray, type
museum, type
restaurant, type
other, type
На основе этого я хочу создать файл такого типа:
package org.citymap;
import java.io.Serializable;
public class PointAttributes implements Serializable {
private boolean muslim;
public boolean isMuslim() {
return muslim;
}
public void setMuslim(boolean value) {
muslim = value;
}
....
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PointAttributes that = (PointAttributes) o;
if (muslim != that.muslim) return false;
if (christian != that.christian) return false;
...
return true;
}
public int hashCode() {
int result = 0;
result = 31 * result + (muslim ? 1 : 0);
result = 31 * result + (christian ? 1 : 0);
....
return result;
}
}
Maven
Проект создаём в maven. Делаем его мульти модульным, иначе красиво генерировать код не получается. В модуле папе перечислены подмодули:
<modules>
<module>util</module>
<module>codegen</module>
<module>common</module>
<module>persistency</module>
<module>web</module>
</modules>
и сам он указан типа «pom»
В модуле codegen будет код генерации, а генерировать будем в модуле common.
Собственно генерировать будет класс org.citymap.CommonTemplatesRunner
Поэтому в модуль common домабвляем следущее:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/gencode</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<phase> generate-sources </phase>
<configuration>
<target>
<java classname="org.citymap.CommonTemplatesRunner">
<classpath refid="maven.dependency.classpath" />
<arg value="${project.build.directory}"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Как видите, мы добавили директорию с соурсами в target. Это нужно для того, чтобы при mvn clean сгенерированный код стирался (как и все продукты компиляции). А помощью anta мы запускаем генератор кода на этапе generate-sources. Как аргумент передаём генератору директории где генерить код (текущая директория зависит от того запускали мавен в этом проекте или в папе)
Jamon
Для генерации я использую фреймворк jamon, потому что у него прекрасный плагин для maven.
Добавляю его в проект codegen так:
<dependencies>
<dependency>
<groupId>org.jamon</groupId>
<artifactId>jamon-runtime</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/gencode</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jamon</groupId>
<artifactId>jamon-maven-plugin</artifactId>
<version>2.3.2</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>translate</goal>
</goals>
<configuration>
<templateSourceDir>src/main/jamon</templateSourceDir>
<templateOutputDir>target/gencode</templateOutputDir>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Он сам генерирует код — на основе шаблонов в src/main/jamon он сгенерирует файлы умеющие генерировать по шаблонам в target/gencode. Надеюсь никого не запутал с генерацией генераторов :)
Мой шаблон выглядит так:
<%import>
java.util.*;
org.citymap.*;
org.apache.commons.lang.StringUtils;
</%import>
<%args>
Map<String, PointProperty> pojos;
</%args>
package org.citymap;
import java.io.Serializable;
public class PointAttributes implements Serializable {
<%for String name:pojos.keySet() %>
private boolean <% name%>;
public boolean is<% StringUtils.capitalize(name)%>() {
return <% name%>;
}
public boolean get<% StringUtils.capitalize(name)%>() {
return <% name%>;
}
public void set<% StringUtils.capitalize(name)%>(boolean value) {
<% name%> = value;
}
</%for>
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PointAttributes that = (PointAttributes) o;
<%for String name:pojos.keySet() %>
if (<% name%> != that.<% name%>) return false;
</%for>
return true;
}
public int hashCode() {
int result = 0;
<%for String name:pojos.keySet() %>
result = 31 * result + (<% name%> ? 1 : 0);
</%for>
return result;
}
}
И вызываю я его следующим образом:
public class CommonTemplatesRunner {
public static void main(String[] args) throws IOException {
String targetDir = args[0];
final Reader csv = new InputStreamReader(CodeGen.class.getClassLoader().getResourceAsStream("properties.csv"));
final ObjectCSVMapper<PointProperty> csvProcessor = new ObjectCSVMapper<PointProperty>(PointProperty.class, csv);
final Map<String, PointProperty> pojos = csvProcessor.getEntities();
File dir = new File(targetDir+"/gencode/org/citymap");
dir.mkdirs();
FileWriter fw = new FileWriter(new File(dir,"PointAttributes.java"));
new PointAttributesTemplate().render(fw, pojos);
}
}
Осталось создать для него аргумент — этот самый Map<String, PointProperty>.
Это я делаю используя библиотеку supercsv, генерируя из указанного в начале properties.csv объекты типа PointProperty.
Итак, повторяем процесс:
В модуле codegen ничего не запускается, только создается генератор кода.
На основе шаблона PointAttributesTemplate.jamon создается класс PointAttributesTemplate
В модуле common запускается CommonTemplatesRunner:
1. Из properties.csv делаем объекты типа PointProperty.
2. Передаем их как аргумент в PointAttributesTemplate и он генерит PointAttributes.java
Фух. Всё.
Вопросы?