DEV Community

Cover image for Avoiding Boilerplate Code With MapStruct, Spring Boot and Kotlin
Antonello Zanini for Writech

Posted on • Edited on

Avoiding Boilerplate Code With MapStruct, Spring Boot and Kotlin

Building Spring Boot Rest Web Services might become cumbersome, especially when domain model objects need to be converted in many DTOs (Data Transfer Object) and vice versa. Writing mapping manually is boring and involves boilerplate code. Is it possible to avoid it?

Yes, it is! Thanks to MapStruct! MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes.

Integrating MapStruct with Spring Boot in Java is well documented, but what if we wanted to use Kotlin?

Let's see how MapStruct can be integrated into a Spring Boot and Kotlin project.

1. Gradle Dependency

Let's add the Kapt compiler plugin and the MapStruct processor dependencies in build.gradle.kts.

plugins {
   kotlin("kapt") version "1.3.72"
}
dependencies {   
   kapt("org.mapstruct:mapstruct-processor:1.3.1.Final")
}
Enter fullscreen mode Exit fullscreen mode

_mapstruct-processor_ is required to generate the mapper implementation during build-time, while _kapt_ is the Kotlin Annotation Processing Tool, and it is used to reference the generated code from Kotlin.

Now, our build.gradle.kts file looks like this_:_

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.3.1.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    kotlin("jvm") version "1.3.72"
    kotlin("plugin.spring") version "1.3.72"
    kotlin("kapt") version "1.3.72"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.boot:spring-boot-starter-web:2.3.1.RELEASE")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
    implementation("org.mapstruct:mapstruct-jdk8:1.3.1.Final")
    kapt("org.mapstruct:mapstruct-processor:1.3.1.Final")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Creating the Domain Model Objects and Their DTOs

We are going to create two domain model classes: Author and Book.

class Author {
    var id: Int? = null
    var name: String? = null
    var surname: String? = null
    var birthDate: Date? = null
    val books: MutableList<Book> = ArrayList()

    // the default constructor is required by MapStruct to convert
    // a DTO object into a model domain object
    constructor()

    constructor(id : Int, name: String, surname: String, birthDate: Date) {
        this.id = id
        this.name = name
        this.surname = surname
        this.birthDate = birthDate
    }

    constructor(id: Int, name: String, surname: String, birthDate: Date, books: List<Book>) : this(id, name, surname, birthDate) {
        this.books.addAll(books)
    }
}
Enter fullscreen mode Exit fullscreen mode
class Book {
    var id: Int? = null
    var title: String? = null
    var releaseDate: Date? = null

    // the default constructor is required by MapStruct to convert
    // a DTO object into a model domain object
    constructor()

    constructor(id : Int, title : String, releaseDate: Date) {
        this.id = id
        this.title = title
        this.releaseDate = releaseDate
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to create the corresponding DTO classes.

class AuthorDto {
    @JsonProperty("id")
    var id: Int? = null

    @JsonProperty("name")
    var name: String? = null

    @JsonProperty("surname")
    var surname: String? = null

    @JsonProperty("birth")
    @JsonFormat(pattern = "MM/dd/yyyy")
    var birthDate: Date? = null

    @JsonProperty("books")
    var books: List<BookDto> = ArrayList()
}
Enter fullscreen mode Exit fullscreen mode
class BookDto {
    @JsonProperty("id")
    var id: Int? = null

    @JsonProperty("title")
    var title: String? = null

    @JsonProperty("release")
    @JsonFormat(pattern = "MM/dd/yyyy")
    var releaseDate: Date? = null
}
Enter fullscreen mode Exit fullscreen mode

What if we wanted a special DTO for Author? Maybe, we might be interested only in the first book written by an author, and not in their birthdate. Let's build a special DTO to accomplish this goal.

class SpecialAuthorDto {
    @JsonProperty("name")
    var name: String? = null
    @JsonProperty("surname")
    var surname: String? = null
    // no birthDate
    @JsonProperty("firstBook")  
    var firstBook : BookDto? = null
}
Enter fullscreen mode Exit fullscreen mode

3. Defining the MapStruct Mappers

MapStruct is not based on magic and, in order to map domain model objects in DTO objects, requires the definition of one or more mappers.

Each mapper is an interface or an abstract class and must be annotated with @Mapper annotation. This causes the MapStruct code generator to create an implementation of the mapper interface or abstract class during build-time. Hence, implementation is not required. This is where the magic begins and allows us to avoid boilerplate code.

By default, in the generated method implementations all readable properties from the source type (e.g., Author) will be mapped into the corresponding property in the target type (e.g., AuthorDto).

Let's see how a mapper can be defined.

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
interface AuthorMapper {
    fun authorToAuthorDto(
        author : Author
    ) : AuthorDto

    fun authorsToAuthorDtos(
        authors : List<Author>
    ) : List<AuthorDto>
}
Enter fullscreen mode Exit fullscreen mode

The componentModel attribute is set to "spring" to force the MapStruct processor to generate a singleton Spring bean mapper that can be injected directly where need it.

If you are interested in studying what the MapStruct processor produced at build-time, you can find the implementation classes in build/generated/source/kapt/.

Let's see an example.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2020-06-28T09:24:47+0200",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 13.0.2 (Oracle Corporation)"
)
@Component
public class AuthorMapperImpl implements AuthorMapper {

