Category: Java

Java Collections

Otro tema en el que creo falta más expertiz a todos los que trabajamos con código y en Java, es al manejo de la API Collections. Todos sabemos de la interfaz List y ArrayList. Pero, ¿sabemos que es una LinkedList, un Stack o un Queue?

Este post tiene como objetivo ver toda este framework que proporciona una arquitectura para almacenar y manipular grupos de objetos.

La API Collections proporciona las interfaces Set, List, Queue y Deque y las clases ArrayList, Vector, LinkedList, PriorityQueue, HashSet, LinkedHashSet y Treeset.

Jerarquía de la API Collections.

Como podemos ver en la imagen, hay una jerarquía, y todos tienen la interfaz Iterable, esto permite que podamos recorrer estos grupos de colecciones. Además, la Interfaz Collection proporciona muchos métodos en común para todos los demás elementos:

Métodos Interfaz Collection

Esta Interfaz Collection, extiende de la Interfaz Iterable, que es la base de todas las classes de Collection. Sólo contiene un método abstracto:

Interfaz Iterable

Como podemos ver, esta Interfaz sólo contiene un método abstracto Iterator, y finalmente, ese Iterator, es una interfaz que contiene 3 métodos:

Interfaz List

La interfaz list es la interfaz hija de la Interfaz Collection. Como características importantes:

  • Estructura en la que se puede almacenar una colección ordenada de objetos.
  • Puede tener datos duplicados.
  • La interfaz es implementada por las clases: ArrayList, LinkedList, Vector y Stack.

Vamos viendo ahora estas clases una por una.

List <String> stringList= new ArrayList();  
List <String> strList = new LinkedList();  
List <Long> longList = new Vector();  
List <Example> exampleList = new Stack();  

Clase ArrayList

Características principales:

  • Permite almacenar datos duplicados de elementos de distintos data types.
  • Mantiene el orden de inserción y es de tipo non-synchronized.
  • Los elementos de los ArrayList pueden ser accedidos aleatoriamente.
  • Es un Array redimensionable que aumenta su tamaño según se necesite.

Clase LinkedList

Características:

  • Internamente usa una lista doblemente vinculada para almacenar los elementos. Esto es, que cada uno de los elementos tiene un puntero al anterior y al siguiente elemento.
  • Puede almacenar elementos duplicados.
  • Mantiene el orden de inserción y es non-synchronized.
  • Bajo ciertas ocasiones es mejor el rendimiento.

Clase Vector

Utiliza un array dinámico para almacenar los elementos.

  • Es similar a ArrayList, pero es synchronized.
  • Puede ser más lento que los arrays estándar, pero puede ser útil en programas donde se necesite mucha manipulación en los arrays.

Clase Stack

Stack hereda de Vector. Tal como crees implementa una estructura de pila, es decir, Last-In-First-Out.

  • Contiene los mismos métodos que Vector y adiciona otros útiles como: peek(), push(object o).

Cuando hablamos de non-synchronized nos referimos que si dos o más hilos acceden de forma concurrente al mismo no asegura la integridad de la información.

Interfaz Queue

La interfaz Queue mantiene un orden de tipo FIFO. Podemos definirla como una lista ordenada que mantiene elementos que serán procesados.

Las clases que implementan la interfaz Queue: PriorityQueue, Deque, ArrayDeque.

Queue<String> priorityQueue = new PriorityQueue();  
Queue<String> arrayDeque = new ArrayDeque();  

Veamos las clases que implementan esta infertaz.

Clase PriorityQueue

  • Implementa la interfaz Queue.
  • Mantiene los elementos u objetos que serán procesados por su prioridad.
  • No acepta valores nulos a ser almacenados en la cola.

Un ejemplo:

public static void main(String[] args) {
        Queue<String> priorityQueue = new PriorityQueue<>();
        priorityQueue.add("Gonzalo");
        priorityQueue.add("Munoz");
        priorityQueue.add("Mendez");

        //Contiene algunos métodos útiles como element que muestra el elemento que viene
        System.out.println(priorityQueue.element());

        //elimina el elemento que viene
        priorityQueue.remove();

        System.out.println(priorityQueue.poll());
    }

Y la consola muestra:

Interfaz Deque

  • Extiende de Interfaz Queue.
  • La ventaja que tiene es que podemos remover y añadir los elementos desde ambos lados.
  • Su nombre proviene de dounble-ended queue.

Clase ArrayDeque

  • Esta clase implementa la interfaz Deque. Nos facilita utilizar la Deque.
  • A excepción de queue, podemos añadir o borrar elementos desde ambos lados de la cola. Los métodos que ayudan a esto: addFirst(Object o) y addLast(Object o).
  • Es más veloz que un ArrayList y un Stack y no tiene restricciones de capacidad.

Interfaz Set

  • Representa un set de elementos sin ordenar y no nos permite elementos duplicados.
  • Podemos almacenar como máximo un valor nulo.
  • Es implementada por clases HashSet, LinkedHashSet y TreeSet.
Set<String> s1 = new HashSet<>();  
Set<Example> s2 = new LinkedHashSet<>();  
Set<Integer> s3 = new TreeSet<>();  

Clase HashSet

  • Implementa la interfaz Set.
  • Representa una colección que utiliza una tabla hash para el almacenaje.
  • Contiene elementos únicos.
HashSet<String> setExample=new HashSet<String>();  
set.add("1");  
set.add("2");  
set.add("3");  
set.add("1"); 

Esto nos retorna todos los elementos únicos.

Clase LinkedHashSet

  • Representa la implementación LinkedList para la interfaz Set.
  • Es similar a HashSet, la diferencia es que usa una lista doblemente enlazada para almacenar data y retener el orden de los elementos.

Interfaz SortedSet

  • Es una interfaz optativa a Set que proporciona una ordenación total de los elementos.
  • Los elementos en SortedSet están ordenados en orden ascendente.
  • Proporciona los métodos adicionales que inhiben el orden natural de los elementos.

Clase TreeSet

  • Es la clase que usa la interfaz SortedSet.
  • Contiene elementos únicos.
  • Están almacenados en orden ascendente.
SortedSet<String> ts
                = new TreeSet<>();

        ts.add("Gonzalo");
        ts.add("Zon");
        ts.add("Alberto");
        Iterator<String> itr = ts.iterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
        }

Esto nos ordena los elementos y el resultado es:

Java Generics

Los genéricos en Java están a partir del SDK 5.0 del lenguaje, y a pesar de llevar tanto tiempo con nosotros, aún hay muchos creadores de código que no saben utilizarlos correctamente.

Un pequeño ejemplo de genéricos

Es muy común que tengamos la necesidad de almacenar objetos en listas para su uso.

Por ejemplo, tomemos un listado con nombres:

List names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

Ahora, sabemos que esta lista tiene Strings, porque nosotros la declaramos con sólo nombres, verdad? Entonces hagamos alguna iteración e imprimamos los resultados, esto debería hacerse así:

for(String name: names){
    System.out.println("Name " + name);
}

o quizás:

names.forEach((String name) -> System.out.println(name));

Verdad? Bueno, la verdad es que el compilador nos va a lanzar un error en estos casos. No hay un contrato que nos asegure que cada elemento de esa lista es un String, por lo que en nuestros for y forEach lo que tenemos no es un String, sino que un Object que podemos castear para obtener nuestro esperado String:

for(Object name: names){
    String obtainedName = (String) name;
    System.out.println("Name " + obtainedName);
}

names.forEach(name -> System.out.println((String) name));

Este cast tiene varios problemas, en primer lugar, es molesto tener que hacer casting cada vez que necesitamos una variable, y por otro lado, si por equivocación metemos un valor numérico u otro objeto a la lista y hacemos un cast, nos encontraremos con una excepción.

Este problema se soluciona fácilmente si declaramos entre <> el tipo que queremos tenga nuestra lista.

List<String> names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

for(String name: names){
    System.out.println("Name " + name);
}

names.forEach(name -> System.out.println( name));

Bienvenido a los genéricos!

Clases genéricas

Veamos una clase Java

public class GenericClassTest<T> {

    T object;


    GenericClassTest(T object){
        this.object = object;
    }

    public T getObject(){
        return this.object;
    }

