Top 10 cele mai frecvente greșeli ale cadrului de primăvară

Publicat: 2022-03-11

Spring este, fără îndoială, unul dintre cele mai populare cadre Java și, de asemenea, o fiară puternică de îmblânzit. În timp ce conceptele sale de bază sunt destul de ușor de înțeles, a deveni un dezvoltator puternic Spring necesită ceva timp și efort.

În acest articol vom acoperi unele dintre cele mai frecvente greșeli din Spring, orientate în mod special către aplicațiile web și Spring Boot. După cum afirmă site-ul web Spring Boot, Spring Boot are o viziune cu părere despre modul în care ar trebui să fie construite aplicațiile pregătite pentru producție, așa că acest articol va încerca să imite această viziune și să ofere o prezentare generală a unor sfaturi care se vor încorpora bine în dezvoltarea aplicației web standard Spring Boot.

În cazul în care nu sunteți foarte familiarizat cu Spring Boot, dar doriți totuși să încercați unele dintre lucrurile menționate, am creat un depozit GitHub care însoțește acest articol. Dacă vă simțiți pierdut în orice moment al articolului, vă recomand să clonați depozitul și să vă jucați cu codul de pe mașina dvs. locală.

Greșeala obișnuită #1: Mergeți la un nivel prea scăzut

Lovim cu această greșeală comună, deoarece sindromul „nu a fost inventat aici” este destul de comun în lumea dezvoltării de software. Simptomele care includ rescrierea regulată a bucăților de cod utilizate în mod obișnuit și o mulțime de dezvoltatori par să sufere din cauza asta.

În timp ce înțelegerea elementelor interne ale unei anumite biblioteci și implementarea acesteia este în cea mai mare parte bună și necesară (și poate fi, de asemenea, un proces de învățare excelent), este dăunător dezvoltării dvs. ca inginer software să abordați în mod constant aceeași implementare de nivel scăzut. Detalii. Există un motiv pentru care există abstracții și cadre precum Spring, care este tocmai pentru a vă separa de munca manuală repetitivă și pentru a vă permite să vă concentrați asupra detaliilor de nivel superior – obiectele domeniului și logica de afaceri.

Deci, îmbrățișați abstracțiile - data viitoare când vă confruntați cu o anumită problemă, faceți mai întâi o căutare rapidă și determinați dacă o bibliotecă care rezolvă problema respectivă este deja integrată în Spring; în zilele noastre, sunt șanse să găsiți o soluție existentă potrivită. Ca exemplu de bibliotecă utilă, voi folosi adnotările Project Lombok în exemple pentru restul acestui articol. Lombok este folosit ca un generator de cod standard, iar dezvoltatorul leneș din tine, sperăm că nu ar trebui să aibă probleme în a te familiariza cu biblioteca. De exemplu, vedeți cum arată un „bean standard Java” cu Lombok:

 @Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }

După cum vă puteți imagina, codul de mai sus se compilează în:

 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() { } }

Rețineți, totuși, că cel mai probabil va trebui să instalați un plugin în cazul în care intenționați să utilizați Lombok cu IDE-ul dvs. Versiunea IntelliJ IDEA a pluginului poate fi găsită aici.

Greșeala obișnuită nr. 2: „Scurgeri” la interior

Expunerea structurii tale interne nu este niciodată o idee bună, deoarece creează inflexibilitate în proiectarea serviciilor și, în consecință, promovează practici proaste de codare. „Scurgerile” interne se manifestă prin faptul că structura bazei de date este accesibilă de la anumite puncte finale API. Ca exemplu, să presupunem că următorul POJO („Plain Old Java Object”) reprezintă un tabel din baza de date:

 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }

Să presupunem că există un punct final care trebuie să acceseze datele TopTalentEntity . Oricât de tentant ar fi să returnezi instanțe TopTalentEntity , o soluție mai flexibilă ar fi crearea unei noi clase care să reprezinte datele TopTalentEntity pe punctul final API:

 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }

