Skip to main content

Annotation processor to generate DTO classes from Entity class

Annotation Processor

Annotation processors can be used to generate the classes at compile time basis on specified annotations. So we can define annotations, we can use them on classes and then we can process them using annotation processor.
Here we will see how we can use Annotation processor in Java to generate the classes at runtime. We will try to generate the DTO (Data transfer object) classes from entity classes. There are situation when a developer has to write DTO classes for the Entity class with almost same fields, so data can be transferred from one layer to another layer like UI to backend and backend to UI. We can use entity objects also to transfer between UI and backed but it will create more problem instead of solving as entity objects live in transaction context and reading or updating them in non-transactional context may change their state which may not be as expected. I just took the example of UI and backend but there can be many other scenario when it is required to transfer the data using DTO classes to hide the backend object or revealing only required data instead of whole business object data.
It becomes like a repeated task to create two similar classes, one for entity and another for DTO. How it will be if we just create entity class and tell the compiler to generate the similar DTO class from it? Yes, we can achieve it using the annotation processor in java. Now we will see the implementation.
We will create two different applications as given below.

1. DTO Generator

This application will have the custom annotations which will be used to annotate the entity classes and then annotation processors will generate the DTO class.

Custom Annotations

com.ttj.dtogen.annotations.DtoClass
This annotation will be used on Entity classes and has below attributes.
  • name - Class name for DTO class to be generated, if not given then annotated class name will be used as DTO class name by adding "Dto" to end of the name.
  • classPackage - Package where DTO class need to be generated, if not given then annotated class's package will be use by adding "dto" as sub-package under it.
  • includeAllFields - If true then all the fields will be used for DTO generation otherwise only those fields will be use which have DtoProperty annotation.
Code:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
/**
 * This annotation is used to mark the class which
 * will be read by annotation processor to generate the DTO classes
 */
public @interface DtoClass {
    /**
     * Class name, if not given then annotated class name will be used
     * by appending Dto to it.
     * @return
     */
    String name() default "";

    /**
     * Package where DTO class need to be generated, if not given
     * then annotated class's package will be use by adding "dto"
     * subpackage under it.
     * @return
     */
    String classPackage() default "";

    /**
     * if true then all the fields will be used for DTO generation
     * @return
     */
    boolean includeAllFields() default false;
}
com.ttj.dtogen.annotations.DtoProperty
This annotation is used to annotate the field of entity class which we want to include in DTO class. Below are the attributes available with this annotation.
  • name - New name for the field, if not given then annotated field's name will be used.
  • getter - If true then  getter method will be generated.
  • setter - If true then setter method will be generated.
Code:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
/**
 * This annotation is used to annotate the fields in entity classes
 * which we want to include in DTO class
 */
public @interface DtoProperty {
    /**
     * new name for the field, if not given then annotated
     * field's name will be used
     * @return
     */
    String name() default "";

    /**
     * if true then  getter method will be created
     * @return
     */
    boolean getter() default true;

    /**
     * if true then setter method will be generated
     * @return
     */
    boolean setter() default true;
}

Annotation Processor

Below is the class implemented as annotation processor.
com.ttj.dtogen.processors.DTOAnnotationProcessor
This class will read all the classes which are annotated with the "DtoClass" annotation at compile time and generate the DTO classes.
This class need to extend the "AbstractProcessor" class and override below methods.
  • public synchronized void init(ProcessingEnvironment processingEnv) - This method is used to initialize the environment related variables which will be required during the annotation processing.
  • public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) - This is the main method where we need to keep our logic to process the classes with DtoClass annotation and generate the DTO classes as per specified configurations.
  • public Set<String> getSupportedAnnotationTypes() - This method returns the list of annotations which are supported by this processor, for example - DtoClass.
  • public SourceVersion getSupportedSourceVersion() - This method return the supported version.