    public static void main(String[] args) {
        GenericClassTest<Long> longGenericClassTest = new GenericClassTest<>(29L);
        System.out.println("Long Generic Test Class: " + longGenericClassTest.getObject());

        GenericClassTest<List<String>> listGenericClassTest = new GenericClassTest<>(Arrays.asList("Gonzalo", "Munoz"));
        listGenericClassTest.getObject().forEach(System.out::println);
    }

}

Analicemos el código, acá en la declaración de la clase estamos agregándole el uso de genéricos. Esto hará que acepte cualquier tipo de objeto.

En los atributos de clase definimos un object de tipo T, y creamos un constructor que inicializará esta variable.

Finalmente estamos haciendo un método getter para obtener este objeto.

Más abajo, en la misma clase he declarado un método main donde hacemos una instancia de nuestra clase, primero con un Long, y luego con un listado de String, e imprimiendo estos valores por consola para ver que nuestra clase genérica funciona de las mil maravillas.

También podemos agregar más genéricos a una clase:

public class GenericClassTwoParameters<T,U> {

    T obj1;
    U obj2;

    GenericClassTwoParameters(T obj1, U obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    /**
     * Simple method to retrieve the generic data
     */
    public void getParameters(){
        System.out.println("Obj1: "+obj1);
        System.out.println("Obj2: "+obj2);
    }

    public static void main(String[] args) {
        GenericClassTwoParameters<String, Integer> genericClassTwoParameters = new GenericClassTwoParameters<>("Manzanas", 20);
        
        genericClassTwoParameters.getParameters();
    }
}

Funciones Genéricas

También podemos escribir funciones genéricas que pueden ser llamadas con diferentes tipos de argumentos a partir de lo que le pasemos al método genérico.

 static <T> void genericMethod(T element){
        System.out.println(element);
    }

    public static void main(String[] args) {
        //Veamos como se comporta este método:
        genericMethod("Hello World!");

        genericMethod(2000L);

        genericMethod(2.0);
    }

Acá podemos apreciar que nuestro método genérico le estamos pasando un String, luego un Long y finalmente un Double, y la salida por consola:

Algunas características de los métodos genéricos:

  • Antes del tipo de retorno tienen un type parameter.
  • El type parameter puede ser acotado: Esto es cuando ponemos por ejemplo que los tipos aceptados deban ser numéricos:
public <T extends Number> void generic(T element){...}
// Y esto también se acepta
public <T extends Number & Comparable> void generic {...}
  • Los métodos genéricos también pueden tener distintos type parameters:
public static <T, U> List<T> fromArrayToList(T[] a) {...}

Java Message Service (JMS)

¿Qué es JMS?

JMS es un servicio de mensajería en Java. Es una API en Java que permite a una aplicación enviar mensajes con otras aplicaciones.

Conceptualmente pensemos en JPA, en donde ésta es una API estándar e Hibernate es la implementación, esto es similar a JMS, donde ésta es una API estándar que requiere una implementación subyacente a ser proporcionada.

JMS es altamente escalable y permite evitar el acoplamiento entre aplicaciones usando mensajería asíncrona.

Algunas implementaciones JMS:

  • Amazon SQS
  • Apache ActiveMQ
  • JBoss Messaging
  • RabbitMQ
  • Entre muchas otras.

¿Por qué utilizar JMS?

Si bien es cierto que esta mensajería se podría hacer vía REST, JMS proporciona algunas ventajas al ser un servicio específico de mensajería.

Algunas ventajas son:

  • Es asíncrono.
  • Tiene una mejor performance que utilizar protocolo HTTP.
  • Hay flexibilidad en la entrega de mensajes.
  • Es muy confiable, posee una gran robustez cuando hablamos de seguridad.

Tipos de Mensajería

Hay algunos tipos de mensajería que debemos revisar:

Mensajería Point
  • El mensaje es encolado y entregado a un consumidor.
  • Se puede tener múltiples consumidores, pero el mensaje debe ser entregado sólo una vez.
  • Los consumidores se conectan a una cola.
MENSAJERÍA Publish / Subscribe
  • El mensaje es entregado a uno o más suscriptores.
  • Los suscriptores se suscribirán a un tópico, y luego recibirán una copia de todos los mensajes enviados a dicho tópico.

Términos Clave

