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>
class PropertyInfo {
Map<PropertyDescriptor, PropertyDescriptor> propertyDescriptorMap;
List<String> missingProperties;
public PropertyInfo(Map<PropertyDescriptor, PropertyDescriptor> propertyDescriptorMap, List<String> missingProperties) {
this.propertyDescriptorMap = propertyDescriptorMap;
this.missingProperties = missingProperties;
}
}
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);
}
public DTOClassName toDTO(DomainClassName domainClassParameter)
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);
}
MethodSpec.Builder toDTOMethodBuilder = MethodSpec.methodBuilder("toDTO")
.addModifiers(Modifier.PUBLIC)
.addParameter(domainClass, domainClassParameterName)
.returns(dtoClass);
DTOClass dto = new DTOClass();toDTOMethodBuilder.addStatement("$T dto = new $T()", dtoClass, dtoClass);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 + ")");
}
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 + ")");
}
}
for (String property : missingProperties) {
// in early versions of JavaPoet, use addStatement. In later versions, use addComment
toDTOMethodBuilder.addStatement("// TODO deal with property: " + property);
}
toDTOMethodBuilder.addStatement("return dto");
return toDTOMethodBuilder.build();
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));
For more examples of JavaPoet syntax, check out the readme on the github project: https://github.com/square/javapoet