Saltar al contenido principal

Mapping

Código Completo
El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub springboot-demo-projects, en el/los commit(s) Mapper.
Por qué mapping está aquí

Mapping no es técnicamente un cross-cutting concern. No es un filter, aspect, o interceptor. Pero lo encontrarás tan frecuentemente en proyectos reales que vale la pena configurarlo temprano con la ayuda de una librería junto con tus otras decisiones de infraestructura. Por eso lo cubrimos aquí.

En el documento de primer endpoint, FilmRestMapper era una clase escrita a mano que convertía un modelo de dominio Film en un DTO FilmResponse campo por campo. Eso funciona para siete campos. Deja de ser aceptable cuando hay decenas de campos, objetos anidados y conversiones de tipo que gestionar, y nunca genera errores en tiempo de compilación cuando se agrega un campo nuevo al dominio, pero se olvida en el mapper.

Este documento reemplaza el mapper escrito a mano con una librería especializada: MapStruct para Java y Kotlin, ModelMapper para Groovy.

Archivos modificados

Archivos a Crear/Modificar
File Tree
.
├── build.gradle
├── ...
└── src
└── main
└── java
└── dev
└── pollito
└── spring_java
├── sakila
│ └── film
│ ├── adapter
│ │ └── in
│ │ └── rest
│ │ ├── ...
│ │ └── FilmRestMapper.java
│ └── ...
└── ...

El archivo de build recibe nuevas dependencias, y FilmRestMapper se reemplaza. Para Groovy, el controller también cambia porque el mapper ya no es una utilidad estática, sino un bean gestionado por Spring.

Agregar la dependencia

build.gradle (dependencies block)
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'
  • Java y Kotlin usan MapStruct.
    • Kotlin además requiere el plugin kapt porque MapStruct genera código en tiempo de compilación mediante procesamiento de anotaciones, y kapt es el ejecutor de procesadores de anotaciones de Kotlin.
  • Groovy usa ModelMapper, una librería basada en reflection. A diferencia de MapStruct, no requiere procesamiento de anotaciones. Funciona completamente en tiempo de ejecución, haciendo coincidir campos por nombre y tipo.

(Solo Groovy) Configurar ModelMapper

Groovy declara ModelMapperConfig, una clase @Configuration estándar que expone un bean ModelMapper. ModelMapper no tiene estado y es thread-safe, por lo que una única instancia compartida en toda la aplicación es la elección correcta.

spring_groovy/config/mapper/ModelMapperConfig.groovy
package dev.pollito.spring_groovy.config.mapper

import groovy.transform.CompileStatic
import org.modelmapper.ModelMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@CompileStatic
class ModelMapperConfig {
@Bean
ModelMapper modelMapper() {
new ModelMapper()
}
}

Reemplazar FilmRestMapper

java/dev/pollito/spring_java/sakila/film/adapter/in/rest/FilmRestMapper.java
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);
}
  • Java y Kotlin ahora declaran FilmRestMapper como una interfaz. La anotación @Mapper(componentModel = SPRING) le indica a MapStruct que genere una implementación gestionada por Spring en tiempo de compilación.
  • Groovy reemplaza la clase de utilidad estática con un @Component de Spring. Recibe el bean ModelMapper mediante inyección por constructor y delega la conversión en él. ModelMapper.map(source, FilmResponse) inspecciona ambas clases en tiempo de ejecución y copia los campos coincidentes por nombre y tipo.

(Solo Groovy) Actualizar el controller

El controller original de Groovy importaba FilmRestMapper.convert como un método estático. Ahora que FilmRestMapper es un bean de Spring, el controller necesita recibirlo mediante inyección.

groovy/dev/pollito/spring_groovy/sakila/film/adapter/in/rest/FilmRestController.groovy
package dev.pollito.spring_groovy.sakila.film.adapter.in.rest

import dev.pollito.spring_groovy.sakila.film.adapter.in.rest.dto.FilmResponse
import dev.pollito.spring_groovy.sakila.film.domain.port.in.FilmUseCases
import groovy.transform.CompileStatic
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/films")
@CompileStatic
class FilmRestController {
FilmUseCases useCases
FilmRestMapper mapper

FilmRestController(FilmUseCases useCases, FilmRestMapper mapper) {
this.useCases = useCases
this.mapper = mapper
}

@GetMapping("/{id}")
FilmResponse getFilm(@PathVariable("id") Integer id) {
mapper.map(useCases.getFilm(id))
}
}

Los controllers de Java y Kotlin no necesitan actualizarse. Ya inyectaban FilmRestMapper como campo, y esa inyección sigue funcionando igual, solo que ahora apunta a la implementación generada por MapStruct en lugar de la clase escrita a mano.

Con la librería de mapping en su lugar, FilmRestMapper ya no se mantiene a mano. MapStruct genera las implementaciones de Java y Kotlin en tiempo de compilación, y ModelMapper maneja Groovy en tiempo de ejecución. Ambos enfoques se integran con la inyección de dependencias de Spring, por lo que el resto de la aplicación permanece sin cambios.