    @Override
    public AuthorDto authorToAuthorDto(Author author) {
        if ( author == null ) {
            return null;
        }

        AuthorDto authorDto = new AuthorDto();

        authorDto.setId( author.getId() );
        authorDto.setName( author.getName() );
        authorDto.setSurname( author.getSurname() );
        authorDto.setBirthDate( author.getBirthDate() );
        authorDto.setBooks( bookListToBookDtoList( author.getBooks() ) );

        return authorDto;
    }

    @Override
    public List<AuthorDto> authorsToAuthorDtos(List<Author> authors) {
        if ( authors == null ) {
            return null;
        }

        List<AuthorDto> list = new ArrayList<AuthorDto>( authors.size() );
        for ( Author author : authors ) {
            list.add( authorToAuthorDto( author ) );
        }

        return list;
    }

    protected BookDto bookToBookDto(Book book) {
        if ( book == null ) {
            return null;
        }

        BookDto bookDto = new BookDto();

        bookDto.setId( book.getId() );
        bookDto.setTitle( book.getTitle() );
        bookDto.setReleaseDate( book.getReleaseDate() );

        return bookDto;
    }

    protected List<BookDto> bookListToBookDtoList(List<Book> list) {
        if ( list == null ) {
            return null;
        }

        List<BookDto> list1 = new ArrayList<BookDto>( list.size() );
        for ( Book book : list ) {
            list1.add( bookToBookDto( book ) );
        }

        return list1;
    }
}
Enter fullscreen mode Exit fullscreen mode

What if we need a custom mapping logic, as in the SpecialAuthor example?

We have to tell the MapStruct processor that a specific field of the source object has to be mapped through a special method, containing the mapping logic.

In order to make MaStruct build a correct implementation class, a mapper abstract class is required. In fact, Kotlin default method implementation in interfaces seems to be ignored by the MapStruct processor.

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
abstract class SpecialAuthorMapper {
    // author's book property is accessed through java setter
    @Mappings(
        Mapping(target="firstBook", expression = "java(booksToFirstBook(author.getBooks()))")
    )
    abstract fun authorToSpecialAuthorDto(
        author : Author
    ) : SpecialAuthorDto

    // required in order to convert Book into BookDto
    // in booksToFirstBook
    abstract fun bookToBookDto(
        book : Book
    ) : BookDto

