Cómo generar clases de datos en Java

Kotlin tiene una sintaxis concisa para declarar clases de datos:

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

La sintaxis equivalente de Java es detallada. Tienes que crear una clase Java con campos privados. Y getter y setter métodos para los campos. Y métodos adicionales como equals(), hashCode()y toString().

Pero, ¿quién dice que tienes que crear el código Java a mano?

En este artículo, le mostraré cómo generar archivos fuente Java a partir de un archivo YAML.

Aquí está el archivo YAML de ejemplo:

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

La salida de ejemplo del generador de código son dos archivos fuente de Java User.javay 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 es similar.

El objetivo de este artículo es: aprenderá a programar un generador de código desde cero. Y es fácil adaptarlo a tus necesidades.

El método principal

El main()método hace dos cosas:

  • Paso 1: lea el archivo YAML, en las especificaciones de la clase
  • Paso 2: generar archivos fuente Java a partir de las especificaciones de la clase

Desacopla lectura y generación. Puede cambiar el formato de entrada en el futuro o admitir más formatos de entrada.

Este es el main()método:

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

Paso 1: lea el archivo YAML en las especificaciones de la clase

Déjame explicarte lo que sucede en esta línea:

List classSpecifications = yamlReader.read(yamlFile);

Una especificación de clase es una definición de una clase que se generará y sus campos.

¿Recuerda el Userarchivo YAML de ejemplo?

User: name: Name age: Integer

Cuando el lector YAML lea eso, creará un ClassSpecificationobjeto, con el nombre User. Y esa especificación de clase hará referencia a dos FieldSpecificationobjetos, llamados namey age.

El código para la ClassSpecificationclase y la FieldSpecificationclase es simple.

El contenido de ClassSpecification.javase muestra a continuación:

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

El contenido de FieldSpecification.javaes:

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 única pregunta que queda para el Paso 1 es: ¿cómo se pasa de un archivo YAML a objetos de estas clases?

El lector YAML usa la biblioteca SnakeYAML para analizar archivos YAML.

SnakeYAML hace que el contenido de un archivo YAML esté disponible en estructuras de datos como mapas y listas.

Para este artículo, solo necesita comprender los mapas, porque eso es lo que usamos en los archivos YAML.

Mira el ejemplo de nuevo:

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

Lo que ves aquí son dos mapas anidados.

La clave del mapa exterior es el nombre de la clase (como User).

Cuando obtiene el valor de la Userclave, obtiene un mapa de los campos de clase:

name: Name age: Integer

La clave de este mapa interno es el nombre del campo y el valor es el tipo de campo.

Es un mapa de cadenas a un mapa de cadenas a cadenas. Eso es importante para comprender el código del lector YAML.

Este es el método que se lee en el contenido completo del archivo 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; }
      
     
    

Con yamlClassSpecificationscomo entrada, el lector YAML crea los ClassSpecificationobjetos:

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

El createClassNameToFieldSpecificationsMap()método crea

  • las especificaciones de campo para cada clase, y en base a estas
  • un mapa de cada nombre de clase a sus especificaciones de campo.

Luego, el lector YAML crea un ClassSpecificationobjeto para cada entrada en ese mapa.

El contenido del archivo YAML ahora está disponible para el Paso 2 de forma independiente de YAML. Terminamos con el Paso 1.

Paso 2: generar archivos fuente Java a partir de las especificaciones de la clase

Apache FreeMarker es un motor de plantillas Java que produce resultados textuales. Las plantillas están escritas en FreeMarker Template Language (FTL). Permite que el texto estático se mezcle con el contenido de los objetos Java.

Aquí está la plantilla para generar los archivos fuente de 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}; }  }

Let’s look at the first line:

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