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

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. Congratulations on your article, it was very helpful and successful. 9652ca1e3406b8226eb7823983fc04c4
    numara onay
    website kurma
    sms onay

    ReplyDelete
  3. Thank you for your explanation, very good content. 4ea57ff36a905639aec6a2740a171ac9
    altın dedektörü

    ReplyDelete
  4. With most slots that includes a Return to Player of 92-96%, any games with a Return to Player above 96% is a good selection. In terms of win-size, progressive slots are the slot machines that pay one of the best – but they're also those with the bottom winning odds. Finding a great casino bonus is 솔카지노 essential to play slots on-line – especially if you want to|if you want to} get a small advantage. The most typical bonus varieties include free spins, more money that matches your deposit, and special provides for recurring gamers.

    ReplyDelete

Post a Comment

Popular Posts

Setting up kerberos in Mac OS X

Kerberos in MAC OS X Kerberos authentication allows the computers in same domain network to authenticate certain services with prompting the user for credentials. MAC OS X comes with Heimdal Kerberos which is an alternate implementation of the kerberos and uses LDAP as identity management database. Here we are going to learn how to setup a kerberos on MAC OS X which we will configure latter in our application. Installing Kerberos In MAC we can use Homebrew for installing any software package. Homebrew makes it very easy to install the kerberos by just executing a simple command as given below. brew install krb5 Once installation is complete, we need to set the below export commands in user's profile which will make the kerberos utility commands and compiler available to execute from anywhere. Open user's bash profile: vi ~/.bash_profile Add below lines: export PATH=/usr/local/opt/krb5/bin:$PATH export PATH=/usr/local/opt/krb5/sbin:$PATH export LDFLAGS=&

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

Multiple data source with Spring boot, batch and cloud task

Here we will see how we can configure different datasource for application and batch. By default, Spring batch stores the job details and execution details in database. If separate data source is not configured for spring batch then it will use the available data source in your application if configured and create batch related tables there. Which may be the unwanted burden on application database and we would like to configure separate database for spring batch. To overcome this situation we will configure the different datasource for spring batch using in-memory database, since we don't want to store batch job details permanently. Other thing is the configuration of  spring cloud task in case of multiple datasource and it must point to the same data source which is pointed by spring batch. In below sections, we will se how to configure application, batch and cloud task related data sources. Application Data Source Define the data source in application properties or yml con

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