Как да генерирам класове данни в Java

Kotlin има кратък синтаксис за деклариране на класове данни:

data class User(val name: String, val age: Int)

Еквивалентният синтаксис на Java е многословен. Трябва да създадете Java клас с частни полета. И getter и setter методи за полетата. И допълнителни методи като equals(), hashCode()и toString().

Но кой казва, че трябва да създадете Java кода на ръка?

В тази статия ще ви покажа как да генерирате изходни Java файлове от YAML файл.

Ето примерния YAML файл:

User: name: Name age: Integer Name: firstName: String lastName: String

Примерният изход на генератора на кода е два изходни файла на Java User.javaи Name.java.

public class User{ private Name name; private Integer age; public User(){ } public Name getName(){ return name; } public void setName(Name name){ this.name = name; } public Integer getAge(){ return age; } public void setAge(Integer age){ this.age = age; } }

Name.java е подобно.

Смисълът на тази статия е: Ще научите как да програмирате генератор на код от нулата. И е лесно да го адаптирате към вашите нужди.

Основният метод

В main()метода прави две неща:

  • Стъпка 1: Прочетете в YAML файла в спецификации на класа
  • Стъпка 2: Генерирайте Java изходни файлове от спецификациите на класа

Той отделя четенето и генерирането. Можете да промените входния формат в бъдеще или да поддържате повече входни формати.

Ето main()метода:

public static void main(String[] args) throws Exception { // Make sure there is exactly one command line argument, // the path to the YAML file if (args.length != 1) { System.out.println("Please supply exactly one argument, the path to the YAML file."); return; } // Get the YAML file's handle & the directory it's contained in // (generated files will be placed there) final String yamlFilePath = args[0]; final File yamlFile = new File(yamlFilePath); final File outputDirectory = yamlFile.getParentFile(); // Step 1: Read in the YAML file, into class specifications YamlClassSpecificationReader yamlReader = new YamlClassSpecificationReader(); List classSpecifications = yamlReader.read(yamlFile); // Step 2: Generate Java source files from class specifications JavaDataClassGenerator javaDataClassGenerator = new JavaDataClassGenerator(); javaDataClassGenerator.generateJavaSourceFiles(classSpecifications, outputDirectory); System.out.println("Successfully generated files to: " + outputDirectory.getAbsolutePath()); }

Стъпка 1: Прочетете YAML файла в спецификациите на класа

Позволете ми да обясня какво се случва в този ред:

List classSpecifications = yamlReader.read(yamlFile);

Спецификация на клас е дефиниция на клас, който трябва да бъде генериран, и неговите полета.

Помните ли Userв примерния YAML файл?

User: name: Name age: Integer

Когато четецът на YAML чете това, той ще създаде един ClassSpecificationобект с името User. И тази спецификация на класа ще препраща към два FieldSpecificationобекта, наречени nameи age.

Кодът за ClassSpecificationкласа и FieldSpecificationкласа е прост.

Съдържанието на ClassSpecification.javaе показано по-долу:

public class ClassSpecification { private String name; private List fieldSpecifications; public ClassSpecification(String className, List fieldSpecifications) { this.name = className; this.fieldSpecifications = fieldSpecifications; } public String getName() { return name; } public List getFieldSpecifications() { return Collections.unmodifiableList(fieldSpecifications); } }

Съдържанието на FieldSpecification.javaе:

public class FieldSpecification { private String name; private String type; public FieldSpecification(String fieldName, String fieldType) { this.name = fieldName; this.type = fieldType; } public String getName() { return name; } public String getType() { return type; } }

Единственият оставащ въпрос за стъпка 1 е: как да стигнете от YAML файл до обекти от тези класове?

YAML четецът използва библиотеката SnakeYAML, за да анализира YAML файлове.

SnakeYAML прави съдържанието на YAML файл достъпно в структури от данни като карти и списъци.

За тази статия трябва само да разбирате карти - защото това е, което използваме в YAML файловете.

Погледнете отново примера:

User: name: Name age: Integer Name: firstName: String lastName: String

Това, което виждате тук, са две вложени карти.

Ключът на външната карта е името на класа (подобно User).

Когато получите стойността за Userключа, получавате карта на полетата на класа:

name: Name age: Integer

Ключът на тази вътрешна карта е името на полето, а стойността е типът поле.

Това е карта на низове до карта на низове към низове. Това е важно, за да разберете кода на YAML четеца.

Ето метода, който чете в пълното съдържание на YAML файла:

private Map readYamlClassSpecifications(Reader reader) { Yaml yaml = new Yaml(); // Read in the complete YAML file to a map of strings to a map of strings to strings Map yamlClassSpecifications = (Map) yaml.load(reader); return yamlClassSpecifications; }

С yamlClassSpecificationsвхода като, YAML четецът създава ClassSpecificationобектите:

private List createClassSpecificationsFrom(Map yamlClassSpecifications) { final Map classNameToFieldSpecificationsMap = createClassNameToFieldSpecificationsMap(yamlClassSpecifications); List classSpecifications = classNameToFieldSpecificationsMap.entrySet().stream() .map(e -> new ClassSpecification(e.getKey(), e.getValue())) .collect(toList()); return classSpecifications; }

В createClassNameToFieldSpecificationsMap()метода създава

  • полевите спецификации за всеки клас и въз основа на тях
  • карта на името на всеки клас към неговите спецификации на полета.

След това YAML четецът създава ClassSpecificationобект за всеки запис в тази карта.

Съдържанието на YAML файла вече е достъпно за стъпка 2 по независим от YAML начин. Приключихме със стъпка 1.

Стъпка 2: Генерирайте Java изходни файлове от спецификациите на класа

Apache FreeMarker е механизъм за шаблони на Java, който произвежда текстови резултати. Шаблоните са написани на езика на шаблона на FreeMarker (FTL). Позволява статичен текст да се смесва със съдържанието на Java обекти.

Ето шаблона за генериране на изходните файлове на Java javadataclass.ftl:

public class ${classSpecification.name}{  private ${field.type} ${field.name};  public ${classSpecification.name}(){ }  public ${field.type} get${field.name?cap_first}(){ return ${field.name}; } public void set${field.name?cap_first}(${field.type} ${field.name}){ this.${field.name} = ${field.name}; }  }

Нека да разгледаме първия ред:

public class ${classSpecification.name}{

Можете да видите, че започва с текста на статичната на декларация на клас: public class. Най-интересното е в средата: ${classSpecification.name}.

Когато Freemarker обработва шаблона, той осъществява достъп до classSpecificationобекта в своя модел. Той извиква getName()метода върху него.

Ами тази част от шаблона?

 private ${field.type} ${field.name}; 

Отначало Freemarker се обажда classSpecification.getFieldSpecifications(). След това се итерира над спецификациите на полето.

Последно нещо. Този ред е малко странен:

public ${field.type} get${field.name?cap_first}(){

Да приемем, че примерното поле е age: Integer(в YAML). Freemarker превежда това на:

public Integer getAge(){

Това ?cap_firstозначава: главна буква на първата буква, тъй като файлът YAML съдържа ageмалки букви.

Стига за шаблоните. Как генерирате изходните файлове на Java?

First, you need to configure FreeMarker by creating a Configuration instance. This happens in the constructor of the JavaDataClassGenerator:

To generate source files, the JavaDataClassGenerator iterates over the class specifications, and generates a source file for each:

And that’s it.

Conclusion

I showed you how to build a Java source code generator based on YAML files. I picked YAML because it is easy to process, and thus easy to teach. You can replace it with another format if you like.

You can find the complete code on Github.

To make the code as understandable as possible, I took a few shortcuts:

  • no methods like equals(), hashCode() and toString()
  • no inheritance of data classes
  • generated Java classes are in the default package
  • the output directory is the same as the input directory
  • error handling hasn’t been my focus

A production-ready solution would need to deal with those issues. Also, for data classes, Project Lombok is an alternative without code generation.

So think of this article as a beginning, not an end. Imagine what is possible. A few examples:

  • scaffold JPA entity classes or Spring repositories
  • generate several classes from one specification, based on patterns in your application
  • generate code in different programming languages
  • produce documentation

I currently use this approach to translate natural language requirements

directly to code, for research purposes. What will you do?

If you want to know what I’m hacking on, visit my GitHub project.

You can contact me on Twitter or LinkedIn.

The original version of this article was posted on dev.to