Introducción
Contenido
- 1 Introducción
- 2 MapStruct
- 3 Dependencias de MapStruct
- 4 Asignaciones básicas
- 5 Asignaciones de diferentes campos de origen y destino
- 6 Asignación de entidades secundarias
- 7 Actualización de instancias existentes
- 8 Inyección de dependencia
- 9 Mapeo de enumeraciones
- 10 Mapeo de tipos de datos
- 11 Agregar métodos personalizados
- 12 Creación de mapeadores personalizados
- 13 @BeforeMapping y @AfterMapping
- 14 Agregar valores predeterminados
- 15 Agregar expresiones de Java
- 16 Manejo de excepciones durante el mapeo
- 17 Configuraciones de mapeo
- 18 Conclusión
A medida que los microservicios y las aplicaciones distribuidas se apoderan rápidamente del mundo del desarrollo, la integridad y la seguridad de los datos son más importantes que nunca. Un canal de comunicación seguro y una transferencia de datos limitada entre estos sistemas débilmente acoplados son primordiales. La mayoría de las veces, el usuario final o el servicio no necesita acceder a la totalidad de los datos de un modelo, sino solo a algunas partes específicas.
Los objetos de transferencia de datos (DTO) se aplican regularmente en estas aplicaciones. Los DTO son solo objetos que contienen la información solicitada de otro objeto. Normalmente, la información tiene un alcance limitado. Dado que los DTO son un reflejo de los objetos originales, los mapeadores entre estas clases juegan un papel clave en el proceso de conversión.
En este artículo, nos sumergiremos en MapStruct , un mapeador extenso para Java Beans.
Tabla de contenido:
- MapStruct
- Asignaciones básicas
- Asignación de diferentes campos de origen y destino
- Diferentes nombres de propiedad
- Varias clases de fuentes
- Asignación de entidades secundarias
- Actualización de instancias existentes
- Inyección de dependencia
- Mapeo de enumeraciones
- Mapeo de tipos de datos
- Agregar métodos personalizados
- Creación de mapeadores personalizados
- @BeforeMapping y @AfterMapping
- Agregar valores predeterminados
- Agregar expresiones de Java
- Manejo de excepciones durante el mapeo
- Heredar configuración
- Heredar configuración inversa
MapStruct
MapStruct es un generador de código de código abierto basado en Java que crea código para implementaciones de mapas.
Utiliza el procesamiento de anotaciones para generar implementaciones de clases de mapeadores durante la compilación y reduce en gran medida la cantidad de código repetitivo que se escribiría regularmente a mano.
Dependencias de MapStruct
Si está usando Maven, instale MapStruct agregando la dependencia:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Esta dependencia importará las anotaciones centrales de MapStruct. Dado que MapStruct funciona en tiempo de compilación y está adjunto a constructores como Maven y Gradle, también tendremos que agregar un complemento a <build>
:
<build>
<plugins>
<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>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Si está utilizando Gradle, instalar MapStruct es tan simple como:
plugins {
id 'net.ltgt.apt' version '0.20'
}
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
compile "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
El net.ltgt.apt
complemento es responsable del procesamiento de anotaciones. Puede aplicar los complementos apt-idea
y apt-eclipse
según su IDE.
Puede consultar la última versión en Maven Central .
Asignaciones básicas
Comencemos con un mapeo básico. Tendremos un Doctor
modelo y un DoctorDto
. Sus campos tendrán los mismos nombres para nuestra conveniencia:
public class Doctor {
private int id;
private String name;
}
Y:
public class DoctorDto {
private int id;
private String name;
}
Ahora, para hacer un mapeador entre estos dos, crearemos una DoctorMapper
interfaz. Al @Mapper
anotarlo con , MapStruct sabe que este es un mapeador entre nuestras dos clases:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
DoctorDto toDto(Doctor doctor);
}
Tenemos una INSTANCE
de DoctorMapper
tipo. Este será nuestro «punto de entrada» a la instancia una vez que generemos la implementación.
Hemos definido un toDto()
método en la interfaz, que acepta una Doctor
instancia y devuelve una DoctorDto
instancia. Esto es suficiente para que MapStruct sepa que nos gustaría mapear una Doctor
instancia a una DoctorDto
instancia.
Cuando construimos / compilamos la aplicación, el complemento del procesador de anotaciones MapStruct tomará la DoctorMapper
interfaz y generará una implementación para ella:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
La DoctorMapperImpl
clase ahora contiene un toDto()
método que asigna nuestros Doctor
campos a los DoctorDto
campos.
Ahora, para mapear una Doctor
instancia a una DoctorDto
instancia, haríamos:
DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
Nota: Es posible que haya notado un DoctorDtoBuilder
en la implementación anterior. Hemos omitido la implementación por brevedad, ya que los compiladores tienden a ser largos. MapStruct intentará utilizar su constructor si está presente en la clase. Si no, simplemente lo instanciará a través de la new
palabra clave.
Si desea leer más sobre el patrón de diseño de constructores en Java, ¡lo tenemos cubierto!
Asignaciones de diferentes campos de origen y destino
A menudo, un modelo y un DTO no tendrán los mismos nombres de campo. Puede haber ligeras variaciones debido a que los miembros del equipo asignan sus propias versiones y cómo le gustaría empaquetar la información para el servicio que solicitó el DTO.
MapStruct proporciona soporte para manejar estas situaciones a través de la @Mapping
anotación.
Diferentes nombres de propiedad
Actualicemos la Doctor
clase para incluir specialty
:
public class Doctor {
private int id;
private String name;
private String specialty;
}
Y para el DoctorDto
, agreguemos un specialization
campo:
public class DoctorDto {
private int id;
private String name;
private String specialization;
}
Ahora, tendremos que informarnos DoctorMapper
de esta discrepancia. Lo haremos configurando los indicadores source
y target
de la @Mapping
anotación con estas dos variantes:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
El specialty
campo de la Doctor
clase corresponde al specialization
campo de la DoctorDto
clase.
Después de compilar el código, el procesador de anotaciones ha generado esta implementación:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.specialization(doctor.getSpecialty());
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
Varias clases de fuentes
A veces, una sola clase no es suficiente para construir un DTO. A veces, queremos agregar valores de varias clases en un solo DTO para el usuario final. Esto también se hace estableciendo las banderas apropiadas en la @Mapping
anotación:
Creemos otro modelo Education
:
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
Y agregue un nuevo campo en DoctorDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
}
Ahora, actualice la DoctorMapper
interfaz:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
Hemos agregado otra @Mapping
anotación en la que hemos establecido el origen como el degreeName
de la Education
clase y el target
como el degree
campo de la DoctorDto
clase.
Si las clases Education
y Doctor
contienen campos con el mismo nombre, tendremos que informar al asignador cuál usar o lanzará una excepción. Si ambos modelos contienen un id
, tendremos que elegir cuál id
se asignará a la propiedad DTO.
Asignación de entidades secundarias
En la mayoría de los casos, los POJO no contienen solo tipos de datos primitivos. En la mayoría de los casos, contendrán otras clases. Por ejemplo, a Doctor
tendrá 1..n
pacientes:
public class Patient {
private int id;
private String name;
}
Y hagamos uno List
de ellos para Doctor
:
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
}
Dado que los Patient
datos se transferirán, también crearemos un DTO:
public class PatientDto {
private int id;
private String name;
}
Y finalmente, actualice el DoctorDto
con uno List
de los recién creados PatientDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
}
Antes de cambiar algo en DoctorMapper
, tendremos que hacer un mapeador que convierta entre las clases Patient
y PatientDto
:
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
PatientDto toDto(Patient patient);
}
Es un mapeador básico que solo mapea un par de tipos de datos primitivos.
Ahora, actualice nuestro DoctorMapper
para incluir a los pacientes del médico:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
Dado que estamos trabajando con otra clase que requiere mapeo, hemos establecido la uses
marca de la @Mapper
anotación. Este @Mapper
usa otro @Mapper
. Puede poner tantas clases / mapeadores aquí como desee; solo tenemos uno.
Debido a que agregamos esta bandera, al generar la implementación del mapeador para la DoctorMapper
interfaz, MapStruct también convertirá el Patient
modelo en un PatientDto
, ya que registramos el PatientMapper
para esta tarea.
Ahora, compilar la aplicación dará como resultado una nueva implementación:
public class DoctorMapperImpl implements DoctorMapper {
private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.specialization( doctor.getSpecialty() );
doctorDto.id( doctor.getId() );
doctorDto.name( doctor.getName() );
return doctorDto.build();
}
protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
if ( list == null ) {
return null;
}
List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );
for ( Patient patient : list ) {
list1.add( patientMapper.toDto( patient ) );
}
return list1;
}
}
Evidentemente, patientListToPatientDtoList()
se ha agregado un nuevo mapeador , además del toDto()
mapeador. Esto se hace sin una definición explícita, simplemente porque agregamos el PatientMapper
al DoctorMapper
.
El método itera sobre una lista de Patient
modelos, los convierte en PatientDto
sy los agrega a una lista contenida dentro de un DoctorDto
objeto.
Actualización de instancias existentes
A veces, deseamos actualizar un modelo con los últimos valores de un DTO. Usando la @MappingTarget
anotación en el objeto de destino ( Doctor
en nuestro caso), podemos actualizar las instancias existentes.
Agreguemos un nuevo @Mapping
a nuestro DoctorMapper
que acepta Doctor
e DoctorDto
instancias. La DoctorDto
instancia será la fuente de datos, mientras Doctor
que será el destino:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Ahora, después de generar la implementación nuevamente, tenemos el updateModel()
método:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public void updateModel(DoctorDto doctorDto, Doctor doctor) {
if (doctorDto == null) {
return;
}
if (doctor.getPatientList() != null) {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.getPatientList().clear();
doctor.getPatientList().addAll(list);
}
else {
doctor.setPatientList(null);
}
}
else {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.setPatientList(list);
}
}
doctor.setSpecialty(doctorDto.getSpecialization());
doctor.setId(doctorDto.getId());
doctor.setName(doctorDto.getName());
}
}
Lo que vale la pena señalar es que la lista de pacientes también se actualiza, ya que es una entidad secundaria del módulo.
Inyección de dependencia
Hasta ahora, hemos estado accediendo a los mapeadores generados a través del getMapper()
método:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
Sin embargo, si está utilizando Spring , puede actualizar la configuración de su mapeador e inyectarla como una dependencia normal.
Actualicemos nuestro DoctorMapper
para trabajar con Spring:
@Mapper(componentModel = "spring")
public interface DoctorMapper {}
Agregar (componentModel = "spring")
la @Mapper
anotación le dice a MapStruct que al generar la clase de implementación del asignador, nos gustaría que se creara con el soporte de inyección de dependencia a través de Spring. Ahora, no es necesario agregar el INSTANCE
campo a la interfaz.
El generado DoctorMapperImpl
ahora tendrá la @Component
anotación:
@Component
public class DoctorMapperImpl implements DoctorMapper {}
Una vez marcado como a @Component
, Spring puede elegirlo como un bean y usted es libre de @Autowire
hacerlo en otra clase, como un controlador:
@Controller
public class DoctorController() {
@Autowired
private DoctorMapper doctorMapper;
}
Si no está utilizando Spring, MapStruct también es compatible con Java CDI :
@Mapper(componentModel = "cdi")
public interface DoctorMapper {}
Mapeo de enumeraciones
La asignación de enumeraciones funciona de la misma manera que la asignación de campos. MapStruct mapeará los que tengan los mismos nombres sin ningún problema. Sin embargo, para Enums con diferentes nombres, usaremos la @ValueMapping
anotación. Nuevamente, esto es similar a la @Mapping
anotación con tipos regulares.
Creemos dos enumeraciones, siendo la primera PaymentType
:
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT
}
Estas son, digamos, las opciones de pago disponibles en una aplicación. Y ahora, tengamos una vista más general y limitada de esas opciones:
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD
}
Ahora, hagamos una interfaz de mapeador entre estos dos enum
s:
@Mapper
public interface PaymentTypeMapper {
PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_MASTER", target = "CARD"),
@ValueMapping(source = "CARD_CREDIT", target = "CARD")
})
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
}
Aquí, tenemos un general CARD
de valor, y más específicos CARD_VISA
, CARD_MASTER
y CARD_CREDIT
valores. Hay una discrepancia con el número de valores: PaymentType
tiene 6 valores, mientras que PaymentTypeView
solo tiene 3.
Para hacer un puente entre estos, podemos usar la @ValueMappings
anotación, que acepta múltiples @ValueMapping
anotaciones. Aquí, podemos establecer la fuente para que sea cualquiera de los tres casos específicos y el destino como el CARD
valor.
MapStruct manejará estos casos:
public class PaymentTypeMapperImpl implements PaymentTypeMapper {
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if (paymentType == null) {
return null;
}
PaymentTypeView paymentTypeView;
switch (paymentType) {
case CARD_VISA: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD;
break;
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType );
}
return paymentTypeView;
}
}
CASH
y CHEQUE
tienen sus valores correspondientes por defecto, mientras que el CARD
valor específico se maneja a través de un switch
bucle.
Sin embargo, este enfoque puede resultar poco práctico cuando tiene muchos valores que le gustaría asignar a uno más general. En lugar de asignar cada uno manualmente, simplemente podemos dejar que MapStruct revise todos los valores restantes disponibles y mapearlos todos a otro.
Esto se hace a través de MappingConstants
:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
Aquí, una vez realizadas las asignaciones predeterminadas, se asignarán todos los valores restantes (que no coincidan) CARD
.
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if ( paymentType == null ) {
return null;
}
PaymentTypeView paymentTypeView;
switch ( paymentType ) {
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: paymentTypeView = PaymentTypeView.CARD;
}
return paymentTypeView;
}
Otra opción sería utilizar ANY_UNMAPPED
:
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
En este caso, en lugar de mapear primero los valores predeterminados, y luego mapear los restantes a un solo objetivo, MapStruct simplemente mapeará todos los valores no mapeados al objetivo.
Mapeo de tipos de datos
MapStruct admite la conversión de tipos de datos entre propiedades source
y target
. También proporciona conversión automática de tipos entre tipos primitivos y sus envoltorios correspondientes.
La conversión automática de tipos se aplica a:
- Conversión entre tipos primitivos y sus respectivos tipos de envoltura. Por ejemplo, la conversión entre
int
yInteger
,float
yFloat
,long
yLong
,boolean
yBoolean
etc. - Conversión entre cualquier tipo primitivo y cualquier tipo de envoltorio. Por ejemplo, entre
int
ylong
,byte
yInteger
etc. - Conversión entre todos los tipos primitivos y envoltorios y
String
. Por ejemplo, la conversión entreboolean
yString
,Integer
yString
,float
yString
etc.
Por lo tanto, durante la generación del código del asignador, si la conversión de tipo entre el campo de origen y el de destino se encuentra en cualquiera de los escenarios anteriores, MapStrcut manejará la conversión de tipo en sí.
Actualicemos nuestro PatientDto
para incluir un campo para almacenar dateofBirth
:
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
}
Por otro lado, digamos que nuestro Patient
objeto tiene un dateOfBirth
tipo de String
:
public class Patient {
private int id;
private String name;
private String dateOfBirth;
}
Ahora, sigamos adelante y creemos un mapeador entre estos dos:
@Mapper
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
}
Al convertir entre fechas, también podemos usar la dateFormat
bandera para establecer el especificador de formato. La implementación generada se verá así:
public class PatientMapperImpl implements PatientMapper {
@Override
public Patient toModel(PatientDto patientDto) {
if (patientDto == null) {
return null;
}
PatientBuilder patient = Patient.builder();
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy")
.format(patientDto.getDateOfBirth()));
}
patient.id(patientDto.getId());
patient.name(patientDto.getName());
return patient.build();
}
}
Tenga en cuenta que MapStruct ha utilizado el patrón proporcionado por la dateFormat
bandera. Si no especificamos el formato, se habría configurado en el formato predeterminado de a LocalDate
:
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
.format(patientDto.getDateOfBirth()));
}
Agregar métodos personalizados
Hasta ahora, hemos estado agregando un método de marcador de posición que queremos que MapStruct lo implemente. Lo que también podemos hacer es agregar un default
método personalizado a la interfaz. Al agregar un default
método, también podemos agregar la implementación directamente. Podremos acceder a él a través de la instancia sin problema.
Para esto, hagamos a DoctorPatientSummary
, que contiene un resumen entre ay Doctor
una lista de sus Patient
s:
public class DoctorPatientSummary {
private int doctorId;
private int patientCount;
private String doctorName;
private String specialization;
private String institute;
private List<Integer> patientIds;
}
Ahora, en nuestro DoctorMapper
, agregaremos un default
método que, en lugar de mapear a Doctor
a a DoctorDto
, convierte los objetos Doctor
y Education
en a DoctorPatientSummary
:
@Mapper
public interface DoctorMapper {
default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
Este objeto se crea a partir de los objetos Doctor
y Education
utilizando el patrón Builder Design.
Esta implementación estará disponible para su uso después de que MapStruct genere la clase de implementación del asignador. Puede acceder a él del mismo modo que accedería a cualquier otro método de mapeador:
DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);
Creación de mapeadores personalizados
Hasta ahora, hemos estado usando interfaces para crear planos para mapeadores. También podemos hacer planos con abstract
clases, anotadas con la @Mapper
anotación. MapStruct creará una implementación para esta clase, similar a la creación de una implementación de interfaz.
Reescribamos el ejemplo anterior, aunque esta vez lo convertiremos en una abstract
clase:
@Mapper
public abstract class DoctorCustomMapper {
public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
Puede usar esta implementación de la misma manera que usaría una implementación de interfaz. El uso de abstract
clases nos brinda más control y opciones al crear implementaciones personalizadas debido a menos limitaciones. Otra ventaja es la posibilidad de añadir @BeforeMapping
y @AfterMapping
métodos.
@BeforeMapping y @AfterMapping
Para un control y personalización adicionales, podemos definir @BeforeMapping
y @AfterMapping
métodos. Obviamente, estos se ejecutan antes y después de cada mapeo. Es decir, estos métodos se agregarán y ejecutarán antes y después del mapeo real entre dos objetos dentro de la implementación.
Agreguemos estos métodos a nuestro DoctorCustomMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public abstract class DoctorCustomMapper {
@BeforeMapping
protected void validate(Doctor doctor) {
if(doctor.getPatientList() == null){
doctor.setPatientList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget DoctorDto doctorDto) {
doctorDto.setName(doctorDto.getName().toUpperCase());
doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
}
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
public abstract DoctorDto toDoctorDto(Doctor doctor);
}
Ahora, generemos un mapeador basado en esta clase:
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDoctorDto(Doctor doctor) {
validate(doctor);
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
updateResult(doctorDto);
return doctorDto;
}
}
El validate()
método se ejecuta antes de DoctorDto
que se cree una instancia del objeto y el updateResult()
método se ejecuta una vez finalizada la asignación.
Agregar valores predeterminados
Un par de indicadores útiles que puede usar con la @Mapping
anotación son las constantes y los valores predeterminados. constant
Siempre se utilizará un valor, independientemente del source
valor de. Se default
utilizará un valor si el source
valor es null
.
Actualicemos nuestro DoctorMapper
con un constant
y default
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")
DoctorDto toDto(Doctor doctor);
}
Si la especialidad no está disponible, asignaremos la Information Not Available
cadena en su lugar. Además, hemos codificado el id
to be -1
.
Generemos el mapeador:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
}
else {
doctorDto.setSpecialization("Information Not Available");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
doctorDto.setId(-1);
return doctorDto;
}
}
Si doctor.getSpecialty()
regresa null
, establecemos la especialización en nuestro mensaje predeterminado. El id
se establece independientemente, ya que es un constant
.
Agregar expresiones de Java
MapStruct va tan lejos como para permitirle ingresar completamente expresiones Java como indicadores para la @Mapping
anotación. Puede establecer a defaultExpression
(si el source
valor es null
) o expression
cuál es constante.
Agreguemos un externalId
que será un String
y un appointment
que será de LocalDateTime
tipo a nuestro Doctor
y DoctorDto
.
Nuestro Doctor
modelo se verá así:
public class Doctor {
private int id;
private String name;
private String externalId;
private String specialty;
private LocalDateTime availability;
private List<Patient> patientList;
}
Y DoctorDto
se verá así:
public class DoctorDto {
private int id;
private String name;
private String externalId;
private String specialization;
private LocalDateTime availability;
private List<PatientDto> patientDtoList;
}
Y ahora, actualice nuestro DoctorMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface DoctorMapper {
@Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDtoWithExpression(Doctor doctor);
}
Aquí, hemos asignado el valor de java(UUID.randomUUID().toString())
a externalId
, mientras que hemos establecido condicionalmente la disponibilidad a un nuevo LocalDateTime
, si availability
no está presente.
Dado que las expresiones son solo String
s, tenemos que especificar las clases utilizadas en las expresiones. Este no es un código que se está evaluando, es un valor de texto literal. Por lo tanto, hemos agregado imports = {LocalDateTime.class, UUID.class}
a la @Mapper
anotación.
El mapeador generado se verá así:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDtoWithExpression(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setSpecialization(doctor.getSpecialty());
if (doctor.getAvailability() != null) {
doctorDto.setAvailability(doctor.getAvailability());
}
else {
doctorDto.setAvailability(LocalDateTime.now());
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(UUID.randomUUID().toString());
return doctorDto;
}
}
El externalId
está configurado para:
doctorDto.setExternalId(UUID.randomUUID().toString());
Mientras que, si availability
es así null
, está configurado para:
doctorDto.setAvailability(LocalDateTime.now());
Manejo de excepciones durante el mapeo
El manejo de excepciones es inevitable. Las aplicaciones incurren en estados excepcionales todo el tiempo. MapStruct proporciona soporte para incluir el manejo de excepciones sin problemas, lo que simplifica mucho su trabajo como desarrollador.
Consideremos un escenario en el que queremos validar nuestro Doctor
modelo mientras lo asignamos DoctorDto
. Hagamos una Validator
clase separada para esto:
public class Validator {
public int validateId(int id) throws ValidationException {
if(id == -1){
throw new ValidationException("Invalid value in ID");
}
return id;
}
}
Ahora, queremos actualizar nuestro DoctorMapper
para usar la Validator
clase, sin que tengamos que especificar la implementación. Como de costumbre, agregaremos las clases a la lista de clases utilizadas por @Mapper
, y todo lo que tenemos que hacer es decirle a MapStruct que nuestro toDto()
método throws ValidationException
:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor) throws ValidationException;
}
Ahora, generemos una implementación para este mapeador:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Autowired
private Validator validator;
@Override
public DoctorDto toDto(Doctor doctor) throws ValidationException {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(validator.validateId(doctor.getId()));
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(doctor.getExternalId());
doctorDto.setAvailability(doctor.getAvailability());
return doctorDto;
}
}
MapStruct ha establecido automáticamente el ID de doctorDto
con el resultado de la Validator
instancia. También agregó una throws
cláusula para el método.
Configuraciones de mapeo
MapStruct proporciona una configuración muy útil para escribir métodos de mapeador. La mayoría de las veces, las configuraciones de mapeo que especificamos para un método de mapeador se replican al agregar otro método de mapeador para tipos similares.
En lugar de configurarlos manualmente, podemos configurar tipos similares para tener los mismos métodos de mapeo o similares.
Heredar configuración
Revisemos el escenario en Actualización de instancias existentes, donde creamos un asignador para actualizar los valores de un Doctor
modelo existente a partir de un DoctorDto
objeto:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Digamos que tenemos otro mapeador que genera un a Doctor
partir de un DoctorDto
:
@Mapper(uses = {PatientMapper.class, Validator.class})
public interface DoctorMapper {
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
Doctor toModel(DoctorDto doctorDto);
}
Ambos métodos de mapeador utilizan la misma configuración. La source
sy la target
s son iguales. En lugar de repetir las configuraciones para ambos métodos de mapeadores, podemos usar la @InheritConfiguration
anotación.
Al anotar un método con la @InheritConfiguration
anotación, MapStruct buscará otro método ya configurado cuya configuración también se pueda aplicar a este. Por lo general, esta anotación se usa para métodos de actualización después de un método de mapeo, tal como lo estamos usando:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctorDto.specialization", target = "specialty")
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
Doctor toModel(DoctorDto doctorDto);
@InheritConfiguration
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Heredar configuración inversa
Otro escenario similar es escribir funciones de mapeador para mapear Model a DTO y DTO a Model , como en el siguiente código donde tenemos que especificar la misma asignación de destino de origen en ambas funciones:
Tus configuraciones no siempre serán las mismas. Por ejemplo, pueden ser inversas. Asignar un modelo a un DTO y un DTO a un modelo: utiliza los mismos campos, pero a la inversa. Así es como se ve normalmente:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
PatientDto toDto(Patient patient);
}
En lugar de escribir esto dos veces, podemos usar la @InheritInverseConfiguration
anotación en el segundo método:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@InheritInverseConfiguration
PatientDto toDto(Patient patient);
}
El código generado de ambas implementaciones del mapeador será el mismo.
Conclusión
En este artículo exploramos MapStruct, una biblioteca para crear clases de mapeadores, comenzando desde mapeos de nivel básico hasta métodos personalizados y mapeadores personalizados. También analizamos diferentes opciones proporcionadas por MapStruct, incluida la inyección de dependencias, las asignaciones de tipos de datos, las asignaciones de enumeración y el uso de expresiones.
MapStruct proporciona un potente complemento de integración para reducir la cantidad de código que tiene que escribir un usuario y hace que el proceso de creación de mapeadores sea fácil y rápido.
El código fuente del código de muestra se puede encontrar aquí .
.