MapStruct, simplificando mapeos

En los últimos tiempos han ido saliendo iniciativas que nos ayudan con nuestras tareas del día a día como desarrolladores.

Una de ellas, y de la que hablaremos en este post, es lo que se conoce como “MapStruct”, que es un procesador de anotaciones, rápido, seguro y fácil de entender.

¿Qué nos ofrece MapStruct?

MapStruct es un proyecto que nació en el año 2013, que no tuvo su versión estable hasta el año 2015. Últimamente ha adquirido cierta fama debido a su simplicidad de uso ya que, mediante anotaciones, somos capaces de generar mapeos entre diferentes objetos en tiempo de compilación.

MapStruct nos proporcciona una forma rápida y segura de mapear objetos entre sí ahorrándonos codificarlos a mano, lo cual suele ser un tarea tediosa y engorrosa.

Utiliza invocaciones de métodos en vez de reflexión, no tiene dependencias en tiempo de ejecución (código autocontenido) y nos informa en tiempo de compilación de posibles errores en los mapeos y es fácilmente debuggeable.

Configuración

MapStruct requiere de al menos Java 1.6 o posterior. Funciona en tiempo de compilación ya sea con Maven, Gradle, Ant etc.

Configuración para Maven

Si nuestro gestor de construcción de proyecto es Maven, debemos importar la dependencia MapStruct de la siguiente manera:

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId> <!-- OR use this with Java 8 and beyond: <artifactId>mapstruct-jdk8</artifactId> -->
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

Adicionalmente debemos saber que, como MapStruct realiza los mapeos en tiempo de compilación, el goal que utiliza para ello es el generate-sources de Maven, que es el que genera las clases de forma automática.

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.6</source> <!-- or 1.7 or 1.8, .. -->
                <target>1.6</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Configuración para Gradle

Si nuestro gestor de construcción de proyecto es Gradle debemos de importar la dependencia MapStruct de la siguiente manera:

dependencies {
    ...
    compile 'org.mapstruct:mapstruct:1.3.0.Final' // OR use this with Java 8 and beyond: org.mapstruct:mapstruct-jdk8:...

    annotationProcessor 'org.mapstruct:mapstruct-processor:1.3.0.Final'
    testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.3.0.Final' // if you are using mapstruct in test code
}

Configuración para IDE’s

Para los distintos IDE’s tenemos plugins que nos dan soporte sobre dicha librería:

  • En el caso de Eclipse simplemente tenemos que asegurarnos que tenemos el plugin m2e_apt.
  • Para IntelliJ tenemos que tener instalada al menos la versión 2018.2 y para activarlo debemos de activar la siguiente opción: Build, Execution, Deployment > Compiler > Annotation Processors.

Propósito y ejemplo

En muchos proyectos se usan los llamados DTO’s para aislar lo que es un objeto de negocio de un objeto que solo va a almacenar los datos.

En proyectos donde esa transformación se hace de una capa a otra para aislar el tratamiento de dichos datos, es interesante utilizar esta librería.

Para mostrar cómo se utiliza nada mejor que un ejemplo:

@Mapper
public interface InventoryServiceMapper {

   PersistenceMongoFindInventoryIDTO toPersistenceMongoFindInventoryIDTO(InventoryGetServiceIDTO idto);

}

En tiempo de compilación MapStruct genera la implementación de esta interfaz. La implementación generada utiliza invocaciones a método sencillas para la asignación entre los objetos de origen y destino.

De forma predeterminada, las propiedades se asignan si tienen el mismo nombre en origen y destino. En el caso de que no sea así tenemos un puñado de anotaciones que nos permiten realizan estos mapeos de forma personalizada.

Vamos a ver antes de entrar más a fondo con las anotaciones qué es lo que generaría una vez compilada esta interfaz:

@Generated(
   value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class InventoryServiceMapperImpl implements InventoryServiceMapper {

   @Override
   public PersistenceMongoFindInventoryIDTO toPersistenceMongoFindInventoryIDTO(InventoryGetServiceIDTO idto) {
       if ( idto == null ) {
           return null;
       }

       PersistenceMongoFindInventoryIDTO persistenceMongoFindInventoryIDTO = new PersistenceMongoFindInventoryIDTO();

       persistenceMongoFindInventoryIDTO.setItemId( idto.getItemId() );
       persistenceMongoFindInventoryIDTO.setProvider( idto.getProvider() );
       persistenceMongoFindInventoryIDTO.setShipNode( idto.getShipNode() );
       persistenceMongoFindInventoryIDTO.setSupplyType( idto.getSupplyType() );
       persistenceMongoFindInventoryIDTO.setPublished( idto.getPublished() );
       persistenceMongoFindInventoryIDTO.setOrganizationCode( idto.getOrganizationCode() );

       return persistenceMongoFindInventoryIDTO;
   }
}

En este caso, el ejemplo era sencillo, ya que tanto los objetos de entrada como de salida comparten el mismo nombre y tipo de dato.

¿Pero qué ocurre cuando esto no es así? MapStruct nos provee de una anotación @Mappings que nos da diversas posibilidades.

Fundamentalmente utilizaremos las key “source” y “target” para definir qué campo se mapea y cuál es su correspondencia en el objeto destino.

Pero, además, tenemos diversas posibilidades para definir si un determinado campo depende de otro, para indicar si ignoramos en el mapeo ciertos campos que no nos interesen o definir un formato o un valor por defecto:

@Mappings({
       @Mapping(source = "itemId", target = "persistenceItemId"),
       @Mapping(target = "supplyType", ignore = true),
       @Mapping(target = "size", defaultValue = "10")
})
PersistenceMongoFindInventoryIDTO toPersistenceMongoFindInventoryIDTO(InventoryGetServiceIDTO idto);

Siguiendo la misma filosofía es muy interesante saber que MapStruct también nos provee de mapeos entre enumerados:

@ValueMappings({
       @ValueMapping(source = "EXTRA", target = "SPECIAL"),
       @ValueMapping(source = "STANDARD", target = "DEFAULT"),
       @ValueMapping(source = "NORMAL", target = "DEFAULT")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);

También es interesante saber que, a nivel de componente, tenemos diversas posibilidades de cómo declarar nuestro @Mapper.

Por ejemplo, podemos definir el “component model” para determinar quién genera la implementación:

  • default: no utiliza el modelo de componente, son instanciados a través de Mappers.getMapper(Class).
  • cdi: el mapper generado es un CDI de ámbito de aplicación que se obtiene a través de la anotación @Inject.
  • spring: el mapper generado es un bean de spring que se obtiene a través de la anotación @Autowired.
  • jsr330: el mapper generado es anotado con @javax.inject.Named y@Singleton, y puede ser recuperado a través de @Inject.

Me gustaría destacar otra opción que a personalmente me ha resultado útil en algún proyecto, especialmente cuando necesitamos un pre o post procesado del bean a mapear con las anotaciones @BeforeMapping y @AfterMapping:

@AfterMapping
void fillShippingMethod(LineItemRQRType lineItemRQ, @MappingTarget SplitLine splitLine) {
   splitLine.getSplitKey().setShippingMethod(shippingMethodType.getByCode(lineItemRQ.getShippingMethod()));
}

También podemos establecer estrategias de nulos, manejo de excepciones, mapeos de listas, y diversas opciones muy interesantes. Podemos profundizar en esto en este enlace.

Por último, también hay que destacar que en la versión 1.2 se ha establecido compatibilidad con Lombok, de forma out of the box y con Streams de Java 1.8.

Conclusiones

Como vemos, MapStruct es una librería fácil de usar, nos provee de anotaciones sencillas y complementarias entre sí para establecer el comportamiento que queremos en cada caso, y tiene una amplia contribución por parte de la comunidad que va añadiendo nuevas funcionalidades y utilidades a esta librería tan útil.

Nos permite que nuestro código sea más legible, menos engorroso en mapeos entre clases, nos ahorra tiempo de codificación y además contiene funciones lo suficientemente potentes como para llevar a cabo estrategias de transformación de objetos a tener en cuenta en futuros proyectos.

Para obtener el código fuente y que podáis profundizar y contribuir, podéis acceder al repositorio en github.

Foto de raulmartinez

Ingeniero Informático con 11 años de experiencia en el desarrollo de aplicaciones web en entornos J2EE. Después de 8 años en Indra, donde trabajó en proyectos para el Ministerio de Educación, DGT (Dirección General de Tráfico), RFEF (Real Federación Española de Fútbol) y SELAE (Loterías y Apuestas del Estado), vio en Paradigma una oportunidad de seguir creciendo. Con gran experiencia en frameworks Spring (Spring 4, Spring Boot, Spring Webflow, Spring Data, etc.), actualmente inmerso en proyectos de eCommerce con ATG 10.2 para El Corte Inglés.

Ver toda la actividad de Raúl Martínez

Escribe un comentario