În acest fel, efectuarea de modificări în back-end-ul bazei de date nu va necesita nicio modificare suplimentară în nivelul de serviciu. Luați în considerare ce s-ar întâmpla în cazul adăugării unui câmp „parolă” la TopTalentEntity pentru stocarea hash-urilor parolei utilizatorilor dvs. în baza de date - fără un conector precum TopTalentData , uitarea de a schimba front-end-ul serviciului ar expune accidental câteva informații secrete foarte nedorite. !

Greșeala comună nr. 3: lipsa separării preocupărilor

Pe măsură ce aplicația dvs. crește, organizarea codului începe să devină din ce în ce mai importantă. În mod ironic, majoritatea principiilor bune de inginerie software încep să se defecteze la scară – mai ales în cazurile în care nu s-a acordat prea multă atenție designului arhitecturii aplicației. Una dintre cele mai frecvente greșeli la care dezvoltatorii tind să cedeze este amestecarea preocupărilor legate de cod și este extrem de ușor de făcut!

Ceea ce de obicei întrerupe separarea preocupărilor este doar „descărcarea” de noi funcționalități în clasele existente. Aceasta este, desigur, o soluție grozavă pe termen scurt (pentru început, necesită mai puțină tastare), dar inevitabil devine o problemă mai departe, fie în timpul testării, întreținerii sau undeva la mijloc. Luați în considerare următorul controler, care returnează TopTalentData din depozitul său:

 @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()); } }

La început, s-ar putea să nu pară că este ceva deosebit de greșit cu această bucată de cod; oferă o listă de TopTalentData care este preluată din instanțe TopTalentEntity . Aruncând o privire mai atentă, totuși, putem vedea că există de fapt câteva lucruri pe care TopTalentController efectuează aici; și anume, maparea cererilor către un anumit punct final, preluarea datelor dintr-un depozit și conversia entităților primite de la TopTalentRepository într-un format diferit. O soluție „mai curată” ar fi separarea acestor preocupări în propriile lor clase. Ar putea arăta cam așa:

 @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 avantaj suplimentar al acestei ierarhii este că ne permite să stabilim unde se află funcționalitatea doar prin inspectarea numelui clasei. În plus, în timpul testării, putem înlocui cu ușurință oricare dintre clase cu o implementare simulată, dacă este nevoie.

Greșeala comună #4: Incoerență și gestionarea slabă a erorilor

Subiectul consecvenței nu este neapărat exclusiv pentru Spring (sau Java, de altfel), dar este totuși o fațetă importantă de luat în considerare atunci când lucrați la proiecte Spring. În timp ce stilul de codificare poate fi dezbătut (și este de obicei o chestiune de acord în cadrul unei echipe sau în cadrul unei întregi companii), a avea un standard comun se dovedește a fi un mare ajutor pentru productivitate. Acest lucru este valabil mai ales în cazul echipelor cu mai multe persoane; consecvența permite transferul să aibă loc fără ca multe resurse să fie cheltuite pentru a ține mâna sau pentru a oferi explicații îndelungate cu privire la responsabilitățile diferitelor clase

Luați în considerare un proiect Spring cu diferitele sale fișiere de configurare, servicii și controlere. Fiind consecvenți din punct de vedere semantic în denumirea lor, creează o structură ușor de căutat în care orice dezvoltator nou își poate gestiona drumul în jurul codului; adăugând sufixe de configurare la clasele dvs. de configurare, sufixe de serviciu la serviciile dvs. și sufixe de controler la controlerele dvs., de exemplu.

Strâns legată de subiectul coerenței, gestionarea erorilor pe partea serverului merită un accent special. Dacă ați trebuit vreodată să gestionați răspunsurile de excepție de la un API prost scris, probabil că știți de ce – poate fi dificil să analizați corect excepțiile și chiar mai dureros să determinați motivul pentru care au apărut aceste excepții în primul rând.

