Sådan genereres dataklasser i Java

Kotlin har en kortfattet syntaks til at erklære dataklasser:

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

Den tilsvarende Java-syntaks er detaljeret. Du skal oprette en Java-klasse med private felter. Og getter og setter metoder til markerne. Og yderligere metoder som equals(), hashCode()og toString().

Men hvem siger, at du skal oprette Java-koden manuelt?

I denne artikel viser jeg dig, hvordan du genererer Java-kildefiler fra en YAML-fil.

Her er eksemplet på YAML-filen:

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

Eksemplet på kodegeneratorens output er to Java-kildefiler User.javaog 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 ligner hinanden.

Pointen med denne artikel er: Du lærer at programmere en kodegenerator fra bunden. Og det er nemt at tilpasse det til dine behov.

Den vigtigste metode

Den main()metode gør to ting:

  • Trin 1: Læs i YAML-filen i klassespecifikationer
  • Trin 2: Generer Java-kildefiler ud fra klassespecifikationerne

Det afkobler læsning og generering. Du kan ændre inputformatet i fremtiden eller understøtte flere inputformater.

Her er main()metoden:

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

Trin 1: Læs YAML-filen i klassespecifikationer

Lad mig forklare, hvad der sker i denne linje:

List classSpecifications = yamlReader.read(yamlFile);

En klassespecifikation er en definition af en klasse, der skal genereres, og dens felter.

Husk den Useri eksemplet YAML-fil?

User: name: Name age: Integer

Når YAML-læseren læser det, opretter det et ClassSpecificationobjekt med navnet User. Og den klassespecifikation vil henvise til to FieldSpecificationobjekter, kaldet nameog age.

Koden til ClassSpecificationklassen og FieldSpecificationklassen er enkel.

Indholdet af ClassSpecification.javaer vist nedenfor:

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

Indholdet af FieldSpecification.javaer:

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

Det eneste tilbageværende spørgsmål til trin 1 er: hvordan kommer man fra en YAML-fil til objekter af disse klasser?

YAML-læseren bruger SnakeYAML-biblioteket til at analysere YAML-filer.

SnakeYAML gør en YAML-fils indhold tilgængelig i datastrukturer som kort og lister.

Til denne artikel behøver du kun at forstå kort - for det er det, vi bruger i YAML-filerne.

Se på eksemplet igen:

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

Hvad du ser her er to indlejrede kort.

Nøglen til det ydre kort er klassens navn (som User).

Når du får værdien for Usernøglen, får du et kort over klassefelterne:

name: Name age: Integer

Nøglen til dette indre kort er feltnavnet, og værdien er felttypen.

Det er et kort over strenge til et kort over strenge til strenge. Det er vigtigt at forstå koden til YAML-læseren.

Here’s the method that reads in the complete YAML file contents:

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

With the yamlClassSpecifications as input, the YAML reader creates the ClassSpecification objects:

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

The createClassNameToFieldSpecificationsMap() method creates

  • the field specifications for each class, and based on these
  • a map of each class name to its field specifications.

Then the YAML reader creates a ClassSpecification object for each entry in that map.

The contents of the YAML file are now available to Step 2 in a YAML independent way. We’re done with Step 1.

Step 2: Generate Java source files from the class specifications

Apache FreeMarker is a Java template engine that produces textual output. Templates are written in the FreeMarker Template Language (FTL). It allows static text to mix with the content of Java objects.

Here’s the template to generate the Java source files, 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

En produktionsklar løsning ville være nødvendigt at håndtere disse problemer. Også for dataklasser er Project Lombok et alternativ uden kodegenerering.

Så tænk på denne artikel som en begyndelse, ikke en ende. Forestil dig hvad der er muligt. Et par eksempler:

  • stillads JPA enhedsklasser eller Spring repositories
  • generere flere klasser fra en specifikation, baseret på mønstre i din applikation
  • generere kode på forskellige programmeringssprog
  • producere dokumentation

Jeg bruger i øjeblikket denne tilgang til at oversætte naturlige sprogkrav

direkte til kode til forskningsformål. Hvad vil du gøre?

Hvis du vil vide, hvad jeg hacker på, skal du besøge mit GitHub-projekt.

Du kan kontakte mig på Twitter eller LinkedIn.

Den originale version af denne artikel blev offentliggjort på dev.to