  • JMS Provider: Es la implementación JMS utilizada.
  • JMS Client:Es la aplicación que envía o recibe mensajes desde un JMS provider.
  • JMS Producer or Publisher: Es el JMS Client que envía mensajes.
  • JMS Consumer / Subscriber: Es el JMS client que recibe mensajes.
  • JMS Message: Es la entidad de la data enviada.
  • JMS Queue: Es la cola de los mensajes point to point. Algunas veces es FIFO.
  • JMS Topic: Similar a una cola, pero para publicar y suscribir.

Mensaje JMS

Un mensaje JMS, contiene tres partes:

  • Header: Contiene metadata del mensaje.
    • JMSCorrelationID (Para tracear un mensaje entre múltiples consumidores).
    • JMSExpires: Se puede setear que un mensaje será borrado después de algún tiempo.
    • JMSMessageId: Seteado por el JMS Provider.
    • JMSPriority: Prioridad del mensaje.
    • JMSTimestamp: Fecha en que se envía el mensaje.
    • JMSType: Tipo de mensaje
    • JMSReplyTo: Queue o tópico donde el sender está esperando respuestas,
    • Otras más…
  • Properties: Las propiedades vienen en tres secciones:
    • Application: De la aplicación Java que envía el mensaje.
    • Provider: Usada por el proveedor JMS y la implementación específica.
    • Standar Properties: Definido por la API JMS.
  • Payload: El mensaje.

Hay muchos campos de Application Properties, Standar Properties y Headers que puede tener un mensaje JMS. Acá solo se revisaron algunos.

Tipos de Mensaje JMS