În calitate de dezvoltator API, în mod ideal ați dori să acoperiți toate punctele finale orientate spre utilizator și să le traduceți într-un format de eroare comun. Acest lucru înseamnă, de obicei, să aveți un cod de eroare generic și o descriere, mai degrabă decât o soluție de reținere: a) returnarea unui mesaj „500 Internal Server Error” sau b) pur și simplu descărcarea urmei stivei către utilizator (ceea ce ar trebui de fapt evitat cu orice preț). deoarece vă expune elementele interne pe lângă faptul că este dificil de gestionat din partea clientului).

Un exemplu de format comun de răspuns la eroare ar putea fi:

 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }

Ceva similar cu acesta se întâlnește frecvent în cele mai populare API-uri și tinde să funcționeze bine, deoarece poate fi documentat ușor și sistematic. Traducerea excepțiilor în acest format se poate face prin furnizarea adnotării @ExceptionHandler unei metode (un exemplu de adnotare este în Common Mistake #6).

Greșeala obișnuită nr. 5: Abordarea incorect cu Multithreading

Indiferent dacă este întâlnit în aplicații desktop sau web, Spring sau nu Spring, multithreading poate fi o nucă greu de spart. Problemele cauzate de execuția paralelă a programelor sunt îngrozitor de evazive și de multe ori extrem de dificil de depanat - de fapt, din cauza naturii problemei, odată ce vă dați seama că aveți de-a face cu o problemă de execuție paralelă, probabil o veți face trebuie să renunțați complet la depanator și să vă inspectați codul „de mână” până când găsiți cauza erorii de bază. Din păcate, nu există o soluție de tip cookie-cutter pentru a rezolva astfel de probleme; în funcție de cazul dvs. specific, va trebui să evaluați situația și apoi să atacați problema din unghiul pe care îl considerați cel mai bun.

În mod ideal, desigur, ați dori să evitați cu totul erorile multithreading. Din nou, o abordare universală nu există pentru a face acest lucru, dar iată câteva considerații practice pentru depanare și prevenirea erorilor multithreading:

Evitați starea globală

În primul rând, amintiți-vă întotdeauna problema „statul global”. Dacă creați o aplicație cu mai multe fire, absolut orice lucru care este modificabil la nivel global ar trebui monitorizat îndeaproape și, dacă este posibil, eliminat cu totul. Dacă există un motiv pentru care variabila globală trebuie să rămână modificabilă, utilizați cu atenție sincronizarea și urmăriți performanța aplicației dvs. pentru a confirma că nu este lenta din cauza perioadelor de așteptare recent introduse.

Evitați mutabilitatea

Acesta provine direct din programarea funcțională și, adaptat la OOP, afirmă că trebuie evitată mutabilitatea clasei și schimbarea stării. Acest lucru, pe scurt, înseamnă să renunțați la metodele de setare și să aveți câmpuri finale private pe toate clasele dvs. de model. Singura dată când valorile lor sunt modificate este în timpul construcției. În acest fel, puteți fi sigur că nu apar probleme de dispută și că accesarea proprietăților obiectului va oferi valorile corecte în orice moment.

Înregistrați date cruciale

Evaluați unde aplicația dvs. poate cauza probleme și înregistrați preventiv toate datele esențiale. Dacă apare o eroare, veți fi recunoscători să aveți informații care indică ce solicitări au fost primite și veți avea o perspectivă mai bună asupra motivului pentru care aplicația dvs. s-a comportat incorect. Este din nou necesar să rețineți că înregistrarea în jurnal introduce I/O de fișiere suplimentare și, prin urmare, nu ar trebui să fie abuzată, deoarece poate afecta grav performanța aplicației dvs.

Reutilizați implementările existente

Ori de câte ori aveți nevoie să vă generați propriile fire de execuție (de exemplu, pentru a face cereri asincrone către diferite servicii), reutilizați implementările sigure existente, mai degrabă decât să vă creați propriile soluții. Acest lucru va însemna, în cea mai mare parte, utilizarea ExecutorServices și CompletableFutures în stil funcțional de la Java 8 pentru crearea de fire. Spring permite, de asemenea, procesarea asincronă a cererilor prin clasa DeferredResult.

Greșeala comună #6: Nu folosiți validarea bazată pe adnotări

Să ne imaginăm că serviciul nostru TopTalent de mai devreme necesită un punct final pentru adăugarea de noi Top Talents. În plus, să spunem că, dintr-un motiv cu adevărat valabil, fiecare nume nou trebuie să aibă exact 10 caractere. O modalitate de a face acest lucru ar putea fi următoarea:

 @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); }

