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")
}
_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"
}
}
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)
}
}
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
}
}
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()
}
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
}
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
}
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>
}
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;
}
}
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()
)
}
}
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)
}
}
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))
}
}
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"
}
]
}
]
{
"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"
}
]
}
{
"name":"John",
"surname":"Smith",
"firstBook":{
"id":2,
"title":"Book 2",
"release":"08/11/2010"
}
}
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)
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
You have to use @ObjectFactory as mentioned here:
stackoverflow.com/questions/606010...