Custom Annotations: Automatically Creating MapStruct Mappers


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

  1. What is MapStruct?
  2. Understanding Custom Annotations in Java
  3. Creating a Custom Annotation for MapStruct
  4. Implementing the Annotation Processor
  5. Using the Custom Annotation in Code
  6. Generated Output
  7. 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:

java
package 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.

java
package 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, and mapperName parameters from the AutoMapper annotation.
  • 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:

java
package 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:

java
package 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:

Post a Comment

0 Comments