    // converting books into the first released book
    fun booksToFirstBook(books : List<Book>) : BookDto {
        return bookToBookDto(
            books
                .sortedWith(Comparator { e1: Book, e2: Book -> e1.releaseDate.compareTo(e2.releaseDate) })
                .first()
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

In the expression field we need to use Java-like setters, since Java is required.

4. Putting It All Together

Let's create a controller and test our work.

@RestController
@RequestMapping("/authors")
class AuthorController {
    @Autowired
    lateinit var authorRepository: AuthorRepository

    @Autowired
    lateinit var authorMapper: AuthorMapper

    @Autowired
    lateinit var specialAuthorMapper: SpecialAuthorMapper

    @GetMapping
    fun getAll() : ResponseEntity<List<AuthorDto>> {
        return ResponseEntity(
            authorMapper.authorsToAuthorDtos(authorRepository.findAll()),
            HttpStatus.OK)
    }

    @GetMapping("special/{id}")
    fun getSpecial(@PathVariable(value = "id") id: Int) : ResponseEntity<SpecialAuthorDto> {
        return ResponseEntity(
            specialAuthorMapper.authorToSpecialAuthorDto(authorRepository.find(id)),
            HttpStatus.OK)
    }

    @GetMapping("{id}")
    fun get(@PathVariable(value = "id") id: Int) : ResponseEntity<AuthorDto> {
        return ResponseEntity(
            authorMapper.authorToAuthorDto(authorRepository.find(id)),
            HttpStatus.OK)
    }
}
Enter fullscreen mode Exit fullscreen mode

The two mappers are injected into the controller, which uses them to produce the required DTO classes. These classes are then converted into JSON by Jackson and used to build the response of each API.

Let's generate some authors and books.

@Component
class DataSource {
    val data : MutableList<Author> = ArrayList()

    init {
        val book1 = Book(
            1,
            "Book 1",
            GregorianCalendar(2018, 10, 24).time
        )

        val book2 = Book(
            2,
            "Book 2",
            GregorianCalendar(2010, 7, 12).time
        )

        val book3 = Book(
            3,
            "Book 3",
            GregorianCalendar(2011, 3, 8).time
        )

        val author1 = Author(
                1,
                "John",
                "Smith",
                GregorianCalendar(1967, 1, 2).time,
                listOf(book1, book2, book3)
        )

        val book4 = Book(
            4,
            "Book 4",
            GregorianCalendar(2012, 12, 9).time
        )

        val book5 = Book(
            5,
            "Book 5",
            GregorianCalendar(2017, 9, 22).time
        )

        val author2 = Author(
                2,
                "Emma",
                "Potter",
                GregorianCalendar(1967, 1, 2).time,
                listOf(book5, book4)
        )

        data.addAll(listOf(author1, author2))
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, these will be the responses of each API:

[
   {
      "id":1,
      "name":"John",
      "surname":"Smith",
      "birth":"02/01/1967",
      "books":[
         {
            "id":1,
            "title":"Book 1",
            "release":"11/23/2018"
         },
         {
            "id":2,
            "title":"Book 2",
            "release":"08/11/2010"
         },
         {
            "id":3,
            "title":"Book 3",
            "release":"04/07/2011"
         }
      ]
   },
   {
      "id":2,
      "name":"Emma",
      "surname":"Potter",
      "birth":"02/01/1967",
      "books":[
         {
            "id":5,
            "title":"Book 5",
            "release":"10/21/2017"
         },
         {
            "id":4,
            "title":"Book 4",
            "release":"01/08/2013"
         }
      ]
   }
]
Enter fullscreen mode Exit fullscreen mode
{
   "id":1,
   "name":"John",
   "surname":"Smith",
   "birth":"02/01/1967",
   "books":[
      {
         "id":1,
         "title":"Book 1",
         "release":"11/23/2018"
      },
      {
         "id":2,
         "title":"Book 2",
         "release":"08/11/2010"
      },
      {
         "id":3,
         "title":"Book 3",
         "release":"04/07/2011"
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode
{
   "name":"John",
   "surname":"Smith",
   "firstBook":{
      "id":2,
      "title":"Book 2",
      "release":"08/11/2010"
   }
}
Enter fullscreen mode Exit fullscreen mode

Extras

DTOs can also be used to define a specific (de)serialization layer in a multi-layered architecture as described here: Designing a Multi-Layered Architecture for Building RESTful Web Services With Spring Boot and Kotlin.

The source code of his article can be found on my GitHub repository.

I hope this helps someone use MapStruct with Spring Boot and Kotlin!


The post "Avoiding Boilerplate Code With MapStruct, Spring Boot and Kotlin" appeared first on Writech.

Top comments (2)

Collapse
 
nicodeme profile image
AHLONSOU

very useful. a little parenthesis; how do you map abstract classes with mapstruct..
I was faced with a puzzle that almost drove me crazy.
suppose you have 4 classes:
Person, PersonDTO, Pieces and PiecesDTO;

Personne Entity

public abstract class Personne {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
protected Long id;
@OneToMany(mappedBy = "personne")
private List<Pieces>pieces;
}

Pieces Entity

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Pieces implements Serializable {
@Id
private Long refPiece;
@ManyToOne
@JoinColumn(name = "id_personne")
private Personne personne;
}

PersonneDto

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public abstract class PersonneDto implements Serializable {
Long id;
List<PiecesDto> pieces;
}

pieceDto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PiecesDto implements Serializable {
Long refPiece;
String LibellePiece;
String linkpiece;
Long personneId;
}

PiecesMappers

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE,
componentModel = MappingConstants.ComponentModel.SPRING)
public interface PiecesMapper extends EntityMapper<PiecesDto,Pieces>{
@Override
@Mapping(source="personneId", target = "personne.id")
Pieces toEntity(PiecesDto piecesDto);
@Override
List<Pieces> toEntity(List<PiecesDto> piecesDto);
@Override
@Mapping(source="personne.id", target = "personneId")
PiecesDto toDto(Pieces pieces);
@Override
List<PiecesDto> toDto(List<Pieces> pieces);
}

This give me an error which below:
The return type Person is an abstract class or interface.Provide a non abstract/non interface result type or a factory method

My question: how to properly map a abstract class??
thanks in advance

Collapse
 
antozanini profile image
Antonello Zanini

You have to use @ObjectFactory as mentioned here:
stackoverflow.com/questions/606010...