  • Message: Sólo un mensaje, sin payload. Usado para notificar sobre eventos.
  • BytesMessage: El Payload es un array de bytes.
  • TextMessage: El message está almacenado como un string como un json o xml.
  • StreamMessage: Una secuencia de primitivos Java.
  • MapMessage: El mensaje es nombre en pares de valores.
  • ObjectMessage: El mensaje es un objeto java serializado.

Hoy en día los payloads más populares son los JMS TextMessages, ya que están desacoplados de Java, y un JSON puede ser consumido por cualquier otra tecnología.

Manos a la obra

Ya sabiendo un poco más sobre JMS, veamos qué tal se ve en código.

Vamos a partir creando un nuevo proyecto Spring a partir de un Spring Initializr:

Creación de proyecto Spring
Creación de Proyecto Spring – 2
Creación de Proyecto Spring – Dependencias

Hasta aquí lo único nuevo que tiene es que estamos seleccionando una librería de Spring para que utilicemos ActiveMQ.

Con esto ya estamos listos con nuestro entorno de trabajo:

Proyecto Inicial.

Para nuestro proyecto, querremos levantar un servidor embebido sobre el cual probar las características de JMS, para ello debemos agregar unas nuevas dependencias en nuestro pom:

        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>artemis-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>artemis-jms-server</artifactId>
        </dependency>

Listo!!!

Lo primero que haremos es crearnos un POJO para nuestro mensaje JMS:

Sólo creamos un POJO simple, dentro de un nuevo package model con dos campos. A todo esto, con lombok, le generamos los getters y setters, el patrón builder y los constructores con y sin argumentos. Este POJO no sólo enviará mensajes, sino que los recibirá, por lo cual, también implementamos un Serializable.

Haremos una configuración básica para levantar nuestro servidor MQ Server. Para esto, lo haremos dentro del método main y hacemos las configuraciones básicas:

Todo esto sale directamente desde la documentación de ActiveMQ sobre cómo levantar un servidor embebido. Lo que hicimos fue configurar un pequeño y básico servidor ActiveMQServer.

Ojo que esto es sólo para fines educativos, generalmente se tendrá un servidor MQ externo, fuera de Spring Boot y sólo buscamos poder enviar un mensaje JMS y recibir uno.

Task Configuration

Ahora que nuestro servidor de pruebas está levantado, configuraremos un ejecutor de tareas para Spring, y lo que lograremos será que estaremos mandando un mensaje periódicamente.

Creamos una nueva clase de configuración con las anotaciones @EnableScheduling y @EnableAsync estas anotaciones configurarán Spring para hacer tareas desde un task pool. La anotación @Configuration, le está diciendo a Spring que tome esta clase de configuración, y escanee los métodos anotados como @Bean.

Así tendremos nuestro Bean Task Executor inyectado al Spring Context, y así Spring utilizará esto para ejecutar tareas por nosotros de forma periódica.

Message Converter Configuration

Crearemos ahora una nueva clase llamada JmsConfig. Acá declararemos un Bean llamado messageConverter en el Spring Context. Y esto será un mapper de Jackson a mensaje, específicamente para trabajar con la librería Jackson JSON.

Lo que hará Spring con esto, es que cuando enviemos un mensaje a JMS, Spring lo convertirá a un mensaje de texto de JMS, y el payload tomará ese objeto Java y lo convertirá a JSON plano.

Eso es lo que esta configuración está haciendo, habilita nuestra instancia Spring para que realice esta conversión, y lo mismo cuando llega un mensaje JMS de tipo texto, lo convierta de vuelta a un objeto Java.

Enviar mensajes JMS

Lo primero, es tener seteado un nombre para nuestra cola, esto puede hacerse en la misma clase de configuración de JMS:

Ahora crearemos una clase encargada de enviar los mensajes:

¿Qué está pasando aquí?

Hemos marcado la clase como un @Component Spring, e inyectado un JmsTemplate (que está configurado para hablar con nuestra instancia ActiveMQ). También, tenemos un método llamado SendMessage, que está configurado para ejecutarse cada 2 segundos.

Dentro de este método estamos creando un nuevo elemento de nuestro POJO HelloWorldMessage, y construyéndolo con nuestro Builder de Lombok.

Finalmente, le estamos diciendo a JmsTemplate, que haga la conversión (que hicimos anteriormente en el Message Converter. Y diciéndole que convierte este archivo a tipo Text y lo envíe a la cola que hemos definido.

Si ejecutamos nuestra app, podemos ver en la consola algo como esto:

¡¡¡¡Está funcionando!!!!

Recibir JMS Messages

Ya vimos como enviar mensajes, y lo que nos gustaría ahora es, poder capturarlos. Lo que debemos hacer es configurar un Message Listener, y esto es muy simple dentro de Spring.

Veamos qué hicimos aquí. Primero, creamos una nueva clase y la marcamos como @Component, luego creamos un método listen y lo anotamos como @JmsListener y como parámetro le dimos el nombre de nuestra cola, a la que se conectará.

En la declaración del método vemos que recibiremos un HelloWorldMessage, los headers del mensaje. La anotación @Payload le indica a Spring que puede deserializar el objeto, así como la anotación @Headers le indica que puede extraer los headers del mensaje.

Finalmente, sólo estamos llamando al POJO e imprimiéndolo, si ejecutamos la aplicación ahora nos dará en la consola que se envió el mensaje e instantáneamente, lo recibirá:

Finalmente el objeto Message que no explicamos es sólo el objeto JMS como tal. Si ejecutamos un debug e inspeccionamos los elementos podemos ver lo siguiente:

Conclusiones

Ya hemos visto lo que puede hacer JMS por nosotros. Esto es muy interesante, y sólo dimos un vistazo a todo el potencial que tiene. Próximamente, haré la parte dos de este documento, con ejemplos más complejos, levantando un ActiveMQ en Docker y conectándolo con Spring y veremos comunicación entre micro servicios y más.

Repositorio Github

Finalmente, les dejo el repo de github donde está el ejemplo funcional:

https://github.com/gonzaloan/jmsdemo.git

MapStruct

Permite mapear de forma rápida y segura objetos entre sí, sin tener que codificar a mano.

Generalmente, usamos DTOs para aislar un objeto de negocio de un objeto que se conecta con las bases de datos (@Entity).

Cuando hacemos una transformación de estos objetos, es útil usar MapStruct.

Configuración

<dependency>  
 <groupId>org.mapstruct</groupId>  
 <artifactId>mapstruct</artifactId>  
 <version>1.3.0.Final</version>  
</dependency>

Además, Mapstruct realiza mapeos en tiempos de compilación, ocupa el generate-sources de maven, que genera las clases de forma automática, así que necesitamos agregar un plugin, que funcione también con lombok:

<plugin>  
 <groupId>org.apache.maven.plugins</groupId>  
 <artifactId>maven-compiler-plugin</artifactId>  
 <version>3.5.1</version>  
 <configuration>  
     <source>1.8</source>  
     <target>1.8</target>  
     <annotationProcessorPaths>  
         <path>  
         <groupId>org.mapstruct</groupId>  
         <artifactId>mapstruct-processor</artifactId>  
         <version>${mapstruct.version}</version>  
         </path>  
         <path>  
         <groupId>org.projectlombok</groupId>  
         <artifactId>lombok</artifactId>  
         <version>${lombok.version}</version>  
         </path>  
     </annotationProcessorPaths>  
     <compilerArgs>  
     <compilerArg>-Amapstruct.defaultComponentModel=spring</compilerArg>  
     </compilerArgs>  
     </configuration>  
</plugin>

Configuración en los IDEs

  • Eclipse: Instalar plugin para permitir habilitar el procesamiento de anotaciones Plugin m2e y el plugin de MapStruct
  • IntelliJ: Sólo habilitar el procesamiento de anotaciones: Settings/Build, Execution, Deployment/Compiler/Annotation Processors

Usos

  1. Mapeo Simple

La primera opción es tener un Bean y un DTO iguales.
Por ejemplo:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
@Builder  
public class Beer {  
  private UUID id;  
 private String beerName;  
 private String beerStyle;  
 private Long upc;  
}

y el DTO:

@Getter  
@Setter  
@NoArgsConstructor  
@AllArgsConstructor  
@Builder  
public class BeerDTO {  
 private UUID id;  
 private String beerName;  
 private String beerStyle;  
 private Long upc;  

}

La forma de mapear es la siguiente:

  • Crear un package de mappers: En nuestro proyecto creamos un package mappers para almacenar todos nuestros objetos que harán el trabajo de mapear.
  • Crear una interfaz Mapper: En este caso creamos una interfaz BeerMapper, con una anotación de Mapstruct: @Mapper.
  • Crear métodos de conversion BeerToBeerDTO y BeerDTOToBeer: Estos son los métodos que van a convertir nuestros objetos.
@Mapper  
public interface BeerMapper {
//Instance nos permitirá usar el mapper en nuestras clases. 
BeerMapper INSTANCE = Mappers.getMapper(BeerMapper.class);  
  BeerDTO beerToBeerDTO(Beer beer);  
  Beer beerDTOToBeerDTO(BeerDTO beerDTO);  
}
  • El INSTANCE nos va a permitir llamar a la instancia del Mapper para utilizar los métodos.

Con esto, al momento de compilar, hará todo el proceso de mapeo automáticamente.

Finalmente, para convertir el objeto Bean a nuestro DTO simplemente hacemos lo siguiente:

BeerDTO beerDto = BeerMapper.INSTANCE.beerToBeerDTO(beer);

Y esto nos retornará el objeto mapeado.

2) Mapeo con campos distintos

El siguiente ejemplo es cuando tenemos un Bean con campos distintos a los que hay en el DTO, Por ejemplo:

public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}

public class Employee {
private int id;
private String name;
// getters and setters
}

Cuando se mapean objetos distintos, se debe configurar el campo de origen a su campo de destino, para ello utilizamos la anotación @Mapping:

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

Acá básicamente le decimos por ejemplo, que el campo employeeId debe mapearlo a id.

3) Mapeo de Beans con Beans Hijos

Otro ejemplo usual es cuando un Bean tiene una referencia a otros bean:

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters
}

public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters
}

public class Division {
    private int id;
    private String name;
    //Getters and Setters
}

Por ejemplo, en el caso anterior tenemos Employee, que tiene objeto de Division.

Mapper para este Ejemplo

Para este caso, debemos agregar el mapper del objeto Division y al revés.
MapStruct detectará que el objeto que necesita ser convertido tiene otro objeto del cual existe mapeo, hará la tarea por nosotros.

Solo debemos agregar el Mapper que no tenemos:

DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOtoDivision(DivisionDTO dto);

Y con esto, ya podemos convertir el objeto.

Otros

Hay varias opciones más que explicaré más adelante.