În spațiul dezvoltării moderne de software, Spring Boot a reprezentat un standard esențial în simplificarea modului în care gestionăm și instalăm microserviciile. Arhitectura bazată pe microservicii, caracterizată de scalabilitate și flexibilitate, le permite programatorilor să construiască și să întrețină/gestioneze fiecare serviciu, independent de celelalte servicii. Totuși, această descentralizare introduce o provocare - comunicarea între servicii și descoperirea serviciilor.
Comunicarea între microservicii este esențială pentru performanța generală a aplicației. Comunicarea poate fi sincronă atunci când un serviciu așteaptă un răspuns înainte de a merge mai departe. Comunicarea poate fi asincronă atunci când un serviciu merge mai departe fără a aștepta. Spring Boot oferă o serie de opțiuni pentru asigurarea comunicării între servicii, precum RestTemplate, WebClient și Spring Cloud Open Feign pentru comunicarea sincronă și messaging brokers (mediatori de mesaje) precum RabbitMQ și Apache Kafka pentru comunicarea asincronă.
Descoperirea este un alt aspect critic al arhitecturii bazate pe microservicii. Presupune identificarea locației fiecărei instanțe de microservicii în cadrul rețelei, aspect care se poate modifica dinamic în mediile bazate pe cloud. Spring Boot oferă o soluție acestei provocări prin implementarea de tipare (șabloane) de descoperire a serviciilor, ceea ce permite serviciilor să se găsească unele pe altele și să comunice unele cu altele fără impedimente. Instrumente, precum Netflix Eureka, pot fi integrate în aplicațiile Spring Boot pentru a facilita acest proces.
Un monolit este o aplicație mare instalată pe un server. Poate avea baze de cod diferite sau nu, cel mai adesea un repository stochează codul pentru întreaga aplicație.
Microserviciile pot fi înțelese drept aplicații multiple instalate pe un server. Fiecare aplicație are propriul repository, propriul cod și, frecvent, o bază de date asociată. Aplicațiile trebuie să comunice unele cu celelalte (prin apelări) pentru a genera o arhitectură bazată pe microservicii. Altfel, sunt doar aplicații independente (dezvoltate cu Spring Boot).
Comunicarea dintre microservicii poate fi realizată prin URL-uri hardcodate (modalitate naivă și nerecomandată) sau folosind un mecanism/un tipar de descoperire (precum un server Eureka). Microserviciile se descoperă unele pe celălalte și știu cum să se apeleze unele pe celălalte. În esență, URL-ul fiecărei apelări nu mai este hardcodat, dar mecanismul de descoperire a serviciilor este responsabil de construirea unei căi URL. Cel puțin pentru prefix, trebuie să furnizați calea relativă la serviciul pe care doriți să îl apelați, de exemplu, /users.
Scalabilitatea și modularitatea sunt două mari avantaje în folosirea microserviciilor.
Scalabilitatea se referă la posibilitatea de a avea instanțe multiple ale aceluiași microserviciu instalat. Se mai numește scalare pe verticală. În scenariul în care o aplicație se confruntă cu o încărcare mare, celelalte instanțe pot prelua din sarcină și să o distribuie.
Modularitatea se referă la faptul că aplicațiile (modulele care definesc arhitectura bazată pe servicii) pot fi instalate separat. Acesta reprezintă un aspect util major atunci când o modificare trebuie făcută unei bucăți specifice de cod deținute de un microserviciu. În acest caz, se va reinstala doar microserviciul afectat, în loc să se instaleze întreaga aplicație monolitică.
Dezavantajele sunt următoarele. În ceea ce privește scalabilitatea, trebuie să ne asigurăm că modulele pot scala, că putem avea mai multe copii ale aceluiași microserviciu fără a afecta funcționarea de ansamblu a aplicației. În ceea ce privește modularitatea, trebuie să ne asigurăm că procesul de release a funcționat și că toate modulele sunt instalate corect. În loc să instalați doar o aplicație, acum este posibil să fiți nevoiți să instalați mai multe.
În esență, am rezolvat niște probleme și am dat de altele noi.
În abordarea monolitică, problemele pe care doream să le rezolvăm erau foarte specifice aplicației de business. Eram mai preocupați de cum putem apela, de exemplu, serviciul /ratings din serviciul /movies și așa mai departe.
În lumea microserviciilor, setul de probleme este mai generic: de exemplu, load balancing (echilibrarea sarcinii). Indiferent de felul aplicației, dacă o spargi în microservicii, fiecare microserviciu individual face ceea ce trebuie să facă. Totuși, comunicarea dintre microservicii este o problemă comună, indiferent dacă realizați o aplicație de cumpărături sau o aplicație e-commerce. Dacă este o problemă comună, atunci unele tehnologii sau paradigme vor rezolva acea problemă.
Prin urmare, problemele pe care trebuie să le rezolvi pentru fiecare monolit se rezolvă separat. Trecând la microservicii, vei rezolva problemele domeniului microserviciilor în general. Le vei aplica mai multor feluri de microservicii, indiferent de domeniul aplicației. Trebuie să învățați mai multe tehnologii și tipare, dar obțineți un tipar, un șablon care funcționează și o tehnologie care funcționează, indiferent ce aplicații construiți.
În SOA punem accent pe construirea unui serviciu care este, de fapt, o utilitate, dar care nu știm unde va fi folosită, deoarece nu putem să construim serviciul cu o aplicație specifică în minte. Logica este următoarea: voi crea un serviciu, îl voi lăsa undeva și oricine vrea să îl folosească, îl poate folosi. Acest serviciu este construit pentru a putea fi refolosit.
În lumea microserviciilor, știi în mare care este aplicația la care vei contribui. Să zicem că noi construim următoarea aplicație Amazon, deci știm, de la bun început, că va fi o aplicație e-commerce pe care o vom sparge în microservicii. Știm că acel microserviciu specific va face exact ceea ce ne dorim să facă și ne simțim confortabil cu asta. Acest serviciu nu este construit pentru a putea fi refolosit. Ar putea fi refolosit, din moment ce orice microserviciu poate fi refolosit, dar nu este o necesitate. Nu este creat cu acest scop.
În acest articol, ne vom axa pe dezvoltarea unei arhitecturi bazate pe microservicii, compusă din 3 aplicații independente care vor comunica unele cu altele pentru a atinge niște obiective de business. Sistemul pe care am vrea să îl construim este unul care să evalueze și să ofere un scor filmelor, similar cu IMDB. Un utilizator poate partaja o listă de filme pe care le-a urmărit împreună cu un scor (rating) pentru fiecare film. Prezentăm mai jos cele 3 aplicații.
Pentru un utilizator dat, serviciul va afișa toate filmele pe care utilizatorul le-a urmărit și le-a evaluat. Se vor afișa următoarele detalii: numele filmului, descrierea filmului și scorul acordat de utilizator acelui film. Vom hardcoda informația și nu vom stabili conexiuni la baza de date, din moment ce depășește scopul pentru care sunt create microserviciile.
Exemplu:
mymoviecatalog.com/api/vlad
{
id: vlad
name: Vlad I
movies: [
{id: 1234, name: “test”, desc: “test”, rating: 3} {id: 5678, name: “test2”, desc: “test2”, rating: 4}
]
}
Acesta este un microserviciu care va fi responsabil de furnizarea informațiilor despre film. Este o aplicație separată, cu scop separat. Preia ID-ul unui film și returnează informație despre acel film. ID-ul filmului poate fi extras dintr-o bază de date sau poate fi hardcodat.
Acesta este un microserviciu care va stoca scorul filmelor. Pentru fiecare ID de utilizator, se va returna o listă de ID-uri de filme și scorurile lor.
Pentru a obține elementele descrise anterior, trebuie să construim 3 aplicații Spring Boot. Mai întâi, vom construi serviciul Movie Catalog și vom hardcoda niște elemente. Din moment ce acesta este microserviciul "principal", un programator Javascript poate, de exemplu, să apeleze acest API pentru a obține toate informațiile de care are nevoie, fără nevoia de a ști cum a fost agregată informația.
Apoi, construim serviciul Movie Info, iar apoi serviciul Ratings Data, cele 2 dependințe pe care se bazează serviciul Movie Catalog.
Ultimul pas este ca serviciul Catalog să apeleze celelalte 2 servicii (în variantă hardcodată, într-o variantă inițială care nu va ajunge în producție). Până când nu se apelează, acestea nu sunt microservicii, ci doar proiecte Spring Boot.
Primul pas este să creăm cele 3 aplicații Spring Boot. O aplicație Spring Boot se poate crea în mai multe modalități. Ne vom axa pe opțiunea de a utiliza interfața web http://start.spring.io, urmând pașii descriși acolo. Alte 2 modalități de a crea o aplicație Spring Boot sunt Maven combinat cu adăugarea manuală de dependințte în proiect sau utilizarea Spring CLI (Command Line Interface).
Mai jos puteți analiza configurația din interfața web, cea folosită pentru primul microserviciu, movie-catalog-service.
Am folosit Java (versiunea 17) ca limbaj și Maven pentru compilarea proiectului. Înainte de a da click pe acțiunea Generate project (care nu este vizibilă în imagine), trebuie să adăugăm o dependință de proiect. Fiind un proiect web, folosim REST pentru comunicarea între microservicii și adăugăm dependința Spring Web. Pentru aceste proiecte vom folosi minimul necesar de librării. Putem adăuga multe alte dependințe, dar apoi pachetul jar riscă să devină foarte mare și să influențeze performanța. Mai mult, dacă nu folosim toate librăriile, nu ar trebui să le adăugăm deloc. Generarea proiectului va produce un fișier zip care poate fi apoi importat ca proiect în IDE. Vom folosi IntelliJ în cadrul acestui proiect.
Repetați pașii de mai sus de 2 ori pentru a genera proiectele movie-info-service și ratings-data-service.
După ce zip a fost importat în IntelliJ, să deschidem proiectul. Vom vedea că proiectul are 2 clase, MovieCatalogServiceApplication și clasele de test, MovieCatalogServiceApplicationTests. Nu suntem interesați de ultima, din moment ce nu vom efectua teste. Dacă deschidem clasa MovieCatalogServiceApplication, vom vedea că este adnotată drept @SpringBootApplication
. Această adnotare informează Spring că este vorba de o aplicație Spring Boot. Clasa conține și o metodă principală, astfel reușind să inițializăm o aplicație Spring Boot.
Dacă inițializăm aplicația, vom vedea că serverul Tomcat începe cu port-ul 8080 - cel standard - dar nu execută nimic. Este o aplicație minimalistă, deci nu știe cum să gestioneze apelări. Motivul pentru care serverul Tomcat rulează este faptul că am ales Web ca dependință când am generat proiectul, iar Web are nevoie de Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Dacă deschidem un browser și accesăm http://localhost:8080, ni se va returna o eroare, deoarece Tomcat încearcă să găsească ceva la acel port și nu poate, deoarece nimic nu este acolo. Nu am definit niciun controller la acea cale care să gestioneze interogarea, deci caută o pagină pentru a afișa o eroare. Deoarece nu o poate găsi nici pe aceasta, aruncă o eroare standard, similară cu cea de mai jos.
Putem rezolva acest lucru, adăugând acel controller lipsă. Uitându-ne la ceea ce vrem să facă acest microserviciu, vedem că avem nevoie de o metodă care primește ID de utilizator și care returnează o listă de filme pentru care utilizatorul a oferit un scor. Vom defini MovieCatalogResource \@RestController:
@RestController
@RequestMapping("/catalog")
public class MovieCatalogResource {
@RequestMapping("/{userId}")
public List getCatalog(
@PathVariable("userId") String userId) {
return Collections.singletonList(
new CatalogItem("Transformers", "Test", 4);
}
}
Clasa model CatalogItem va stoca informații despre film, informații pe care trebuie să le returnăm utilizatorului - numele filmului, descrierea filmului și scorul filmului.
Am hardcodat filmul (care în mod normal este extras dintr-o bază de date) și, indiferent de utilizator, API va returna mereu filmul "Transformers".
Să ne uităm la adnotările ce apar în codul de mai sus și să explicăm ce fac.
Adnotarea @RestController
, la nivel de clasă, arată că această clasă este un controller care va gestiona aplările.
Adnotarea @RequestMapping("/catalog")
, definită tot la nivel de clasă, informează Spring Boot că oricând este apelată calea, se va încărca o clasă de resurse (i.e. MovieCatalogService).
Adnotare @RequestMapping("/{userId}")
, definită la nivel de metodă, informează Spring Boot să apeleze acea metodă. Acoladele din userId informează Spring Boot că acest userId nu corespunde literalmente șirului "userId", ci mai degrabă definește o variabilă care trebuie să fie transmisă acestei metode ca argument. Facem acest lucru, folosind adnotarea @PathVariable("userId")
ca unul din argumentele metodei, i.e. @PathVariable("userId") String userId
.
Având deja acest cod pus la punct, dacă deschidem un browser și apelăm http://localhost:8080/catalog/foo
, ar trebui să primim un răspuns precum cel de mai jos.
Acesta nu este un microserviciu, ci doar o aplicație Spring Boot.
Vom construi în mod similar și movie-info-service. Vom adăuga un API care va returna informație hardcodată despre film pe baza unui ID de film. Calea va arăta astfel /movies/{movieId}. În primul rând, vom crea modelul, Movie, care va stoca informațiile despre un film.
Vom crea în continuare controller-ul MovieResource:
@RestController
@RequestMapping("/movies")
public class MovieResource {
@RequestMapping("/{movieId}")
public Movie getMovieInfo(
@PathVariable("movieId") String movieId) {
return new Movie("Test name",
"Great movie to watch. It won an Oscar!");
}
}
Vom seta calea /movies/{movieId} și oricând se trimite o interogare acestui endpoint, vor fi returnate următoarele: un film hardcodat, un nume de Test.
Dacă inițializăm aplicația, vom observa că dăm de o problemă. Același port se folosește și pentru acest microserviciu, deci avem un conflict.
Aceasta este o problemă comună când lucrăm cu microservicii și cînd rulăm microservicii multiple pe mașina locală. Această problemă nu apare foarte des în producție, deoarece microserviciile rulează de obicei pe mașini diferite. Pentru a rezolva această problemă, trebuie să specificăm un alt port pentru unul din microservicii în fișierul application.properties. Acest fișier va fi prezent în toate aplicațiile Spring Boot, indiferent cum sunt acestea create (interfață web, CLI, etc). Acest fișier îți permite să specifici proprietăți de configurație pentru Spring în perechi cheie-valoare. Aceste configurații vor apela Spring, ceea ce va afecta codul, dacă se poate citi fișierul, se poate prelua configurația și se pot face lucrurile diferit. Avantajul fișierului de configurație este că nu trebuie să schimbăm codul și să îl recompilăm. Pentru a schimba port-ul, vom seta configurația de mai jos folosind proprietatea serve.port pentru care specificăm port-ul dorit, de unde ar trebui inițializată aplicația.
spring.application.name=movie-info-service server.port=8082
Dacă inițializăm aplicația acum și accesăm http://localhost:8082/movies/123
, vom vedea că aplicația funcționează și returnează filmul pe care l-am hardcodat.
În acest moment, avem 2 aplicații Spring Boot, dar acestea nu sunt încă microservicii. Trebuie să le facem să comunice unele cu altele.
Înainte de această etapă, reprezintă o bună practică să nu folosim port-ul 8080 pentru niciunul dintre microservicii. Deoarece este un port default, acesta poate fi folosit (fără știrea noastră) de alte aplicații server-side și am putea ajunge în conflicte dacă nu depistăm lucrurile la timp. Prin urmare, vom schimba celelalte 2 proiecte Spring Boot pentru a utiliza port-urile 8081 (movie-catalog-service) și 8083 (ratings-data-service), în loc de 8080.
Acum vom construi aplicația ratings-data-service. În acest API, ne propunem să returnăm un scor pe baza unui ID de film:
/ratingsdata/{movieId}.
Vom crea controller-ul RatingsDataResource, precum în modelul de mai jos.
@RestController
@RequestMapping("/ratingsdata")
public class RatingsDataResource {
@RequestMapping("/{movieId}")
public Rating getRating(
@PathVariable("movieId") String movieId) {
return new Rating(movieId, 4);
}
}
Apoi, vom crea modelul Rating. Pentru fiecare ID de film, vom asocia un scor hardcodat.
Ce am obținut este puțin diferit de ceea ce ne dorim. Dorim să luăm un ID de utilizator și să returnăm o listă de scoruri, dar codul nostru ia un ID de film și returnează un scor pentru acesta.
Deci, acum avem 3 aplicații Spring Boot care nu au nicio relație definită unele pentru altele. Fiecare aplicație se axează pe un anumit domeniu. Orchestrarea dintre acestea este un pas ulterior.
Vom începe orchestrarea dintre aceste microservicii, apelând mai întâi movie-info-service din movie-catalog-service. Vrem să facem acest lucru programatic, folosind o librărie client REST. Din moment ce cunoaște limbajul REST, acesta gestionează doar text în timpul comunicării de date, ceea ce nu ține cont de conceptele "Film/Movie" sau "Scor/Rating". Prin urmare, când răspunsul vine înapoi ca text (JSON), trebuie să îl trimitem mai departe unei instanțe obiect specifice: Film/Movie, Scor/Rating, etc, iar apoi să îl transmitem înapoi. Spring Boot deja vine echipat cu client în classpath pentru a face apelări REST API, sub forma RestTemplate. Este foarte posibil ca, la momentul la care va fi publicat acest articol, RestTemplate să fie deja retras în favoarea WebClient. RestTemplate este un client mai simplu, WebClient este unul complex, din moment ce are nevoie de un program reactiv.
Vom modifica codul din movie-catalog-service pentru a face o apelare API către movie-info-service.
În primul rând, vom rearanja codul puțin pentru a face apelarea mai facilă. Vom hardcoda scorurile precum în codul următor:
@RestController
@RequestMapping("/catalog")
public class MovieCatalogResource {
@RequestMapping("/{userId}")
public List<CatalogItem> getCatalog(
@PathVariable("userId") String userId) {
List<Rating> ratings=Arrays.asList(
new Rating("1234", 4),
new Rating("5678", 3)
);
return ratings.stream().map(rating->
new CatalogItem("Transformers", "Test",
rating.getRating())
.collect(Collectors.toList());
}
}
Am copiat aceeași clasă model Rating din ratings-data-service în movie-catalog-service pentru a reprezenta Rating și aici.
Pentru un utilizator dat, vrem să obținem toate filmele vizionate de acesta și scorurile lor. Pentru moment, am hardcodat lista de scoruri pentru filmele urmărite de un utilizator (vom face din acesta o apelare API mai încolo). După ce avem scorurile, le iterăm și creăm un CatalogItem cu un nume hardcodat de film.
CatalogItem este compus din nume, desc, și un scor.
Mai jos, vom insera prima apelare API către movie-info-service și vom lua de acolo informația.
public class MovieCatalogResource {
...
public List<CatalogItem> getCatalog(
@PathVariable("userId") String userId) {
....
return ratings.stream().map(rating->{
Movie movie=restTemplate.getForObject(
"http://localhost:8882/movies/" +
rating.getMovieId(), Movie.class);
new CatalogItem(movie.getName(),
movie.getDescription(),
rating.getRating())
.collect(Collectors.toList());
}
}
Am creat un restTemplate în metoda getCatalog(...) din cadrul controller-ului MovieCatalogResource, iar apoi am apelat metoda .getForObject unde am transmis URL-ul către care vrem să facem solicitarea și unde am transmis clasa în care dorim să delegăm răspunsul obținut din apelare, în cazul nostru, un film. Pentru fiecare scor, am obținut ID-ul filmului și am apelat movie-info-service. Apoi, am construit obiectul CatalogItem bazat pe informația obținută din acest film. Clasa Film/Movie se află în interiorul aplicației movie-info-service, dar vrem ca acea informație despre film să fie prezentă și în movie-catalog-service. Deci, putem crea o nouă clasă în movie-catalog-service sau o putem copia pe cea existentă din movie-info-service.
Ne putem întreba dacă nu este rău să copiem clase dintr-un proiect în altul în loc să creăm o librărie partajată pe care să o folosim oricând e nevoie. Această logică se aplică aplicațiilor monolitice, dar, în cazul microserviciilor, mergem împotriva scopului cu care au fost create microserviciile dacă folosim o librărie partajată. Dacă cineva schimbă ceva în librăria partajată, atunci toate microserviciile sunt afectate. Vrem să instalăm un microserviciu fără a ne îngrijora ce face cealaltă echipă cu celălalt microserviciu. Prin urmare, este în regulă să creăm copii ale acestor clase.
Deci, avem de-a face cu multiple apelări API. Avem 2 elemente în listă. Pentru fiecare element avem un microserviciu care rulează. Pe o instanță Tomcat, avem o apelare REST API spre un microserviciu diferit ce rulează pe o instanță Tomcat ce returnează un String (șir) care este apoi transmis spre un obiect cu ajutorul căruia punem toate datele într-un singur loc de unde retrimitem totul înapoi.
În codul anterior, facem câteva lucruri greșit. În primul rând, am hardcodat calea pe care o apelăm. Calea ar trebui generată dinamic și ar trebui să folosim un mecanism de descoperire pentru servicii. În al doilea rând, creăm obiectul RestTemplate în metoda getCatalog(...). De fiecare dată când invocăm metoda, o nouă instanță a restTemplate va fi creată. Nu ne dorim acest lucru. Dorim să avem o singură instanță și să o folosim oriunde avem nevoie de ea. Pentru a realiza acest lucru, în Spring, vom crea un bean care va fi injectat ori de câte ori vrem să o folosim. Un bean este un element singular (singleton), ceea ce înseamnă că se va crea un element de acest fel când se va crea un obiect.
@SpringBootApplication
public class MovieCatalogServiceApplication {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(
MovieCatalogServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/catalog")
public class MovieCatalogResource {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{userId}")
public List getCatalog(
..........
}
}
Continuarea articolului va fi publicată în numărul următor.