In one of my project, we had a requirement where in different users would extend the class for adding say fields. Along with this, as we were following hexagonal architecture, we had to create mappers to map our main domain model with DTO's and DAO's. This then became tedious as multiple class's had to implemented and people would not have an idea about all this. That is where one of our colleague came up with the idea of creating a custom annotation and making all the overhead processing automated. People just have to extend class and provide their new field and the mapper creation and other overhead will be taken care by the annotation. In this blog, I'll be explaining of how a custom annotation can be created to create a MapStruct class on the go.
Introduction
Mapping between objects in Java is a common task, especially when converting data between different layers in an application. The MapStruct framework is a powerful tool that helps generate type-safe mappers at compile-time. However, as projects scale, creating and maintaining mappers for multiple classes can become repetitive and error-prone.
Wouldn’t it be great to automate this process further by creating a custom annotation that automatically generates MapStruct mappers? In this blog, I’ll walk you through building a custom annotation and a corresponding annotation processor that dynamically generates MapStruct classes for you.
Table of Contents
- What is MapStruct?
- Understanding Custom Annotations in Java
- Creating a Custom Annotation for MapStruct
- Implementing the Annotation Processor
- Using the Custom Annotation in Code
- Generated Output
- Conclusion
What is MapStruct?
MapStruct is a Java annotation-based code generator that simplifies mapping between different object models. It generates implementation code for mappers based on annotations defined in interfaces, ensuring fast and type-safe mapping.
Note: To know more on MapStruct checkout this post.
Here’s an example of a typical MapStruct mapper:
java@Mapper public interface UserMapper { UserDTO toUserDTO(User user); User toUser(UserDTO userDTO); }
While this is simple and concise, creating multiple mappers manually can get tedious. Let's automate this with a custom annotation!
Understanding Custom Annotations in Java
Custom annotations in Java are a powerful way to add metadata to your code. Combined with annotation processors, you can use this metadata to generate code or perform other compile-time tasks.
To create our custom MapStruct annotation, we'll define a new annotation that specifies the source and target classes, as well as an optional mapper name.
Creating a Custom Annotation for MapStruct
First, let’s define the custom annotation:
javapackage com.example.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface AutoMapper { Class<?> source(); Class<?> target(); String mapperName() default "AutoGeneratedMapper"; }
This AutoMapper annotation has:
source: The source class for mapping.target: The target class for mapping.mapperName: An optional name for the generated mapper interface.
Implementing the Annotation Processor
Now, let’s create an annotation processor that generates the MapStruct mapper interface. We’ll use JavaPoet to generate the code programmatically.
javapackage com.example.processor; import com.example.annotations.AutoMapper; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import org.mapstruct.Mapper; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import java.io.IOException; import java.util.Set; @SupportedAnnotationTypes("com.example.annotations.AutoMapper") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class AutoMapperProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(AutoMapper.class)) { AutoMapper autoMapper = element.getAnnotation(AutoMapper.class); generateMapper(autoMapper, element); } return true; } private void generateMapper(AutoMapper autoMapper, Element element) { String sourceClassName = autoMapper.source().getSimpleName(); String targetClassName = autoMapper.target().getSimpleName(); String mapperName = autoMapper.mapperName(); TypeSpec mapperInterface = TypeSpec.interfaceBuilder(mapperName) .addModifiers(Modifier.PUBLIC) .addAnnotation(Mapper.class) .addMethod(MethodSpec.methodBuilder("to" + targetClassName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .returns(TypeName.get(autoMapper.target())) .addParameter(TypeName.get(autoMapper.source()), "source") .build()) .addMethod(MethodSpec.methodBuilder("to" + sourceClassName) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .returns(TypeName.get(autoMapper.source())) .addParameter(TypeName.get(autoMapper.target()), "target") .build()) .build(); JavaFile javaFile = JavaFile.builder("com.example.generated", mapperInterface) .build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } }
Key Points:
- The processor reads the
source,target, andmapperNameparameters from theAutoMapperannotation. - It then uses JavaPoet to generate a MapStruct interface with mapping methods for the specified classes.
Add the following dependencies in your pom.xml:
xml<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.3.Final</version> </dependency> <dependency> <groupId>com.squareup</groupId> <artifactId>javapoet</artifactId> <version>1.13.0</version> </dependency>
Using the Custom Annotation in Code
Now, let’s see how to use the AutoMapper annotation:
java@AutoMapper(source = SourceClass.class, target = TargetClass.class, mapperName = "SourceToTargetMapper") public class MyMappingConfiguration { // The processor will generate the SourceToTargetMapper interface automatically. }
When you compile this code, the processor generates a MapStruct mapper interface:
javapackage com.example.generated; import org.mapstruct.Mapper; @Mapper public interface SourceToTargetMapper { TargetClass toTarget(SourceClass source); SourceClass toSource(TargetClass target); }
You can now use this mapper interface just like any other MapStruct mapper.
Generated Output
The generated mapper interface is clean, concise, and ready to use:
javapackage com.example.generated; import org.mapstruct.Mapper; @Mapper public interface SourceToTargetMapper { TargetClass toTarget(SourceClass source); SourceClass toSource(TargetClass target); }
Conclusion
By combining the power of custom annotations and MapStruct, we’ve significantly reduced the amount of boilerplate code required to create mappers. This approach scales well for large applications, making it easy to manage object mappings with minimal effort.
Custom annotation processors like this one can be a game-changer for Java developers looking to automate repetitive tasks and keep their codebases clean.
Feel free to experiment and extend this example to fit your project’s needs. Happy coding!
Further Reading:
0 Comments