Cu toate acestea, cele de mai sus (pe lângă faptul că sunt construite prost) nu sunt cu adevărat o soluție „curată”. Verificăm mai mult de un tip de validitate (și anume, că TopTalentData nu este nul șiTopTalentData.name nu este nul șiTopTalentData.name are 10 caractere), precum și lansăm o excepție dacă datele sunt invalide .

Acest lucru poate fi executat mult mai curat utilizând validatorul Hibernate cu Spring. Să refactorizăm mai întâi metoda addTopTalent pentru a sprijini validarea:

 @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 }

În plus, va trebui să indicăm ce proprietate dorim să validăm în clasa TopTalentData :

 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }

Acum Spring va intercepta cererea și o va valida înainte ca metoda să fie invocată - nu este nevoie să folosiți teste manuale suplimentare.

Un alt mod în care am fi putut realiza același lucru este prin crearea propriilor adnotări. Deși de obicei veți folosi adnotări personalizate numai atunci când nevoile dvs. depășesc setul de constrângeri încorporat din Hibernate, pentru acest exemplu, să presupunem că @Length nu există. Ați face un validator care verifică lungimea șirului prin crearea a două clase suplimentare, una pentru validare și alta pentru adnotarea proprietăților:

 @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; } }

Rețineți că, în aceste cazuri, cele mai bune practici privind separarea preocupărilor necesită să marcați o proprietate ca validă dacă este nulă ( s == null în cadrul metodei isValid ), apoi să utilizați o adnotare @NotNull dacă aceasta este o cerință suplimentară pentru proprietate:

 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }

Greșeala comună #7: (încă) Utilizarea unei configurații bazate pe XML

În timp ce XML a fost o necesitate pentru versiunile anterioare de Spring, în prezent cea mai mare parte a configurației se poate face exclusiv prin intermediul codului / adnotărilor Java; Configurațiile XML prezintă doar un cod suplimentar și inutil.

Acest articol (precum și depozitul său GitHub însoțitor) utilizează adnotări pentru configurarea Spring și Spring știe ce bean-uri ar trebui să fie conectat, deoarece pachetul rădăcină a fost adnotat cu o adnotare compusă @SpringBootApplication , astfel:

 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

Adnotarea compozită (puteți afla mai multe despre aceasta în documentația Spring îi oferă pur și simplu lui Spring un indiciu despre pachetele care ar trebui scanate pentru a recupera boabele de fasole. În cazul nostru concret, aceasta înseamnă că următoarele sub pachetul de sus (co.kukurin) vor fi folosite. pentru cablare:

  • @Component ( TopTalentConverter , MyAnnotationValidator )
  • @RestController ( TopTalentController )
  • @Repository ( TopTalentRepository )
  • @Service ( TopTalentService ).

Dacă am avea clase suplimentare adnotate @Configuration , acestea ar fi verificate și pentru configurația bazată pe Java.

Greșeala comună #8: Uitarea de profiluri

O problemă întâlnită adesea în dezvoltarea serverului este diferența dintre diferitele tipuri de configurații, de obicei configurațiile de producție și dezvoltare. În loc să înlocuiți manual diferite intrări de configurare de fiecare dată când treceți de la testare la implementarea aplicației dvs., o modalitate mai eficientă ar fi să folosiți profiluri.

Luați în considerare cazul în care utilizați o bază de date în memorie pentru dezvoltare locală, cu o bază de date MySQL în producție. Acest lucru ar însemna, în esență, că veți folosi o adresă URL diferită și (sperăm) acreditări diferite pentru a accesa fiecare dintre cele două. Să vedem cum se poate face acest lucru două fișiere de configurare diferite:

fisierul 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:

fișier application-dev.yaml

 spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2

Probabil că nu ați dori să efectuați accidental nicio acțiune în baza de date de producție în timp ce modificați codul, așa că este logic să setați profilul implicit la dev. Pe server, puteți apoi să suprascrieți manual profilul de configurare furnizând un parametru -Dspring.profiles.active=prod JVM-ului. Alternativ, puteți seta și variabila de mediu a sistemului de operare la profilul implicit dorit.

Greșeala obișnuită #9: Eșecul de a accepta injectarea dependenței

Utilizarea corectă a injecției de dependență cu Spring înseamnă să îi permiteți să vă conecteze toate obiectele împreună prin scanarea tuturor claselor de configurare dorite; acest lucru se dovedește a fi util pentru decuplarea relațiilor și, de asemenea, face testarea mult mai ușoară. În loc de clase de cuplare strânsă, făcând ceva de genul acesta:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }

Îi permitem lui Spring să facă cablajul pentru noi:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }

Discuția lui Misko Hevery pe Google explică în profunzime „de ce” injectării dependenței, așa că, în schimb, să vedem cum este folosită în practică. În secțiunea despre separarea preocupărilor (Greșeli comune #3), am creat o clasă de service și controler. Să presupunem că vrem să testăm controlerul presupunând că TopTalentService se comportă corect. Putem insera un obiect simulat în locul implementării efective a serviciului furnizând o clasă de configurare separată:

 @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; } }

Apoi putem injecta obiectul simulat spunându-i lui Spring să folosească SampleUnitTestConfig ca furnizor de configurație:

 @ContextConfiguration(classes = { SampleUnitTestConfig.class })

Acest lucru ne permite apoi să folosim configurația contextului pentru a injecta bean-ul personalizat într-un test unitar.

Greșeala comună #10: Lipsa testării sau testarea necorespunzătoare

Chiar dacă ideea testării unitare este cu noi de mult timp, mulți dezvoltatori par să fie „uită” să facă acest lucru (mai ales dacă nu este necesar ), fie pur și simplu o adaugă ca o idee ulterioară. Acest lucru nu este, evident, de dorit, deoarece testele ar trebui nu numai să verifice corectitudinea codului dvs., ci și să servească drept documentație despre modul în care aplicația ar trebui să se comporte în diferite situații.

Când testați serviciile web, rareori faceți teste unitare exclusiv „pure”, deoarece comunicarea prin HTTP necesită, de obicei, să invocați DispatcherServlet -ul Spring și să vedeți ce se întâmplă când este primită o HttpServletRequest (făcându-l un test de integrare , care se ocupă de validare, serializare). , etc). REST Assured, un DSL Java pentru testarea ușoară a serviciilor REST, pe lângă MockMVC, s-a dovedit a oferi o soluție foarte elegantă. Luați în considerare următorul fragment de cod cu injecție de dependență:

 @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 o implementare simulată a TopTalentService în TopTalentController , în timp ce toate celelalte clase sunt conectate folosind configurația standard dedusă din pachetele de scanare înrădăcinate în pachetul Clasei de aplicații. RestAssuredMockMvc este folosit pur și simplu pentru a configura un mediu ușor și a trimite o solicitare GET către punctul final /toptal/get .

A deveni un maestru de primăvară

Spring este un cadru puternic cu care este ușor de început, dar necesită ceva dedicare și timp pentru a obține o stăpânire deplină. Alocarea timpului pentru a vă familiariza cu cadrul va îmbunătăți cu siguranță productivitatea pe termen lung și, în cele din urmă, vă va ajuta să scrieți cod mai curat și să deveniți un dezvoltator mai bun.

Dacă sunteți în căutarea altor resurse, Spring In Action este o carte practică bună, care acoperă multe subiecte principale de primăvară.