I 10 errori più comuni del Framework di primavera
Pubblicato: 2022-03-11La primavera è probabilmente uno dei framework Java più popolari e anche una potente bestia da domare. Sebbene i suoi concetti di base siano abbastanza facili da comprendere, diventare un forte sviluppatore Spring richiede tempo e fatica.
In questo articolo tratteremo alcuni degli errori più comuni in Spring, specificamente orientati verso le applicazioni web e Spring Boot. Come afferma il sito Web di Spring Boot, Spring Boot ha una visione obiettiva su come dovrebbero essere costruite le applicazioni pronte per la produzione, quindi questo articolo cercherà di imitare quella vista e fornire una panoramica di alcuni suggerimenti che si incorporeranno bene nello sviluppo di applicazioni Web Spring Boot standard.
Nel caso in cui tu non abbia molta familiarità con Spring Boot ma desideri comunque provare alcune delle cose menzionate, ho creato un repository GitHub che accompagna questo articolo. Se ti senti perso in qualsiasi momento durante l'articolo, ti consiglio di clonare il repository e giocare con il codice sul tuo computer locale.
Errore comune n. 1: andare a un livello troppo basso
Stiamo andando d'accordo con questo errore comune perché la sindrome del "non inventato qui" è abbastanza comune nel mondo dello sviluppo software. I sintomi includono la riscrittura regolare di pezzi di codice comunemente usati e molti sviluppatori sembrano soffrirne.
Sebbene la comprensione degli interni di una particolare libreria e della sua implementazione sia per la maggior parte buona e necessaria (e può anche essere un ottimo processo di apprendimento), è dannoso per il tuo sviluppo come ingegnere del software affrontare costantemente la stessa implementazione di basso livello dettagli. C'è un motivo per cui esistono astrazioni e framework come Spring, che è proprio quello di separarti dal lavoro manuale ripetitivo e permetterti di concentrarti su dettagli di livello superiore: i tuoi oggetti di dominio e la logica aziendale.
Quindi abbraccia le astrazioni: la prossima volta che ti trovi di fronte a un problema particolare, fai prima una rapida ricerca e determina se una libreria che risolve quel problema è già integrata in Spring; al giorno d'oggi, è probabile che troverai una soluzione esistente adatta. Come esempio di una libreria utile, userò le annotazioni di Project Lombok negli esempi per il resto di questo articolo. Lombok è usato come generatore di codice standard e lo sviluppatore pigro dentro di te si spera non dovrebbe avere problemi a familiarizzare con la libreria. Ad esempio, controlla come appare un "bean Java standard" con Lombok:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }Come puoi immaginare, il codice sopra viene compilato in:
public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }Tieni presente, tuttavia, che molto probabilmente dovrai installare un plug-in nel caso in cui intendi utilizzare Lombok con il tuo IDE. La versione del plugin di IntelliJ IDEA può essere trovata qui.
Errore comune n. 2: interni che "perdono".
Esporre la propria struttura interna non è mai una buona idea perché crea rigidità nella progettazione dei servizi e di conseguenza promuove cattive pratiche di codifica. Gli interni "perdite" si manifestano rendendo la struttura del database accessibile da determinati endpoint API. Ad esempio, supponiamo che il seguente POJO ("Plain Old Java Object") rappresenti una tabella nel database:
@Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } } Supponiamo che esista un endpoint che deve accedere ai dati di TopTalentEntity . Per quanto allettante possa essere restituire istanze TopTalentEntity , una soluzione più flessibile sarebbe la creazione di una nuova classe per rappresentare i dati TopTalentEntity sull'endpoint API:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; } In questo modo, apportare modifiche al back-end del database non richiederà ulteriori modifiche al livello di servizio. Considera cosa accadrebbe se aggiungessi un campo "password" a TopTalentEntity per memorizzare gli hash delle password dei tuoi utenti nel database: senza un connettore come TopTalentData , dimenticare di modificare il front-end del servizio esporrebbe accidentalmente alcune informazioni segrete molto indesiderabili !
Errore comune n. 3: mancanza di separazione delle preoccupazioni
Man mano che la tua applicazione cresce, l'organizzazione del codice inizia a diventare sempre più una questione sempre più importante. Ironia della sorte, la maggior parte dei buoni principi di ingegneria del software iniziano a sgretolarsi su larga scala, specialmente nei casi in cui non si è prestata molta attenzione alla progettazione dell'architettura dell'applicazione. Uno degli errori più comuni a cui gli sviluppatori tendono a soccombere è quello di mescolare problemi di codice, ed è estremamente facile da fare!
Ciò che di solito interrompe la separazione delle preoccupazioni è semplicemente "scaricare" le nuove funzionalità nelle classi esistenti. Questa è, ovviamente, un'ottima soluzione a breve termine (per cominciare, richiede meno digitazione), ma diventa inevitabilmente un problema più avanti, durante i test, la manutenzione o una via di mezzo. Considera il seguente controller, che restituisce TopTalentData dal suo repository:
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } } All'inizio, potrebbe non sembrare che ci sia qualcosa di particolarmente sbagliato in questo pezzo di codice; fornisce un elenco di TopTalentData che viene recuperato dalle istanze di TopTalentEntity . Dando un'occhiata più da vicino, tuttavia, possiamo vedere che in realtà ci sono alcune cose che TopTalentController sta eseguendo qui; vale a dire, sta mappando le richieste a un particolare endpoint, recuperando dati da un repository e convertendo le entità ricevute da TopTalentRepository in un formato diverso. Una soluzione "più pulita" sarebbe separare quelle preoccupazioni nelle loro classi. Potrebbe assomigliare a questo:
@RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }Un ulteriore vantaggio di questa gerarchia è che ci consente di determinare dove risiede la funzionalità semplicemente esaminando il nome della classe. Inoltre, durante i test possiamo facilmente sostituire una qualsiasi delle classi con un'implementazione simulata, se necessario.
Errore comune n. 4: incoerenza e cattiva gestione degli errori
Il tema della coerenza non è necessariamente esclusivo di Spring (o Java, se è per questo), ma è comunque un aspetto importante da considerare quando si lavora su progetti Spring. Mentre lo stile di codifica può essere oggetto di dibattito (e di solito è una questione di accordo all'interno di un team o all'interno di un'intera azienda), avere uno standard comune si rivela un grande aiuto alla produttività. Ciò è particolarmente vero con i team composti da più persone; la coerenza consente che il trasferimento avvenga senza che molte risorse vengano spese per tenersi per mano o fornire lunghe spiegazioni sulle responsabilità delle diverse classi
Si consideri un progetto Spring con i suoi vari file di configurazione, servizi e controller. Essere semanticamente coerenti nel nominarli crea una struttura facilmente ricercabile in cui qualsiasi nuovo sviluppatore può gestire il codice; ad esempio aggiungendo suffissi Config alle tue classi di configurazione, suffissi Service ai tuoi servizi e suffissi Controller ai tuoi controller.
Strettamente correlato al tema della coerenza, la gestione degli errori lato server merita un'enfasi specifica. Se hai mai dovuto gestire le risposte alle eccezioni da un'API scritta male, probabilmente sai perché: può essere difficile analizzare correttamente le eccezioni e ancora più doloroso determinare il motivo per cui tali eccezioni si sono verificate in primo luogo.
Come sviluppatore di API, idealmente vorresti coprire tutti gli endpoint rivolti agli utenti e tradurli in un formato di errore comune. Questo di solito significa avere un codice di errore generico e una descrizione piuttosto che la soluzione di cop-out di a) restituire un messaggio "500 Internal Server Error" o b) semplicemente scaricare la traccia dello stack all'utente (che in realtà dovrebbe essere evitato a tutti i costi poiché espone i tuoi interni oltre ad essere difficile da gestire lato client).
Un esempio di un comune formato di risposta agli errori potrebbe essere:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; } Qualcosa di simile si incontra comunemente nelle API più popolari e tende a funzionare bene poiché può essere facilmente e sistematicamente documentato. La traduzione delle eccezioni in questo formato può essere eseguita fornendo l'annotazione @ExceptionHandler a un metodo (un esempio di annotazione è in Errore comune n. 6).
Errore comune n. 5: gestire in modo improprio il multithreading
Indipendentemente dal fatto che si incontri nelle app desktop o web, Spring o no Spring, il multithreading può essere un osso duro da decifrare. I problemi causati dall'esecuzione parallela di programmi sono elusivi snervanti e spesso estremamente difficili da eseguire il debug - infatti, a causa della natura del problema, una volta che ti rendi conto che stai affrontando un problema di esecuzione parallela, probabilmente lo farai devi rinunciare completamente al debugger e ispezionare il tuo codice "a mano" fino a trovare la causa dell'errore di root. Sfortunatamente, non esiste una soluzione di cookie cutter per risolvere tali problemi; a seconda del tuo caso specifico, dovrai valutare la situazione e quindi affrontare il problema dall'angolazione che ritieni sia migliore.
Idealmente, ovviamente, vorresti evitare del tutto i bug del multithreading. Ancora una volta, non esiste un approccio valido per tutti per farlo, ma ecco alcune considerazioni pratiche per il debug e la prevenzione degli errori di multithreading:
Evita lo stato globale
Innanzitutto, ricorda sempre la questione dello "stato globale". Se stai creando un'applicazione multithread, tutto ciò che è modificabile a livello globale dovrebbe essere attentamente monitorato e, se possibile, rimosso del tutto. Se c'è un motivo per cui la variabile globale deve rimanere modificabile, utilizza con attenzione la sincronizzazione e tieni traccia delle prestazioni dell'applicazione per confermare che non sia lenta a causa dei periodi di attesa appena introdotti.
Evita la mutevolezza
Questo deriva direttamente dalla programmazione funzionale e, adattato all'OOP, afferma che la mutabilità di classe e il cambiamento di stato dovrebbero essere evitati. Questo, in breve, significa rinunciare ai metodi setter e avere campi finali privati su tutte le classi del modello. L'unico momento in cui i loro valori vengono modificati è durante la costruzione. In questo modo puoi essere certo che non si verifichino problemi di contesa e che l'accesso alle proprietà dell'oggetto fornirà sempre i valori corretti.

