Mapping
Mapping isn't technically a cross-cutting concern. It's not a filter, aspect, or interceptor. But you'll encounter it so often in real projects that it deserves early setup with a library alongside your other infrastructure decisions. That's why we cover it here.
In the first endpoint document, FilmRestMapper was a handwritten class that converted a Film domain model into a FilmResponse DTO field by field. That works for seven fields. It stops being acceptable when you have dozens of fields, nested objects, and type conversions to manage, and it never generates errors at compile time when a new field is added to the domain but forgotten in the mapper.
This document replaces the handwritten mapper with a dedicated mapping library: MapStruct for Java and Kotlin, ModelMapper for Groovy.
Changed Files
- Java
- Kotlin
- Groovy
The build file gets new dependencies, and FilmRestMapper is rewritten. For Groovy, the controller also changes because the mapper is no longer a static utility; it's a Spring-managed bean.
Add the dependency
- Java
- Kotlin
- Groovy
def mapstructVersion = '1.7.0.Beta1'
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
kotlin("kapt") version "2.3.10"
val mapstructVersion = "1.7.0.Beta1"
implementation("org.mapstruct:mapstruct:$mapstructVersion")
kapt("org.mapstruct:mapstruct-processor:$mapstructVersion")
implementation 'org.modelmapper:modelmapper:3.2.6'
- Java and Kotlin use MapStruct.
- Kotlin additionally requires the
kaptplugin because MapStruct generates code at compile time through annotation processing, andkaptis Kotlin's annotation processor runner.
- Kotlin additionally requires the
- Groovy uses ModelMapper, a reflection-based library. Unlike MapStruct, it requires no annotation processing. It works entirely at runtime, matching fields by name and type.
(Groovy Only) Configure ModelMapper
Groovy declares ModelMapperConfig, a standard @Configuration class that exposes a ModelMapper bean. ModelMapper is stateless and thread-safe, so a single instance shared across the application is the right choice.
Rewrite FilmRestMapper
- Java
- Kotlin
- Groovy
package dev.pollito.spring_java.sakila.film.adapter.in.rest;
import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
import dev.pollito.spring_java.sakila.film.adapter.in.rest.dto.FilmResponse;
import dev.pollito.spring_java.sakila.film.domain.model.Film;
import org.mapstruct.Mapper;
@Mapper(componentModel = SPRING)
public interface FilmRestMapper {
FilmResponse map(Film source);
}
package dev.pollito.spring_kotlin.sakila.film.adapter.`in`.rest
import dev.pollito.spring_kotlin.sakila.film.adapter.`in`.rest.dto.FilmResponse
import dev.pollito.spring_kotlin.sakila.film.domain.model.Film
import org.mapstruct.Mapper
import org.mapstruct.MappingConstants.ComponentModel.SPRING
@Mapper(componentModel = SPRING)
interface FilmRestMapper {
fun map(source: Film): FilmResponse
}
- Java and Kotlin now declare
FilmRestMapperas an interface. The@Mapper(componentModel = SPRING)annotation tells MapStruct to generate a Spring-managed implementation at compile time. - Groovy replaces the static utility class with a Spring
@Component. It receives theModelMapperbean through constructor injection and delegates conversion to it.ModelMapper.map(source, FilmResponse)inspects both classes at runtime and copies matching fields by name and type.
(Groovy Only) Update the controller
The original Groovy controller imported FilmRestMapper.convert as a static method. Now that FilmRestMapper is a Spring bean, the controller needs to receive it through injection instead.
Java and Kotlin controllers don't need updating. They already injected FilmRestMapper as a field. That injection still works the same way, just against the MapStruct-generated implementation instead of the handwritten class.
With the mapping library in place, FilmRestMapper is no longer maintained by hand. MapStruct generates the Java and Kotlin implementations at compile time, and ModelMapper handles Groovy at runtime. Both approaches integrate with Spring's dependency injection, so the rest of the application stays untouched.