Comment générer des classes de données en Java

Kotlin a une syntaxe concise pour déclarer des classes de données:

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

La syntaxe Java équivalente est verbeuse. Vous devez créer une classe Java avec des champs privés. Et getter et setter méthodes pour les champs. Et des méthodes supplémentaires comme equals(), hashCode()et toString().

Mais qui dit que vous devez créer le code Java à la main?

Dans cet article, je vais vous montrer comment générer des fichiers source Java à partir d'un fichier YAML.

Voici l'exemple de fichier YAML:

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

L'exemple de sortie du générateur de code est constitué de deux fichiers source Java User.javaet 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 est similaire.

Le but de cet article est le suivant: vous apprendrez à programmer un générateur de code à partir de zéro. Et il est facile de l'adapter à vos besoins.

La méthode principale

La main()méthode fait deux choses:

  • Étape 1: Lisez le fichier YAML, dans les spécifications de classe
  • Étape 2: générer des fichiers source Java à partir des spécifications de classe

Il dissocie la lecture et la génération. Vous pouvez modifier le format d'entrée à l'avenir ou prendre en charge plus de formats d'entrée.

Voici la main()méthode:

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

Étape 1: Lisez le fichier YAML dans les spécifications de classe

Laissez-moi vous expliquer ce qui se passe dans cette ligne:

List classSpecifications = yamlReader.read(yamlFile);

Une spécification de classe est une définition d'une classe à générer et de ses champs.

Rappelez-vous le Userdans l'exemple de fichier YAML?

User: name: Name age: Integer

Lorsque le lecteur YAML lit cela, il crée un ClassSpecificationobjet, avec le nom User. Et cette spécification de classe référencera deux FieldSpecificationobjets, appelés nameet age.

Le code de la ClassSpecificationclasse et de la FieldSpecificationclasse est simple.

Le contenu de ClassSpecification.javaest indiqué ci-dessous:

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

Le contenu de FieldSpecification.javaest:

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

La seule question restante pour l'étape 1 est: comment passer d'un fichier YAML aux objets de ces classes?

Le lecteur YAML utilise la bibliothèque SnakeYAML pour analyser les fichiers YAML.

SnakeYAML rend le contenu d'un fichier YAML disponible dans des structures de données telles que des cartes et des listes.

Pour cet article, il vous suffit de comprendre les cartes, car c'est ce que nous utilisons dans les fichiers YAML.

Regardez à nouveau l'exemple:

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

Ce que vous voyez ici, ce sont deux cartes imbriquées.

La clé de la carte extérieure est le nom de la classe (comme User).

Lorsque vous obtenez la valeur de la Userclé, vous obtenez une carte des champs de classe:

name: Name age: Integer

La clé de cette carte interne est le nom du champ et la valeur est le type de champ.

C'est une carte de chaînes en une carte de chaînes en chaînes. C'est important de comprendre le code du lecteur YAML.

Voici la méthode qui lit le contenu complet du fichier 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; }

Avec l' yamlClassSpecificationsentrée as, le lecteur YAML crée les ClassSpecificationobjets:

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

La createClassNameToFieldSpecificationsMap()méthode crée

  • les spécifications de terrain pour chaque classe, et sur la base de ces
  • une carte de chaque nom de classe avec ses spécifications de champ.

Ensuite, le lecteur YAML crée un ClassSpecificationobjet pour chaque entrée de cette carte.

Le contenu du fichier YAML est désormais disponible pour l'étape 2 de manière indépendante de YAML. Nous avons terminé avec l'étape 1.

Étape 2: générer des fichiers source Java à partir des spécifications de classe

Apache FreeMarker est un moteur de modèle Java qui produit une sortie textuelle. Les modèles sont écrits dans le FreeMarker Template Language (FTL). Il permet au texte statique de se mélanger au contenu des objets Java.

Voici le modèle pour générer les fichiers source 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}; }  }

Regardons la première ligne:

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