In un’applicazione Java è frequente dover trasferire dati da un oggetto a un altro. Un caso tipico è la conversione di una entity proveniente dal database in un DTO destinato a una API REST, a un servizio esterno o al livello di presentazione.
Quando gli oggetti contengono molte proprietà, strutture annidate o tipi differenti, copiare manualmente ogni valore con una lunga sequenza di metodi get e set può rendere il codice ripetitivo, fragile e difficile da mantenere. Dozer nasce proprio per automatizzare il mapping tra Java Bean.
⚠️ Aggiornamento importante
Dozer può essere ancora presente in numerose applicazioni legacy, ma il progetto ufficiale non è più attivamente sviluppato. Per una nuova applicazione è consigliabile valutare alternative moderne come MapStruct o ModelMapper. Questa guida rimane utile per mantenere, aggiornare o comprendere progetti che utilizzano Dozer.
Che cos’è Dozer
Dozer è un framework Java per il mapping tra Java Bean. Il suo compito è leggere le proprietà di un oggetto sorgente e copiarle in un oggetto di destinazione, effettuando quando possibile anche la conversione automatica dei tipi.
Il framework può essere utilizzato, ad esempio, per convertire:
- entity JPA in DTO;
- DTO ricevuti da una API in oggetti di dominio;
- modelli interni in oggetti richiesti da servizi esterni;
- Java Bean con proprietà simili ma tipi differenti;
- oggetti contenenti altri oggetti o collezioni;
- modelli legacy in strutture più moderne.
Dozer supporta due modalità principali:
- mapping implicito: associa automaticamente le proprietà che hanno lo stesso nome;
- mapping esplicito: utilizza XML, API Java o converter personalizzati per indicare la corrispondenza tra proprietà differenti.
💡 Esempio: se sia ClienteEntity sia ClienteDTO contengono le proprietà nome ed email, Dozer può copiarle senza alcuna configurazione XML.
Quando utilizzare un Bean Mapper
Un mapper può essere utile quando l’applicazione è organizzata in diversi livelli e non si desidera utilizzare lo stesso oggetto in ogni punto del sistema.
Esporre direttamente una entity JPA attraverso una API REST, ad esempio, può provocare diversi problemi:
- esposizione involontaria di proprietà riservate;
- forte dipendenza tra database e API;
- serializzazione di relazioni non necessarie;
- problemi con proxy e caricamento lazy;
- difficoltà nel modificare il modello interno;
- risposte JSON più pesanti del necessario.
Un DTO permette invece di controllare con precisione quali dati rendere disponibili.
Esempio di conversione manuale
ClienteDTO dto = new ClienteDTO();
dto.setId(cliente.getId());
dto.setNome(cliente.getNome());
dto.setEmail(cliente.getEmail());
dto.setEta(cliente.getEta());
dto.setCitta(cliente.getIndirizzo().getCitta());
La stessa conversione con un mapper
ClienteDTO dto = mapper.map(cliente, ClienteDTO.class);
Il secondo approccio riduce il codice ripetitivo, ma non elimina la necessità di progettare correttamente DTO, entity e confini dell’applicazione.
Installazione di Dozer con Maven o Gradle
Le vecchie versioni di Dozer utilizzavano il gruppo Maven net.sf.dozer. Nelle versioni moderne la dipendenza principale è com.github.dozermapper:dozer-core.
Dipendenza Maven
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>7.0.0</version>
</dependency>
Dipendenza Gradle
dependencies {
implementation 'com.github.dozermapper:dozer-core:7.0.0'
}
✅ Nota: è buona pratica centralizzare la versione della libreria nel blocco properties di Maven o nel catalogo delle versioni di Gradle.
Primo mapping automatico tra Java Bean
Vediamo un esempio completo basato su un oggetto ClienteEntity e un oggetto ClienteDTO.
Classe sorgente: ClienteEntity
public class ClienteEntity {
private Long id;
private String nome;
private String email;
private Integer eta;
public ClienteEntity() {
}
public ClienteEntity(Long id, String nome, String email, Integer eta) {
this.id = id;
this.nome = nome;
this.email = email;
this.eta = eta;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getEta() {
return eta;
}
public void setEta(Integer eta) {
this.eta = eta;
}
}
Classe di destinazione: ClienteDTO
public class ClienteDTO {
private Long id;
private String nome;
private String email;
private Integer eta;
public ClienteDTO() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getEta() {
return eta;
}
public void setEta(Integer eta) {
this.eta = eta;
}
@Override
public String toString() {
return "ClienteDTO{" +
"id=" + id +
", nome='" + nome + '\'' +
", email='" + email + '\'' +
", eta=" + eta +
'}';
}
}
Creazione del Mapper
Nelle versioni moderne di Dozer il mapper può essere creato attraverso DozerBeanMapperBuilder.
import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
public class DozerExample {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
ClienteEntity cliente = new ClienteEntity(
10L,
"Mario Rossi",
"mario.rossi@example.com",
38
);
ClienteDTO dto = mapper.map(cliente, ClienteDTO.class);
System.out.println(dto);
}
}
Dozer crea una nuova istanza di ClienteDTO e copia automaticamente tutte le proprietà compatibili che hanno lo stesso nome.
Mapping su un oggetto già esistente
È possibile utilizzare anche un oggetto di destinazione precedentemente creato:
ClienteDTO dto = new ClienteDTO();
mapper.map(cliente, dto);
🚫 Da evitare
Non creare un nuovo Mapper per ogni conversione. L’inizializzazione ha un costo e l’istanza dovrebbe normalmente essere riutilizzata.
Conversione automatica dei tipi
Dozer può effettuare diverse conversioni tra tipi compatibili. Una proprietà rappresentata come String nell’oggetto sorgente, ad esempio, può essere convertita in Integer nell’oggetto di destinazione.
Oggetto sorgente
public class ProdottoRequest {
private String codice;
private String quantita;
public ProdottoRequest() {
}
public String getCodice() {
return codice;
}
public void setCodice(String codice) {
this.codice = codice;
}
public String getQuantita() {
return quantita;
}
public void setQuantita(String quantita) {
this.quantita = quantita;
}
}
Oggetto di destinazione
public class Prodotto {
private String codice;
private Integer quantita;
public Prodotto() {
}
public String getCodice() {
return codice;
}
public void setCodice(String codice) {
this.codice = codice;
}
public Integer getQuantita() {
return quantita;
}
public void setQuantita(Integer quantita) {
this.quantita = quantita;
}
}
Esecuzione della conversione
ProdottoRequest request = new ProdottoRequest();
request.setCodice("ABC-100");
request.setQuantita("15");
Prodotto prodotto = mapper.map(request, Prodotto.class);
System.out.println(prodotto.getQuantita()); // 15
Queste conversioni automatiche sono comode, ma l’input deve essere valido. Se quantita contenesse il valore "quindici", la conversione numerica potrebbe fallire.
🔎 Buona pratica: validare sempre i dati ricevuti dall’esterno prima di avviare il mapping.
Mapping XML con proprietà differenti
Il mapping implicito funziona quando le proprietà hanno lo stesso nome. Se una classe utilizza nomeCompleto e l’altra utilizza nominativo, occorre definire una regola esplicita.
Creiamo il file:
src/main/resources/dozer-mapping.xml
Configurazione XML aggiornata
<?xml version="1.0" encoding="UTF-8"?>
<mappings
xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://dozermapper.github.io/schema/bean-mapping
http://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>it.javaboss.model.ClienteEntity</class-a>
<class-b>it.javaboss.dto.ClienteDTO</class-b>
<field>
<a>nomeCompleto</a>
<b>nominativo</b>
</field>
</mapping>
</mappings>
Il mapper deve quindi essere inizializzato indicando il file XML:
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingFiles("dozer-mapping.xml")
.build();
Mapping implicito ed esplicito insieme
Per impostazione predefinita, l’attributo wildcard è attivo. Di conseguenza Dozer:
- applica le corrispondenze dichiarate nel file XML;
- continua a copiare automaticamente le altre proprietà con lo stesso nome.
Per copiare esclusivamente i campi dichiarati si può disattivare il comportamento automatico:
<mapping wildcard="false">
<class-a>it.javaboss.model.ClienteEntity</class-a>
<class-b>it.javaboss.dto.ClienteDTO</class-b>
<field>
<a>nomeCompleto</a>
<b>nominativo</b>
</field>
</mapping>
🔐 Caso d’uso: wildcard="false" è particolarmente utile quando si vuole impedire che nuove proprietà aggiunte all’entity vengano esposte automaticamente nel DTO.
Mapping di oggetti annidati
Dozer può accedere anche alle proprietà di oggetti annidati. Supponiamo che ClienteEntity contenga un oggetto Indirizzo, mentre il DTO esponga direttamente la città.
Classe Indirizzo
public class Indirizzo {
private String via;
private String citta;
private String cap;
public Indirizzo() {
}
public String getVia() {
return via;
}
public void setVia(String via) {
this.via = via;
}
public String getCitta() {
return citta;
}
public void setCitta(String citta) {
this.citta = citta;
}
public String getCap() {
return cap;
}
public void setCap(String cap) {
this.cap = cap;
}
}
Proprietà nell’entity e nel DTO
// ClienteEntity
private Indirizzo indirizzo;
// ClienteDTO
private String citta;
private String codicePostale;
Mapping delle proprietà annidate
<mapping>
<class-a>it.javaboss.model.ClienteEntity</class-a>
<class-b>it.javaboss.dto.ClienteDTO</class-b>
<field>
<a>indirizzo.citta</a>
<b>citta</b>
</field>
<field>
<a>indirizzo.cap</a>
<b>codicePostale</b>
</field>
</mapping>
In questo modo il DTO può avere una struttura più semplice rispetto al modello di dominio.
Mapping di liste e collezioni
Dozer supporta il mapping di array, liste e set. Quando però si lavora con tipi generici, una soluzione semplice e facilmente leggibile consiste nel convertire esplicitamente ogni elemento della collezione.
Conversione di una lista con Stream
import java.util.List;
List<ClienteDTO> risultati = clienti.stream()
.map(cliente -> mapper.map(cliente, ClienteDTO.class))
.toList();
Su versioni di Java precedenti a Java 16 è possibile utilizzare Collectors.toList():
import java.util.List;
import java.util.stream.Collectors;
List<ClienteDTO> risultati = clienti.stream()
.map(cliente -> mapper.map(cliente, ClienteDTO.class))
.collect(Collectors.toList());
Collezioni annidate
Se entrambi gli oggetti contengono una proprietà generica compatibile, Dozer può determinare il tipo degli elementi:
public class OrdineEntity {
private List<RigaOrdineEntity> righe;
public OrdineEntity() {
}
public List<RigaOrdineEntity> getRighe() {
return righe;
}
public void setRighe(List<RigaOrdineEntity> righe) {
this.righe = righe;
}
}
public class OrdineDTO {
private List<RigaOrdineDTO> righe;
public OrdineDTO() {
}
public List<RigaOrdineDTO> getRighe() {
return righe;
}
public void setRighe(List<RigaOrdineDTO> righe) {
this.righe = righe;
}
}
È comunque indispensabile verificare il risultato con test automatici, soprattutto quando le collezioni contengono gerarchie, interfacce o tipi non facilmente deducibili.
Creare un Custom Converter
Le conversioni automatiche non possono coprire ogni possibile requisito. Quando è necessario applicare una logica personalizzata, Dozer permette di creare un converter.
Il seguente esempio converte un LocalDate in una data testuale nel formato italiano.
Implementazione del converter
import com.github.dozermapper.core.DozerConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LocalDateStringConverter
extends DozerConverter<LocalDate, String> {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
public LocalDateStringConverter() {
super(LocalDate.class, String.class);
}
@Override
public String convertTo(LocalDate source, String destination) {
if (source == null) {
return null;
}
return source.format(FORMATTER);
}
@Override
public LocalDate convertFrom(String source, LocalDate destination) {
if (source == null || source.isBlank()) {
return null;
}
return LocalDate.parse(source, FORMATTER);
}
}
Associazione del converter a un campo
<mapping>
<class-a>it.javaboss.model.UtenteEntity</class-a>
<class-b>it.javaboss.dto.UtenteDTO</class-b>
<field
custom-converter="it.javaboss.converter.LocalDateStringConverter">
<a>dataNascita</a>
<b>dataNascita</b>
</field>
</mapping>
⚠️ Attenzione: un converter personalizzato deve gestire esplicitamente valori nulli, formati non validi e conversioni in entrambe le direzioni quando il mapping è bidirezionale.
Configurare Dozer con Spring Boot
In un’applicazione Spring Boot è consigliabile creare una sola istanza del mapper e registrarla come bean gestito dal container.
Configurazione Java
import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DozerConfiguration {
@Bean
public Mapper dozerMapper() {
return DozerBeanMapperBuilder.create()
.withMappingFiles("dozer-mapping.xml")
.build();
}
}
Utilizzo in un service
import com.github.dozermapper.core.Mapper;
import org.springframework.stereotype.Service;
@Service
public class ClienteService {
private final Mapper mapper;
private final ClienteRepository clienteRepository;
public ClienteService(
Mapper mapper,
ClienteRepository clienteRepository) {
this.mapper = mapper;
this.clienteRepository = clienteRepository;
}
public ClienteDTO trovaCliente(Long id) {
ClienteEntity cliente = clienteRepository.findById(id)
.orElseThrow(() ->
new ClienteNonTrovatoException(id));
return mapper.map(cliente, ClienteDTO.class);
}
}
Questa configurazione evita di inizializzare continuamente il mapper e rende più semplice sostituire Dozer in futuro con un’altra implementazione.
Testare il mapping con JUnit
Un mapping automatico deve essere trattato come qualsiasi altro componente applicativo: deve essere verificato con test.
Esempio con JUnit 5
import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class ClienteMappingTest {
private Mapper mapper;
@BeforeEach
void setUp() {
mapper = DozerBeanMapperBuilder.create()
.withMappingFiles("dozer-mapping.xml")
.build();
}
@Test
void dovrebbeConvertireClienteInDto() {
ClienteEntity cliente = new ClienteEntity(
25L,
"Anna Bianchi",
"anna@example.com",
41
);
ClienteDTO dto = mapper.map(
cliente,
ClienteDTO.class
);
assertEquals(25L, dto.getId());
assertEquals("Anna Bianchi", dto.getNome());
assertEquals("anna@example.com", dto.getEmail());
assertEquals(41, dto.getEta());
}
@Test
void dovrebbeGestireUnValoreNullo() {
ClienteEntity cliente = new ClienteEntity(
26L,
"Luca Verdi",
null,
30
);
ClienteDTO dto = mapper.map(
cliente,
ClienteDTO.class
);
assertNull(dto.getEmail());
}
}
I test dovrebbero verificare almeno:
- proprietà con lo stesso nome;
- proprietà rinominate;
- oggetti annidati;
- collezioni;
- valori nulli;
- converter personalizzati;
- campi che non devono essere esposti;
- conversioni non valide.
Errori comuni e relative soluzioni
1. Mancanza del costruttore senza argomenti
Dozer crea normalmente l’oggetto di destinazione tramite reflection. Una classe priva di costruttore accessibile senza parametri può provocare una MappingException.
public ClienteDTO() {
}
2. Il file XML non viene trovato
Verificare che il file sia presente nel classpath:
src/main/resources/dozer-mapping.xml
e che il nome utilizzato dal builder sia corretto:
.withMappingFiles("dozer-mapping.xml")
3. Proprietà non copiata
Controllare:
- il nome dei getter e setter;
- la visibilità dei metodi;
- il tipo della proprietà;
- il percorso dell’oggetto annidato;
- la corrispondenza dichiarata nel file XML;
- l’eventuale utilizzo di
wildcard="false".
4. Conversione numerica non valida
Una stringa non numerica non può essere convertita automaticamente in Integer o Long. La validazione dovrebbe avvenire prima del mapping.
5. Mapping ricorsivo indesiderato
Le entity JPA possono contenere relazioni bidirezionali, come Cliente → Ordini → Cliente. È preferibile progettare DTO più semplici ed escludere le relazioni non necessarie.
6. Nuovi campi esposti automaticamente
Se la sicurezza o la stabilità del contratto API sono prioritarie, utilizzare mapping espliciti e wildcard="false".
Prestazioni e buone pratiche
Dozer esegue il mapping a runtime e utilizza reflection. Per questo motivo la configurazione e il contesto di utilizzo hanno un impatto importante.
🚀 Buone pratiche consigliate
- creare una sola istanza del mapper;
- registrarla come singleton o bean Spring;
- eseguire il mapping ai confini dell’applicazione;
- evitare conversioni inutili in cicli molto grandi;
- non utilizzare il mapper per nascondere un modello mal progettato;
- preferire mapping espliciti per dati sensibili;
- aggiungere test per ogni conversione importante;
- monitorare le prestazioni nei processi batch;
- non affidarsi ciecamente alle conversioni automatiche;
- documentare i file XML e i converter personalizzati.
In un normale endpoint REST la differenza potrebbe non essere significativa. In un processo che converte centinaia di migliaia di elementi, invece, un mapper basato su reflection può diventare un punto critico.
Migrare da Dozer 5 a Dozer 7
Le applicazioni legacy possono contenere configurazioni simili a questa:
Vecchia dipendenza
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.4.0</version>
</dependency>
Nuova dipendenza
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>7.0.0</version>
</dependency>
Vecchia inizializzazione
Mapper mapper = new DozerBeanMapper();
Nuova inizializzazione
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
Vecchio namespace XML
xmlns="http://dozer.sourceforge.net"
Namespace XML moderno
xmlns="http://dozermapper.github.io/schema/bean-mapping"
Controlli da effettuare durante la migrazione
- aggiornare le coordinate Maven o Gradle;
- modificare gli import da
org.dozeracom.github.dozermapper.core; - sostituire l’inizializzazione diretta con il builder;
- aggiornare namespace e schema dei file XML;
- controllare eventuali converter personalizzati;
- verificare l’integrazione con Spring;
- eseguire nuovamente tutti i test di mapping;
- controllare le conversioni di date, numeri ed enum;
- valutare contestualmente la migrazione a un mapper più moderno.
Dozer, MapStruct o ModelMapper?
La scelta dipende dal tipo di progetto. Dozer può essere ragionevole per mantenere un’applicazione esistente, ma non rappresenta necessariamente la scelta migliore per un nuovo sviluppo.
| Caratteristica | Dozer | MapStruct | ModelMapper |
|---|---|---|---|
| Funzionamento | Runtime e reflection | Codice generato in compilazione | Runtime e convenzioni |
| Controllo in compilazione | Limitato | Elevato | Limitato |
| Prestazioni | Inferiori al codice diretto | Molto vicine al mapping manuale | Dipendono dalla configurazione |
| Configurazione | XML o API | Interfacce e annotazioni | API fluente e convenzioni |
| Uso consigliato | Manutenzione di progetti esistenti | Nuovi progetti con mapping stabile | Mapping dinamico e prototipi |
Perché valutare MapStruct
MapStruct genera in fase di compilazione normali metodi Java. Questo offre alcuni vantaggi:
- assenza di reflection durante il mapping;
- errori rilevati durante la compilazione;
- codice generato facilmente ispezionabile;
- buone prestazioni;
- supporto per Java Bean e record;
- integrazione con Spring.
Esempio equivalente con MapStruct
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface ClienteMapper {
@Mapping(
target = "nominativo",
source = "nomeCompleto"
)
ClienteDTO toDto(ClienteEntity cliente);
}
🧭 Quale scegliere?
Dozer è soprattutto una soluzione da conoscere per applicazioni legacy. Per un progetto nuovo e orientato alle prestazioni, MapStruct è generalmente una scelta più solida. ModelMapper può essere interessante quando si preferisce una configurazione dinamica a runtime.
Domande frequenti su Dozer
Dozer copia automaticamente tutte le proprietà?
In modalità implicita copia le proprietà compatibili che hanno lo stesso nome. Le proprietà con nomi differenti devono essere associate tramite XML, API Java o annotazioni.
Dozer esegue una copia profonda?
Dozer può mappare ricorsivamente oggetti annidati. Il risultato concreto dipende però dalla struttura delle classi, dalle regole configurate e dall’eventuale uso della copia per riferimento.
È possibile escludere una proprietà?
Sì. È possibile escludere campi specifici o utilizzare wildcard="false" per copiare soltanto le proprietà dichiarate esplicitamente.
Dozer può convertire una entity in un DTO?
Sì, ed è uno dei casi d’uso più comuni. È comunque consigliabile evitare di mappare indiscriminatamente tutte le relazioni JPA.
Dozer può mappare una lista?
Sì, supporta diverse conversioni tra array, liste e set. Per conversioni semplici è possibile anche utilizzare uno Stream e applicare il mapper a ogni elemento.
È necessario creare un mapper per ogni classe?
No. Una sola istanza di Mapper può gestire numerose coppie di classi e dovrebbe normalmente essere riutilizzata.
Dozer è thread-safe?
L’utilizzo previsto consiste normalmente nel configurare una singola istanza e riutilizzarla. La configurazione dovrebbe essere completata durante l’avvio dell’applicazione e non modificata durante le operazioni di mapping.
Dozer è ancora consigliato?
Può essere mantenuto in applicazioni esistenti, ma il progetto ufficiale non ne consiglia l’adozione per nuovi sviluppi. In questi casi è opportuno confrontarlo con MapStruct e ModelMapper.
Qual è la differenza tra Dozer e BeanUtils?
BeanUtils è principalmente una raccolta di utilità per lavorare con proprietà Java Bean. Dozer è invece progettato specificamente per definire mapping complessi tra tipi differenti, includendo conversioni, oggetti annidati e regole personalizzate.
Conclusioni
Dozer permette di ridurre il codice ripetitivo necessario per copiare dati tra Java Bean, entity e DTO. Il mapping implicito è semplice da utilizzare, mentre XML e converter personalizzati consentono di gestire nomi differenti, oggetti annidati e trasformazioni specifiche.
Quando si interviene su un’applicazione esistente, aggiornare le vecchie configurazioni a Dozer 7 può migliorare compatibilità e manutenzione. Per un progetto completamente nuovo, invece, è consigliabile valutare una soluzione attivamente mantenuta e capace di rilevare gli errori già durante la compilazione.
📌 In sintesi
Usa Dozer per comprendere o mantenere applicazioni legacy, riutilizza sempre la stessa istanza del mapper, proteggi i dati sensibili con mapping espliciti e valuta MapStruct prima di introdurre Dozer in un nuovo progetto.
Riferimenti tecnici:
repository ufficiale Dozer,
documentazione Dozer e
documentazione MapStruct.