Generating code with JavaPoet

Why write code when you can generate it? There are lots of situations when it makes more sense to generate. In this article I’m going to work through an example of how to use JavaPoet and Apache BeanUtils to write a class that will generate domain to DTO conversion code.

In our app, due to the gradual removal of our old way of doing things, we have a lot of code that does the following:

domain object -> legacy DTO object -> new DTO object

The legacy DTO objects are no longer needed, so now we would like to delete them. Really we want the code to convert from the domain object directly to the new DTO object. When doing this sort of conversion, you always face a choice – you just code a generic converter class, which understands all of the data conversions that you need to perform, and uses runtime reflection, simply iterating over all of the properties, and converting each one. However, one major problem with this is that it is very fragile – you cannot search for usages of getters or setters in your IDE, and if someone changes or removes a property, you will end up with a runtime failure, not a build or test failure. For this reason, we want to use plain old java code to do the conversion. However, we don’t want to write it by hand, so it makes sense to use JavaPoet to generate it. JavaPoet is a very easy way to do code generation. Let me show how I used it in this scenario.

Firstly, download JavaPoet or add to your Maven dependencies:

https://github.com/square/javapoet

In my case, both the domain and DTO classes are java beans (i.e. they have properties, and each property has a getter and setter) so rather than just using reflection, I can use the Apache BeanUtils classes to make it easier to read these properties, so my Maven setup includes both JavaPoet and Apache BeanUtils:

<dependency>
  <groupId>commons-beanutils</groupId>
  <artifactId>commons-beanutils</artifactId>
  <version>1.8.3</version>
</dependency>
<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.9.0</version>
</dependency>

Now let’s start solving the problem at hand. Firstly, we need to map between the properties in the DTO and the domain class, and also keep a record of any properties that exist in the DTO, but cannot be found in the domain class, so we can put warnings in the generated code to say that the properties need to be manually checked. To begin with, I’ll create a mini helper class to return a map of the properties, and any missing ones:

class PropertyInfo {
    Map<PropertyDescriptor, PropertyDescriptor> propertyDescriptorMap;
    List<String> missingProperties;
 
    public PropertyInfo(Map<PropertyDescriptor, PropertyDescriptor> propertyDescriptorMap, List<String> missingProperties) {
        this.propertyDescriptorMap = propertyDescriptorMap;
        this.missingProperties = missingProperties;
    }
}

Now we can write a method using Apache BeanUtils that iterates over the properties and matches them on their names:

PropertyInfo getPropertyMapping(Class source, Class target) {
    // iterate over each property / field to generate a list of properties we can deal with, and ones we cannot
    Map<PropertyDescriptor, PropertyDescriptor> propertyDescriptorMap = new HashMap<>();
    // store properties needing to be populated in target, not found in source
    List<String> missingProperties = new ArrayList<>();
 
    Map<String, PropertyDescriptor> sourcePropertiesByName
            = Arrays.stream(PropertyUtils.getPropertyDescriptors(source))
            .collect(toMap(PropertyDescriptor::getName, Function.<PropertyDescriptor>identity()));
    System.out.println("Source class has: " + sourcePropertiesByName.size() + " properties");
 
    PropertyDescriptor[] targetProperties = PropertyUtils.getPropertyDescriptors(target);
    System.out.println("Target class has: " + targetProperties.length + " properties");
 
    // only do declared properties for now i.e. don't go up to superclasses.
    // navigating up to superclasses would create problems as it would go all the way up to java.lang.Object
    Set<String> declaredTargetFields = new HashSet<>();
    for (Field declaredField : target.getDeclaredFields()) {
        declaredTargetFields.add(declaredField.getName());
    }
    System.out.println("Target has: " + declaredTargetFields.size() + " fields declared in class itself");
 
    for (PropertyDescriptor targetPropertyDescriptor : targetProperties) {
        String targetPropertyName = targetPropertyDescriptor.getName();
        System.out.println("Processing property: " + targetPropertyName);
 
        if (declaredTargetFields.contains(targetPropertyName)) {
            PropertyDescriptor sourcePropertyDescriptor = sourcePropertiesByName.get(targetPropertyName);
            if (sourcePropertyDescriptor != null) {
                System.out.println("Found mapping for " + targetPropertyName);
                propertyDescriptorMap.put(sourcePropertyDescriptor, targetPropertyDescriptor);
            } else {
                System.out.println("WARNING - cannot find property " + targetPropertyName + " in source");
                missingProperties.add(targetPropertyName);
            }
        } else {
            System.out.println("Skipping property: " + targetPropertyName + " as declared in superclass");
        }
    }
    return new PropertyInfo(propertyDescriptorMap, missingProperties);
}