Registra dati cruciali
Valuta dove la tua applicazione potrebbe causare problemi e registra preventivamente tutti i dati cruciali. Se si verifica un errore, sarai grato di avere informazioni che indichino quali richieste sono state ricevute e di avere una migliore comprensione del motivo per cui la tua domanda si è comportata in modo anomalo. È ancora una volta necessario notare che la registrazione introduce I/O di file aggiuntivi e pertanto non dovrebbe essere abusata in quanto può influire gravemente sulle prestazioni dell'applicazione.
Riutilizza le implementazioni esistenti
Ogni volta che hai bisogno di generare i tuoi thread (ad esempio per fare richieste asincrone a servizi diversi), riutilizza le implementazioni sicure esistenti piuttosto che creare le tue soluzioni. Ciò, per la maggior parte, significherà l'utilizzo di ExecutorServices e CompletableFutures in stile funzionale di Java 8 per la creazione di thread. Spring consente anche l'elaborazione asincrona delle richieste tramite la classe DeferredResult.
Errore comune n. 6: non utilizzare la convalida basata su annotazioni
Immaginiamo che il nostro servizio TopTalent di prima richieda un endpoint per l'aggiunta di nuovi Top Talents. Inoltre, diciamo che, per qualche ragione davvero valida, ogni nuovo nome deve essere lungo esattamente 10 caratteri. Un modo per farlo potrebbe essere il seguente:
@RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); } Tuttavia, quanto sopra (oltre ad essere mal costruito) non è davvero una soluzione "pulita". Stiamo verificando più di un tipo di validità (vale a dire, che TopTalentData non sia null e che TopTalentData.name non sia null e che TopTalentData.name sia lungo 10 caratteri), oltre a lanciare un'eccezione se i dati non sono validi .
Questo può essere eseguito in modo molto più pulito utilizzando il validatore Hibernate con Spring. Per prima cosa eseguiamo il refactoring del metodo addTopTalent per supportare la convalida:
@RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception } Inoltre, dovremo indicare quale proprietà vogliamo convalidare nella classe TopTalentData :
public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }Ora Spring intercetterà la richiesta e la convaliderà prima che il metodo venga invocato: non è necessario utilizzare test manuali aggiuntivi.
Un altro modo in cui avremmo potuto ottenere la stessa cosa è creare le nostre annotazioni. Sebbene di solito utilizzi annotazioni personalizzate solo quando le tue esigenze superano il set di vincoli integrato di Hibernate, per questo esempio facciamo finta che @Length non esista. Dovresti creare un validatore che controlla la lunghezza della stringa creando due classi aggiuntive, una per la convalida e un'altra per le proprietà di annotazione:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } } Si noti che in questi casi, le migliori pratiche sulla separazione delle preoccupazioni richiedono di contrassegnare una proprietà come valida se è null ( s == null all'interno del metodo isValid ), quindi utilizzare un'annotazione @NotNull se questo è un requisito aggiuntivo per il proprietà:
public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }Errore comune n. 7: (ancora) utilizzando una configurazione basata su XML
Mentre XML era una necessità per le versioni precedenti di Spring, oggigiorno la maggior parte della configurazione può essere eseguita esclusivamente tramite codice / annotazioni Java; Le configurazioni XML rappresentano semplicemente un codice boilerplate aggiuntivo e non necessario.
Questo articolo (così come il relativo repository GitHub) utilizza le annotazioni per la configurazione di Spring e Spring sa quali bean dovrebbe cablare perché il pacchetto radice è stato annotato con un'annotazione composita @SpringBootApplication , in questo modo:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }L'annotazione composita (puoi saperne di più nella documentazione di Spring fornisce semplicemente a Spring un suggerimento su quali pacchetti devono essere scansionati per recuperare i bean. Nel nostro caso concreto, questo significa che verrà utilizzato il seguente pacchetto sotto il top (co.kukurin) per il cablaggio:
-
@Component(TopTalentConverter,MyAnnotationValidator) -
@RestController(TopTalentController) -
@Repository(TopTalentRepository) -
@Service(TopTalentService).
Se avessimo altre classi annotate da @Configuration , verrebbero verificate anche la configurazione basata su Java.
Errore comune n. 8: dimenticare i profili
Un problema spesso riscontrato nello sviluppo di server è distinguere tra diversi tipi di configurazione, in genere le configurazioni di produzione e sviluppo. Invece di sostituire manualmente varie voci di configurazione ogni volta che si passa dal test alla distribuzione dell'applicazione, un modo più efficiente sarebbe utilizzare i profili.
Si consideri il caso in cui si utilizza un database in memoria per lo sviluppo locale, con un database MySQL in produzione. Ciò significherebbe, in sostanza, che utilizzerai un URL diverso e (si spera) credenziali diverse per accedere a ciascuno dei due. Vediamo come questo potrebbe essere fatto due diversi file di configurazione:
file application.yaml
# set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:file application-dev.yaml
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2 Presumibilmente non vorrai eseguire accidentalmente alcuna azione sul tuo database di produzione mentre armeggi con il codice, quindi ha senso impostare il profilo predefinito su dev. Sul server, è quindi possibile sovrascrivere manualmente il profilo di configurazione fornendo un parametro -Dspring.profiles.active=prod alla JVM. In alternativa, puoi anche impostare la variabile di ambiente del tuo sistema operativo sul profilo predefinito desiderato.
Errore comune n. 9: non accettare l'iniezione di dipendenza
Usare correttamente l'iniezione delle dipendenze con Spring significa consentirgli di collegare tutti i tuoi oggetti insieme scansionando tutte le classi di configurazione desiderate; questo si rivela utile per disaccoppiare le relazioni e rende anche il test molto più semplice. Invece di classi di accoppiamento stretto facendo qualcosa del genere:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }Stiamo permettendo a Spring di fare il cablaggio per noi:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } } Il discorso di Google di Misko Hevery spiega in modo approfondito i "perché" dell'iniezione di dipendenza, quindi vediamo invece come viene utilizzato nella pratica. Nella sezione sulla separazione delle preoccupazioni (Errori comuni n. 3), abbiamo creato una classe di servizio e controller. Diciamo di voler testare il controller partendo dal presupposto che TopTalentService si comporti correttamente. Possiamo inserire un oggetto fittizio al posto dell'effettiva implementazione del servizio fornendo una classe di configurazione separata:
@Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } } Quindi possiamo iniettare l'oggetto fittizio dicendo a Spring di utilizzare SampleUnitTestConfig come fornitore di configurazione:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })Questo ci consente quindi di utilizzare la configurazione del contesto per iniettare il bean personalizzato in uno unit test.
Errore comune n. 10: mancanza di test o test impropri
Anche se l'idea dello unit test è con noi da molto tempo ormai, molti sviluppatori sembrano "dimenticare" di farlo (soprattutto se non è richiesto ), o semplicemente aggiungerlo come ripensamento. Questo ovviamente non è desiderabile poiché i test non dovrebbero solo verificare la correttezza del codice, ma anche servire come documentazione su come l'applicazione dovrebbe comportarsi in diverse situazioni.
Quando si testano i servizi Web, raramente si eseguono test unitari "puri", poiché la comunicazione su HTTP di solito richiede di invocare DispatcherServlet di Spring e vedere cosa succede quando viene ricevuto un HttpServletRequest effettivo (rendendolo un test di integrazione , occupandosi della convalida, della serializzazione , eccetera). REST Assured, un DSL Java per testare facilmente i servizi REST, oltre a MockMVC, ha dimostrato di fornire una soluzione molto elegante. Considera il seguente frammento di codice con iniezione di dipendenza:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } } SampleUnitTestConfig un'implementazione fittizia di TopTalentService in TopTalentController mentre tutte le altre classi sono cablate utilizzando la configurazione standard dedotta dalla scansione dei pacchetti radicati nel pacchetto della classe dell'applicazione. RestAssuredMockMvc viene semplicemente utilizzato per configurare un ambiente leggero e inviare una richiesta GET /toptal/get .
Diventare un maestro di primavera
La primavera è un framework potente con cui è facile iniziare ma richiede un po' di dedizione e tempo per raggiungere la piena padronanza. Prendersi del tempo per familiarizzare con il framework migliorerà sicuramente la tua produttività a lungo termine e alla fine ti aiuterà a scrivere codice più pulito e diventare uno sviluppatore migliore.
Se stai cercando ulteriori risorse, Spring In Action è un buon libro pratico che copre molti argomenti chiave di primavera.
