Ciao a tutti, miei cari amici e lettori! Prima di scrivere l'articolo, un po' di background... Recentemente ho riscontrato un problema lavorando con la libreria Mapstruct , che ho brevemente descritto nel mio canale Telegram qui . Nei commenti, il problema con il post è stato risolto; il mio collega del progetto precedente mi ha aiutato in questo. Successivamente, ho deciso di scrivere un articolo su questo argomento, ma ovviamente non avremo una visione ristretta e proveremo prima ad aggiornarci, a capire cos'è Mapstruct e perché è necessario, e utilizzando un esempio reale lo faremo analizzare la situazione che si è creata in precedenza e come risolverla. Pertanto, consiglio vivamente di eseguire tutti i calcoli parallelamente alla lettura dell'articolo per sperimentare tutto nella pratica. Prima di iniziare, iscriviti al mio canale Telegram , lì raccolgo le mie attività, scrivo pensieri sullo sviluppo in Java e sull'IT in generale. Iscritto? Grande! Bene, ora andiamo!
Mapstruct, domande frequenti?
Un generatore di codice per mappature bean veloci e indipendenti dai tipi. Il nostro primo compito è capire cos'è Mapstruct e perché ne abbiamo bisogno. In generale, puoi leggerlo sul sito ufficiale. Nella pagina principale del sito ci sono tre risposte alle domande: che cos'è? Per quello? Come? Proveremo a fare anche questo:
Cos'è?
Mapstruct è una libreria che aiuta a mappare (mappa, in generale, è quello che dicono sempre: mappa, mappa, ecc.) oggetti di alcune entità in oggetti di altre entità utilizzando codice generato basato su configurazioni descritte tramite interfacce.
Per quello?
Per la maggior parte, sviluppiamo applicazioni multistrato (uno strato per lavorare con il database, uno strato di logica aziendale, uno strato per l'interazione dell'applicazione con il mondo esterno) e ogni strato ha i propri oggetti per l'archiviazione e l'elaborazione dei dati . E questi dati devono essere trasferiti da un livello all'altro trasferendo da un'entità all'altra. Per coloro che non hanno lavorato con questo approccio, questo può sembrare un po' complicato. Ad esempio, abbiamo un'entità per il database Studenti. Quando i dati di questa entità passano al livello della logica aziendale (servizi), dobbiamo trasferire i dati dalla classe Student alla classe StudentModel. Successivamente, dopo tutte le manipolazioni con la logica aziendale, i dati devono essere rilasciati all'esterno. E per questo abbiamo la classe StudentDto. Ovviamente dobbiamo passare i dati dalla classe StudentModel a StudentDto. Scrivere a mano ogni volta che verranno trasferiti i metodi richiede molto lavoro. Inoltre si tratta di codice aggiuntivo nella codebase che deve essere mantenuto. Puoi fare un errore. E Mapstruct genera tali metodi in fase di compilazione e li memorizza in sorgenti generate.
Come?
Utilizzo delle annotazioni. Dobbiamo solo creare un'annotazione che abbia un'annotazione principale del Mapper che indichi alla libreria che i metodi in questa interfaccia possono essere utilizzati per tradurre da un oggetto a un altro. Come ho detto prima riguardo agli studenti, nel nostro caso si tratterà dell'interfaccia StudentMapper, che avrà diversi metodi per trasferire i dati da un livello all'altro:
La bellezza di questo approccio è che se i nomi e i tipi di campi sono gli stessi in classi diverse (come nel nostro caso), allora le impostazioni per Mapstruct sono sufficienti per generare l'implementazione necessaria basata sull'interfaccia StudentMapper in fase di compilazione, che tradurrà. Quindi è già diventato più chiaro, giusto? Andiamo oltre e utilizziamo un esempio reale per analizzare il lavoro in un'applicazione Spring Boot.
Un esempio di funzionamento di Spring Boot e Mapstruct
La prima cosa di cui abbiamo bisogno è creare un progetto Spring Boot e aggiungervi Mapstruct. A questo proposito, ho un'organizzazione in GitHub con modelli per repository e un avvio per Spring Boot è uno di questi. Sulla base di ciò, creiamo un nuovo progetto: Successivamente, otteniamo il project . Sì, amici, date una stella al progetto se lo avete trovato utile, così saprò che non lo sto facendo invano. In questo progetto sveleremo una situazione che ho ricevuto al lavoro e descritta in un post sul mio canale Telegram . Per chi non lo sapesse, illustrerò brevemente la situazione: quando scriviamo test per i mappatori (cioè per quelle implementazioni di interfaccia di cui abbiamo parlato prima), vogliamo che i test passino il più velocemente possibile. L'opzione più semplice con i mapper consiste nell'utilizzare l'annotazione SpringBootTest durante l'esecuzione del test, che preleverà l'intero ApplicationContext dell'applicazione Spring Boot e inietterà il mapper necessario per il test all'interno del test. Ma questa opzione richiede molte più risorse e molto più tempo, quindi non è adatta a noi. Dobbiamo scrivere uno unit test che crei semplicemente il mapper desiderato e controlli che i suoi metodi funzionino esattamente come ci aspettiamo. Perché hai bisogno che i test vengano eseguiti più velocemente? Se i test richiedono molto tempo, rallentano l’intero processo di sviluppo. Fino a quando i test non passano il nuovo codice, questo codice non può essere considerato corretto e non verrà preso per i test, il che significa che non verrà messo in produzione e il che significa che lo sviluppatore non ha completato il lavoro. Sembrerebbe, perché scrivere un test per una biblioteca il cui funzionamento è fuori dubbio? Eppure dobbiamo scrivere un test, perché stiamo testando quanto correttamente abbiamo descritto il mapper e se fa quello che ci aspettiamo. Innanzitutto, per facilitarci il lavoro, aggiungiamo Lombok al nostro progetto aggiungendo un'altra dipendenza a pom.xml:
Nel nostro progetto, dovremo passare dalle classi modello (che vengono utilizzate per lavorare con la logica aziendale) alle classi DTO, che utilizziamo per comunicare con il mondo esterno. Nella nostra versione semplificata, assumeremo che i campi non cambino e che i nostri mappatori saranno semplici. Ma se lo si desidera, sarebbe possibile scrivere un articolo più dettagliato su come lavorare con Mapstruct, come configurarlo e come sfruttare i suoi vantaggi. Ma poi, visto che questo articolo sarà piuttosto lungo. Supponiamo di avere uno studente con un elenco di lezioni e docenti a cui frequenta. Creiamo un pacchetto modello . Sulla base di ciò, creeremo un modello semplice:
Ora creiamo un mappatore che tradurrà una raccolta di modelli di lezioni in una raccolta di lezioni DTO. La prima cosa da fare è aggiungere Mapstruct al progetto. Per fare ciò, utilizzeremo il loro sito ufficiale , lì è tutto descritto. Cioè, dobbiamo aggiungere una dipendenza e un plugin alla nostra memoria (se hai domande su cos'è una memoria, ecco qui, Articolo1 e Articolo2 ):
Successivamente, creiamo un pacchetto mapper accanto a dto e model . In base alle classi che abbiamo mostrato in precedenza, dovrai creare altri cinque mappatori:
Va notato separatamente che in mappatori ci riferiamo ad altri mappatori. Questo viene fatto attraverso il campo uses nell'annotazione Mapper, come avviene in StudentMapper:
Qui utilizziamo due mappatori per mappare correttamente l'elenco delle lezioni e l'elenco dei docenti. Ora dobbiamo compilare il nostro codice e vedere cosa c'è e come. Questo può essere fatto usando il comando mvn clean compile . Ma, come si è scoperto, durante la creazione delle implementazioni Mapstruct dei nostri mapper, le implementazioni del mapper non sovrascrivevano i campi. Perché? Si è scoperto che non era possibile ritirare l'annotazione dei dati da Lombok. E qualcosa andava fatto... Pertanto, abbiamo una nuova sezione nell'articolo.
Collegamento di Lombok e Mapstruct
Dopo alcuni minuti di ricerca, si è scoperto che dovevamo connettere Lombok e Mapstruct in un certo modo. Ci sono informazioni a riguardo nella documentazione di Mapstruct . Dopo aver esaminato l'esempio proposto dagli sviluppatori di Mapstruct, aggiorniamo il nostro pom.xml: aggiungiamo versioni separate:
Dopodiché tutto dovrebbe funzionare. Compiliamo di nuovo il nostro progetto. Ma dove puoi trovare le classi generate da Mapstruct? Sono in generate-sources: ${projectDir}/target/generated-sources/annotations/ Ora che siamo pronti a realizzare la mia delusione per il post su Mapstruct, proviamo a creare test per i mappatori.
Scriviamo test per i nostri mappatori
Creerò un test rapido e semplice che testerà uno dei mappatori nel caso in cui stiamo creando un test di integrazione e non mi preoccuperò del tempo di completamento:
Qui, utilizzando l'annotazione SpringBootTest, lanciamo l'intero applicationContext e da esso, utilizzando l'annotazione Autowired, estraiamo la classe che ci serve per il test. Dal punto di vista della velocità e della facilità di scrittura del test, questo è molto buono. Il test è stato superato con successo, va tutto bene. Ma andremo dall'altra parte e scriveremo un test unitario per un mappatore, ad esempio LectureListMapper...
Poiché le implementazioni generate da Mapstruct sono nella stessa classe del nostro progetto, possiamo usarle facilmente nei nostri test. Sembra tutto fantastico: nessuna annotazione, creiamo la classe di cui abbiamo bisogno nel modo più semplice e basta. Ma quando eseguiremo il test, capiremo che andrà in crash e ci sarà una NullPointerException nella console... Questo perché l'implementazione del mapper LectureListMapper è simile a:
Se guardiamo l'NPE (abbreviazione di NullPointerException), lo otteniamo dalla variabile lectureMapper , che risulta non essere inizializzata. Ma nella nostra implementazione non abbiamo un costruttore con cui inizializzare la variabile. Questo è esattamente il motivo per cui Mapstruct ha implementato il mapper in questo modo! In Spring, puoi aggiungere bean alle classi in diversi modi, puoi inserirli tramite un campo insieme all'annotazione Autowired, come fatto sopra, oppure puoi inserirli tramite un costruttore. Mi sono trovato in una situazione così problematica sul lavoro quando avevo bisogno di ottimizzare i tempi di esecuzione dei test. Pensavo che non si potesse fare nulla e ho sfogato il mio dolore sul mio canale Telegram. E poi mi hanno aiutato nei commenti e hanno detto che era possibile personalizzare la strategia di iniezione. L'interfaccia Mapper ha un campo injectionStrategy , che accetta semplicemente il nome InjectionStrategy , che ha due valori: FIELD e CONSTRUCTOR . Ora, sapendo questo, aggiungiamo questa impostazione ai nostri mapper; la mostrerò usando LectureListMapper come esempio :
Ho evidenziato in grassetto la parte che ho aggiunto. Aggiungiamo questa opzione per tutte le altre e ricompiliamo il progetto in modo che i mapper vengano generati con una nuova riga. Fatto ciò, vediamo come è cambiata l'implementazione del mapper per LectureListMapper (evidenziata in grassetto la parte di cui abbiamo bisogno):
E ora Mapstruct ha implementato l'iniezione del mapper attraverso il costruttore. Questo è esattamente ciò che stavamo cercando di ottenere. Ora il nostro test smetterà di compilare, aggiorniamolo e otteniamo:
Ora, se eseguiamo il test, tutto funzionerà come previsto, poiché in LectureListMapperImpl superiamo il LectureMapper di cui ha bisogno... Vittoria! Per voi non è difficile, ma sono contento: amici, è tutto come al solito, iscrivetevi al mio account GitHub , al mio account Telegram . Lì pubblico i risultati delle mie attività, ci sono cose davvero utili) Ti invito in particolare a partecipare al gruppo di discussione del canale Telegram . Accade così che se qualcuno ha una domanda tecnica, può ottenere una risposta lì. Questo formato è interessante per tutti, puoi leggere chissà cosa e fare esperienza.
Conclusione
Nell'ambito di questo articolo, abbiamo conosciuto un prodotto così necessario e utilizzato di frequente come Mapstruct. Abbiamo capito di cosa si tratta, perché e come. Utilizzando un esempio reale, abbiamo capito cosa si potrebbe fare e come si potrebbe cambiare. Abbiamo anche visto come impostare l'iniezione dei bean attraverso il costruttore, in modo che sia possibile testare adeguatamente i mapper. I colleghi di Mapstruct hanno permesso agli utenti del loro prodotto di scegliere esattamente come iniettare i mapper, cosa per la quale li ringraziamo senza dubbio. MA, nonostante Spring consigli di iniettare i bean attraverso il costruttore, i ragazzi di Mapstruct hanno impostato l'iniezione attraverso il campo per impostazione predefinita. Perché? Nessuna risposta. Sospetto che ci possano essere ragioni che semplicemente non conosciamo, ed è per questo che hanno fatto in questo modo. E per scoprirlo, ho creato un numero di GitHub sul loro repository ufficiale dei prodotti.
GO TO FULL VERSION