Great, now we have enough info to generate our converter. Our conversion method will accept a domain object, and return a DTO, so the method signature will look like this:

public DTOClassName toDTO(DomainClassName domainClassParameter)

How do we do this in JavaPoet? Well, firstly, let’s work out the parameter name. For some domain class names, we just need to take the class name and convert the first letter to lowercase. For some of the domain classes I am using, the class name ends in “Impl”, which I’d like to remove. So my logic to work out the parameter name is this:

String domainClassName = domainClass.getSimpleName();
String domainClassParameterName = domainClassName.substring(0, 1).toLowerCase() + domainClassName.substring(1);
if (domainClassParameterName.endsWith("Impl")) {
     domainClassParameterName = domainClassParameterName.substring(0, domainClassParameterName.length() - 4);
}

Now we can use JavaPoet to generate the method signature, using the MethodSpec.Builder class:

MethodSpec.Builder toDTOMethodBuilder = MethodSpec.methodBuilder("toDTO")
    .addModifiers(Modifier.PUBLIC)
    .addParameter(domainClass, domainClassParameterName)
    .returns(dtoClass);

Next, we need to create a new instance of our DTO object, like this:

DTOClass dto = new DTOClass();

In JavaPoet, you use $T to indicate a type, then supply that type, like this:

toDTOMethodBuilder.addStatement("$T dto = new $T()", dtoClass, dtoClass);

Note that we have to supply the class twice here, as we have used the $T type marker twice in our statement. Why bother using this $T marker? What is wrong with just manually inserting the class name? Well, by using $T, JavaPoet understands that we are giving it a reference to a class, and it can then take care of the import for you! No need to manually keep track of what classes you need to import in your generated code, and whether you have already added an import, JavaPoet will do all that for you!

Now we can simply iterate over the sets of matched properties, and write the conversion code. The easiest case is of course where the property type is the same in both source and target. If every property type was the same, the code would be:

for (PropertyDescriptor domainClassProperty : domainToDTOPropertyMap.keySet()) {
 
   String domainClassPropertyName = domainClassProperty.getName();
   System.out.println("Processing property: " + domainClassPropertyName);
 
   PropertyDescriptor dtoPropertyDescriptor = domainToDTOPropertyMap.get(domainClassProperty);
   Method domainClassReadMethod = domainClassProperty.getReadMethod();
   String dtoWriteMethodName = dtoPropertyDescriptor.getWriteMethod().getName();
   final String getProperty = domainClassParameterName + "." + domainClassReadMethod.getName() + "()";
 
   toDTOMethodBuilder.addStatement("dto." + dtoWriteMethodName + "(" + getProperty + ")");
}

In the more general case, you need to map between different types for the properties, so you will end up with a series of if statements checking the types:

   if (Some.class.equals(domainClassProperty.getPropertyType()) && 
           Other.class.equals(dtoPropertyDescriptor.getPropertyType()) {
        // write code to convert from Some.class to Other.class
   } 
   // if you have properties that might a subclass, or implementation of an interface, use "isAssignableFrom"
   else if (Some2.class.isAssignableFrom(domainClassProperty.getPropertyType()) &&
           Other2.class.isAssignableFrom(dtoPropertyDescriptor.getPropertyType()) {
       // write code to convert from Some2 class (or subclass) to Other2.class (or subclass)
   } 
   else {
       toDTOMethodBuilder.addStatement("dto." + dtoWriteMethodName + "(" + getProperty + ")");
   }
}

Having done all the properties, we should report on any properties that couldn’t be mapped, so a developer can check these manually:

for (String property : missingProperties) {
    // in early versions of JavaPoet, use addStatement. In later versions, use addComment
    toDTOMethodBuilder.addStatement("// TODO deal with property: " + property);
}

Now we simply add the return statement, and call build() on the builder to generate the method spec:

toDTOMethodBuilder.addStatement("return dto");
return toDTOMethodBuilder.build();

To put the conversion method into a Java class and write it out, we do the following:

TypeSpec converterClass = TypeSpec.classBuilder(converterClassName)
    .addModifiers(Modifier.PUBLIC)
    .addMethod(toDTOMethod)
    .build();
 
JavaFile javaFile = JavaFile.builder(converterPackage, converterClass).indent("    ").build();
javaFile.writeTo(new File("/path/to/chosen/directory));

All done! Now a developer can simply run this code to generate the conversion code, whenever they need to convert from a domain object to DTO. If desired, you can write similar code to generate an accompanying unit test.

For more examples of JavaPoet syntax, check out the readme on the github project:

https://github.com/square/javapoet

This entry was posted in Java, Uncategorized and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

502,426 Spambots Blocked by Simple Comments

HTML tags are not allowed.