Як створити класи даних на 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}{

You can see it begins with the static text of a class declaration: public class. The interesting bit is in the middle: ${classSpecification.name}.

When Freemarker processes the template, it accesses the classSpecification object in its model. It calls the getName() method on it.

What about this part of the template?

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

At first, Freemarker calls classSpecification.getFieldSpecifications(). It then iterates over the field specifications.

One last thing. That line is a bit odd:

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

Let’s say the example field is age: Integer (in YAML). Freemarker translates this to:

public Integer getAge(){

So ?cap_first means: capitalize the first letter, as the YAML file contains age in lower case letters.

Enough about templates. How do you generate the Java source files?

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