public class DTOAnnotationProcessor extends AbstractProcessor{
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }
    private String getValue(String value, String defaultValue){
        if(value==null || value.length()<1)
            return defaultValue;
        else
            return value;
    }
    private String[] getClassDetails(DtoClass classAnnotation, TypeElement classElem){
        String[] classDetails = new String[2];
        String parentQualifiedClassName = classElem.getQualifiedName().toString();
        String parentClassName = classElem.getSimpleName().toString();
        String parentPackage = parentQualifiedClassName.substring(0, parentQualifiedClassName.indexOf(parentClassName));
        //set package details
        classDetails[0] = getValue(classAnnotation.classPackage(), (parentPackage==null?"":parentPackage)+"dto");
        //set class name
        classDetails[1] = getValue(classAnnotation.name(), parentClassName+"Dto");

        return classDetails;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        for(Element element : roundEnv.getElementsAnnotatedWith(DtoClass.class)){
            try {
                DtoClass classAnnotation = element.getAnnotation(DtoClass.class);

                String[] classDetails = getClassDetails(classAnnotation, (TypeElement)element);
                String qualifiedGenClass = classDetails[0]+"."+classDetails[1];
                JavaFileObject javaFileObject = filer.createSourceFile(qualifiedGenClass);

                if(new File(javaFileObject.toUri()).exists()) {
                    continue;
                }

                Writer writer = javaFileObject.openWriter();

                JavaFileBuilder javaFileBuilder = new JavaFileBuilder(classDetails[1], classDetails[0]);

                //iterating through annotated fields
                for(Element fieldElem : element.getEnclosedElements()){
                    DtoProperty propAnnotation = fieldElem.getAnnotation(DtoProperty.class);
                    String fieldName = null;
                    String fieldType = null;
                    boolean isGetter = true, isSetter = true;
                    if(propAnnotation!=null) {
                        fieldName = propAnnotation.name();
                        isGetter = propAnnotation.getter();
                        isSetter = propAnnotation.setter();
                    }
                    if(propAnnotation!=null || classAnnotation.includeAllFields()) {
                        if(fieldElem instanceof VariableElement) {
                            if (fieldName == null || fieldName.length() < 1) {
                                fieldName = fieldElem.getSimpleName().toString();
                            }
                            VariableElement varElem = (VariableElement) fieldElem;
                            fieldType = varElem.asType().toString();
                            messager.printMessage(Diagnostic.Kind.NOTE,
                                    MessageFormat.format("[Class: %s] Processing field[%s] for type[%s]",
                                            qualifiedGenClass, fieldName, fieldType));
                            javaFileBuilder.addField(fieldType, fieldName, isGetter, isSetter);
                        }
                    }
                }
                writer.write(javaFileBuilder.toClassCodeString());
                writer.close();
            }catch(Exception e){
                e.printStackTrace();
                messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
            }
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new HashSet<>();
        annotationTypes.add(DtoClass.class.getCanonicalName());
        return annotationTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
com.ttj.dtogen.utils.JavaFileBuilder
This class is an utility class which is used to generate the source code for target class. You can check the source code in GIT whose link is given in the below section.
META-INF/services/javax.annotation.processing.Processor
This file is required to register the annotation by putting the qualified processor class name separated by new line.
com.ttj.dtogen.processors.DTOAnnotationProcessor

Maven Settings

Below dependencies are required for annotations API.
    <dependencies>
        <!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api -->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
Below plugin will be configured  with the compiler configuration as "-proc:none" which is required to not to run the annotation processor in it's own build otherwise you will face compilation error. Please note that this setting is required only in source code build of annotation processor.
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <compilerArgument>
                          -proc:none
                    </compilerArgument>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
Below is the complete GIT source code of annotation processor application.
https://github.com/thetechnojournals/misc_codes/tree/master/DTOGenerator

2. DTO Gen Test

This application will use the DTO Generator application as dependency and annotate the Entity classes with provided annotations. DTO classes will be generated during the compilation of this application.

Entity Class

Below are the two entity classes, for which we are going to generate the DTO classes at compile time.
com.ttj.dtogen.DepartmentEntity
Below is the code of this class where we told at class annotation to include all the fields so we don't need to annotate all the fields. But if we do then that will take precedence over this.
@DtoClass(includeAllFields = true)
public class DepartmentEntity {
    private String deptName;

    @DtoProperty(name="specialNumber", getter = false, setter = true)
    private Double splNumber;

    private Map<String, List<String>> empList;
}
com.ttj.dtogen.TestEntity
This class need to annotate all the fields to get them available in DTO class. See the below code.
@DtoClass(name="TestDto", classPackage = "com.ttj.dtogen.dto")
public class TestEntity {
    private String testId;
    @DtoProperty
    private String name;
    @DtoProperty(name="age", getter=true, setter = true)
    private Integer age;
    @DtoProperty(name="address", getter=true, setter = false)
    private String address;
}

Maven Settings

We need below dependency to use the annotations and annotation processor with our test application.
        <dependency>
            <groupId>com.ttj.dtogen</groupId>
            <artifactId>dto-generator</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
Below plugin is required if we need to generate the classes under specified source directory, otherwise you may see this generated code under target folder.
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <generatedSourcesDirectory>
                        ${project.basedir}/src/main/java
                    </generatedSourcesDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>
Below is the complete GIT source code for DTO Gen Test application.
https://github.com/thetechnojournals/misc_codes/tree/master/DTOGenTest

Executing the application

  1. Build the DTO Generator application.
  2. Build the DTO Gen Test application.
Now you will see the below classes are generated for DepartmentEntity and TestEntity classes. Please note that our Annotation Processor generates the DTO classes only when they don't exist. So to re-generate the DTO classes you need to delete the existing DTO classes.
com.ttj.dtogen.dto.DepartmentEntityDto
package com.ttj.dtogen.dto;

import java.util.List;
import java.lang.Double;
import java.util.Map;
import java.lang.String;

public class DepartmentEntityDto{

 private String deptName;
 private Double specialNumber;
 private Map<String,List<String>> empList;

 public String getDeptName(){
  return deptName;
 }
 public void setDeptName(String deptName){
  this.deptName = deptName;
 }
 public void setSpecialNumber(Double specialNumber){
  this.specialNumber = specialNumber;
 }
 public Map<String,List<String>> getEmpList(){
  return empList;
 }
 public void setEmpList(Map<String,List<String>> empList){
  this.empList = empList;
 }

}
com.ttj.dtogen.dto.TestDto
package com.ttj.dtogen.dto;

import java.lang.String;
import java.lang.Integer;

public class TestDto{

 private String name;
 private Integer age;
 private String address;

 public String getName(){
  return name;
 }
 public void setName(String name){
  this.name = name;
 }
 public Integer getAge(){
  return age;
 }
 public void setAge(Integer age){
  this.age = age;
 }
 public String getAddress(){
  return address;
 }

}

Please check below link in case you interested in how to populate DTO objects using entity object.
https://www.thetechnojournals.com/2019/10/entity-object-conversion-to-dto-object.html 

Comments

Popular Posts

SpringBoot - @ConditionalOnProperty example for conditional bean initialization

@ConditionalOnProperty annotation is used to check if specified property available in the environment or it matches some specific value so it can control the execution of some part of code like bean creation. It may be useful in many cases for example enable/disable service if specific property is available. Below are the attributes which can be used for property check.
havingValue - Provide the value which need to check against specified property otherwise it will check that value should not be false.matchIfMissing - If true it will match the condition and execute the annotated code when property itself is not available in environment.name - Name of the property to be tested. If you want to test single property then you can directly put the property name as string like "property.name" and if you have multiple properties to test then you can put the names like {"prop.name1","prop.name2"}prefix - It can be use when you want to apply some prefix to all prop…

Asynchronous REST service implementation in Spring boot

In this tutorial we will see how to create an asynchronous REST service endpoint using Spring boot application.
Asynchronous service works in a way that it will not block the client request and do the processing in separate thread. When work is complete the response returned to the client so our service will be able to handle more client requests at the same time, compare to synchronous processing model.
Let's understand how it is working in synchronous mode. In such server/client application at server side it has a pool of threads which are serving the request. If a request received by a thread then it will be blocked until it send the response back to client. In this case if processing doesn't take much time it will be able to process it quickly and accept other client requests but there could be one situation when all threads are busy and not able to accept the new client requests.

To overcome of such problems, asynchronous processing model introduced for REST services. In…

Entity to DTO conversion in Java using Jackson

It's very common to have the DTO class for a given entity in any application. When persisting data, we use entity objects and when we need to provide the data to end user/application we use DTO class. Due to this we may need to have similar properties on DTO class as we have in our Entity class and to share the data we populate DTO objects using entity objects. To do this we may need to call getter on entity and then setter on DTO for the same data which increases number of code line. Also if number of DTOs are high then we need to write lot of code to just get and set the values or vice-versa.
To overcome this problem we are going to use Jackson API and will see how to do it with minimal code only.
Maven dependency <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.9</version> </dependency> Entity class Below is our ent…

Web scraper using JSoup and Spring Boot

What is webscraping Webscraping is a technique to extract or pull the data from a website to gather required information by parsing the HTML source of their websites, such as articles from news or books site, products information from online shopping sites or course information from education sites. There are many organisations who uses web scraper to provide the best experience to their customers, for example extract the price for a smartphone from multiple online websites and show their customers the best and cheap product URL.
We will learn here how to code a web scraper by developing a simple new scraper service.
News scraper News scraper is used to extract the news articles or other related contents from a news site. Here we are going to create a web scraper application to pull the articles from news site.
Below are the operations provided by our news scraper service.
List all the authorsSearch articles by author nameSearch articles by article titleSearch articles by article desc…