3. Creació de APIs REST
1. Nivells (capes) de Spring
Estem aprenent sobre alguns patrons de disseny que utilitzarem en la nostra aplicació, i que són necessaris per definir una arquitectura correcta en una aplicació Spring. Per tant, la primera cosa que farem és definir les capes de l'estructura de la nostra aplicació, i un cop les definim, definirem nous patrons de disseny que integrarem a la nostra aplicació.
La idea és crear una estructura de paquets que agrupi les classes en 6 paquets principals: paquet que conté la classe principal, capa web que conté els controladors, capa d'accés a dades que conté el repositori, capa de servei, capa de model de dades i capa dto. Tots aquests estan inclosos, bàsicament en 3: web, servei i repositori.
L'objectiu és que, amb una arquitectura ben definida i acoblada, sigui possible utilitzar la Injecció de Dependències, ja que facilitarà la comunicació i l'accés entre les diferents capes.
1.1. Classe Main
Cada aplicació Java ha de contenir una classe principal amb un mètode main. Aquest mètode, en cas d'implementar una aplicació amb Spring, ha de cridar el mètode run de la classe SpringApplication. Deixarem aquesta classe a l'arrel per defecte, de manera que sempre la tindrem al mateix lloc.
1.2. Capa Web. Capa de Controladors
Ara definirem el comportament de l'aplicació implementant la resta de les classes. Començarem amb la capa de més alt nivell, la capa de controladors, on exposarem els serveis de l'aplicació. En la nostra aplicació es dirà Controller.
Aquesta capa bàsicament contindrà 3 parts:
- Serveis web consumits per aplicacions (servei REST o SOAP).
- Vistes implementades amb JSP, JSF, Thymeleaf, etc. Aquest és el nostre cas principal, tot i que també crearem serveis REST.
- Vistes amb frameworks com Vaadin, Wicket, ZK, etc.
1.3. Capa de Serveis
Capa que s'encarrega d'implementar la lògica de negoci, és a dir, totes les tasques que el nostre sistema és capaç de realitzar. És la el·laboració del que ofereix el controlador.
Aquesta és una de les capes més importants ja que aquí es duran a terme totes les operacions de validació de dades que fan referència a la lògica de negoci (per exemple, comprovar que un compte corrent té saldo en fer un pagament) i la seguretat. Es dirà service.
Normalment, accedeixen a les dades emmagatzemades a la base de dades de l'aplicació a través dels repositories, fan una sèrie d'operacions i envien les dades al controlador. Podem trobar els següents tipus de servei:
- Serveis d'Integritat de Repositoris: s'encarreguen de consumir informació del repositori. Són serveis fàcils d'implementar (per exemple, sol·licitar una llista de clients).
- Serveis d'Operabilitat de Negoci: realitzen operacions específiques per al flux de negoci (realitzen operacions complexes per completar una transacció, com una venda, emmagatzemar una comanda, etc).
- Serveis de Seguretat: dedicats a realitzar operacions de seguretat.
- Serveis de Gestió: dedicats a generar informes i/o estadístiques.
1.4. Capa de Repositori
Els repositoris són les classes encarregades de gestionar l'accés a les dades. Normalment contenen classes que realitzen operacions CRUD utilitzant només una classe d'entitat d'un model de domini. Es dirà repository. Poden contenir-ne operacions de dos models.
1.5. Capa de Model
Contindrà els mapatges de les taules de la base de dades en classes que representen entitats. La capa es dirà model.
1.6. Capa DTO
Els controladors normalment gestionen DTO en lloc de pojos o beans, a causa de l'estructura de l'API o la representació en vistes. Per tant, necessitarem implementar una conversió bidireccional entre un pojo i un model DTO. Més endavant definirem, en els patrons de disseny, què és un DTO i veurem la conversió bidireccional, anomenada mapatge. A més, aquests DTO estaran en una capa separada anomenada dto.
2. Patrons de Disseny
Un patró de disseny és una solució provada que resol un tipus específic de problema de disseny en el desenvolupament de programari. Hi ha molts patrons de disseny que es divideixen en categories, per exemple: creació, estructural, comportamental, interacció, etc.
Per què utilitzar patrons de disseny?: Permeten tenir el codi ben organitzat, llegible i mantenible, també permeten reutilitzar codi i augmenten l'escalabilitat en el teu projecte.
En si mateixos proporcionen una terminologia estàndard i un conjunt de bones pràctiques pel que fa a la solució de problemes de desenvolupament de programari.
Anem a explicar-ne diversos per començar a entendre què són els patrons de disseny.
2.1. Patró MVC.
Ja hem explicat aquest patró en el tema anterior, però li farem una petita revisió.
Permet separar una aplicació en 3 capes, una manera d'organitzar i fer un projecte escalable. Les capes que podem trobar són:
- Model: Aquesta capa representa tot el que té a veure amb l'accés a dades: desar, actualitzar, obtenir dades, així com tot el codi de la lògica de negoci, bàsicament les classes Java i part de la lògica de negoci.
- Vista: La vista té a veure amb la presentació de dades del model i el que l'usuari veu, normalment una vista és la representació visual d'un model (POJO o classe Java). Per exemple, el model d'usuari, que és una classe en Java i les propietats de la qual són nom i cognom, ha de pertànyer a una vista en la qual l'usuari final veu aquestes propietats.
- Controlador: El controlador s'encarrega de connectar el model amb les vistes, funciona com un pont entre la vista i el model, el controlador rep esdeveniments generats per l'usuari des de les vistes i s'encarrega de dirigir la sol·licitud respectiva al model. Per exemple, l'usuari vol veure els clients amb el cognom Álvarez, la sol·licitud va al controlador i aquest s'encarrega d'utilitzar el model adequat i retornar aquest model a la vista.
En cap moment la vista interactuarà directament amb el model, això també manté la seguretat en una aplicació.
L'important d'aquest patró és que permet dividir-lo en parts, que són d'alguna manera independents, així que si, per exemple, es fa un canvi en el model, no afectaria la vista o si hi ha un canvi, seria mínim.
2.2. Patró DTO.
Amb aquest patró, es dissenya una de les capes transversals de l'arquitectura. Soluciona el problema de com permetre a un client intercanviar dades amb el servidor sense fer múltiples crides demanant cada peça de dada. Per exemple, si tenim una entitat anomenada Persona i una entitat anomenada Adreces, quan demanem les persones i les seves adreces hem de fer múltiples crides al servidor per demanar les persones i les adreces de cada persona, construint la vista amb aquesta informació.
El DTO ho soluciona passant un objecte lleuger al client amb totes les dades necessàries, juntes. El client pot llavors fer peticions locals a l'objecte que ha rebut.
Per fer això, es creen classes Java que encapsulen les dades en un paquet que es pot transportar per la xarxa (poden implementar java.io.Serializable, tot i que no és obligatori), és a dir, amb l'exemple anterior, crearíem una classe Java que portaria la persona i les seves adreces, tot junt en el mateix objecte.
Aquests objectes s'utilitzen en totes les capes de l'aplicació, de manera que la informació es porta per totes les capes de l'aplicació. Es recomana omplir sempre tots els camps del DTO per evitar errors de NullPointerException (una cadena buida pot ser millor), fer que els DTO siguin autodescriptius, utilitzar arrays o col·leccions de DTO quan sigui necessari, i considerar mètodes que sobreescriguin equals().
Hi ha dues variants de DTO's:
- DTOs personalitzats que representen part d'un bean o agrupen múltiples beans.
- DTOs de domini anomenats "entitats". Una classe de domini no és directament accessible pel client, ja que, a causa de la separació del patró MVC de la vista, les entitats que mapegen la base de dades (que són les entitats) no poden ser accedides. Per aquesta raó, es fan còpies DTO dels objectes de domini del servidor (entitats). Els clients poden operar sobre còpies locals millorant el rendiment de lectura i actualització.
Amb tot això, podem resumir que el DTO és un patró molt efectiu per transmetre informació entre un client i un servidor, ja que ens permet crear estructures de dades independents del nostre model de dades (Entitats), cosa que ens permet crear tantes "vistes" com sigui necessari. A partir d'un conjunt de taules o fonts de dades. A més, ens permet controlar el format, nom i tipus de dades amb què transmetem les dades per ajustar-nos a un determinat requisit. Finalment, si per alguna raó el model de dades canvia (i amb ell les entitats) el client no es veurà afectat, ja que continuarà rebent el mateix DTO.
A continuació veurem com implementar el patró DTO.
2.3. Patró DAO
El patró Data Access Object (DAO), que permet separar la lògica d'accés a dades dels Objectes de Negoci, de tal manera que el DAO encapsula tota la lògica d'accés a dades per a la resta de l'aplicació. Aquesta proposta proposa separar completament la lògica de negoci de la lògica d'accés a dades, d'aquesta manera, el DAO proporcionarà els mètodes necessaris per inserir, actualitzar, eliminar i consultar la informació; d'altra banda, la capa de negoci només es preocupa per la lògica de negoci i utilitza el DAO per interactuar amb la font de dades. En l'exemple que dissenyarem a continuació, veurem com implementar el patró DAO.
2.4. Patró FACADE (façana)
El patró de disseny Facade simplifica la complexitat d'un sistema mitjançant una interfície més senzilla. Millora l'accés al nostre sistema permetent que altres sistemes o subsistemes utilitzin un punt d'accés comú que redueix la complexitat, minimitzant les interaccions i dependències. És a dir, crearem una interfície Java que tindrà els encapçalaments dels mètodes com a punt d'accés comú, mentre que hi haurà classes Java que implementaran aquesta interfície.
Al llarg d'aquest exemple farem ús d'aquest patró.
3. Creació del projecte i configuració de Hibernate
En aquesta secció crearem un programa senzill de Hibernate amb Spring, per aplicar tot el que hem estudiat en unitats anteriors amb Spring. Només hem de crear un nou projecte, seleccionant les dependències que normalment utilitzem:
Això generarà un pom.xml amb tot el que necessitem.
Necessitem establir els paràmetres que vam establir a hibernate.cfg.xml al fitxer application.properties:
Important
Totes les opcions d'inicialització de la base de dades es poden trobar a inicialització de la base de dades.
3.1. El model.
Anem a utilitzar aquest model per a la nostra pràctica:
Advertència
Tens un script per crear la base de dades completa enllaçat ací. En aquesta unitat no implementem sobre Movimientos.
Per crear el model hem de crear els Beans que vam crear a la unitat 3. Podem utilitzar Lombok per millorar el nostre temps de desenvolupament.
Consell
Pots marcar un atribut amb l'anotació de Lombok @ToString.Exclude per evitar que participi en el mètode toString, per exemple, per evitar la recursió.
Cliente DAO en el paquet model:
3.2. El DTO
Els DTO's (Data Transfer Object) serveixen per transferir dades en el nostre sistema a través de transaccions realitzades per les nostres entitats d'una operació a una altra sense perdre la integritat entre les dades.
Per aquesta raó, és important definir que l'accés a les dades es fa només a través de DAO (Data Access Object) del nostre model, obtenint així una abstracció del model de dades. Les dades només s'accedeixen a través de mètodes definits en el DAO. Els DAOs inclouen un altre concepte que és el DTO (Data Transfer Object). Els DTO's són una classe d'objectes que serveixen únicament per transportar dades. El DTO conté les propietats de l'objecte. Dades que poden originar-se a partir d'una o més entitats d'informació.
Una altra bona pràctica és marcar les classes amb el sufix DTO per recordar el seu significat, així la classe Client es convertiria en ClientDTO.
Una de les característiques dels DTO's és que han de ser objectes Serializable per poder viatjar a través de la xarxa. Necessitem indicar aquesta característica en els DTO's, així que afegirem que implementin la interfície Serializable, i amb això, la propietat UID que identifica la versió de cada objecte transportat.
Per crear un DTO crearem una classe amb els atributs que volem que contingui el DTO. Pot ser més o menys que la classe DAO. Després és important crear mètodes per convertir tant de DAO a DTO com de DTO a DAO. Aquests mètodes es poden crear de manera estàtica.
Com hem dit, evitem accedir a les entitats des de capes superiors.
Ampliació
Pots cercar a internet informació sobre ModelMapper per a gestionar els DTO
3.2.1. Convertint DTO a JSON
Com hem dit, aquests objectes (DTO's) es converteixen a JSON automàticament, però com? Spring utilitza Jackson object mapper per transformar objectes a objectes JSON. En aquest tutorial pots entendre millor com funciona.
Convertir objectes a JSON pot portar-nos a un altre problema de recursió infinita, quan l'objecte conté referències creuades, com els mètodes toString(). Hem après com evitar l'excepció StackOverflowException en els mètodes toString(), utilitzant l'anotació @ToString.Exclude de Lombok, però com evitar-ho quan es converteix a JSON. La solució s'ofereix amb noves anotacions, com segueix:
@JsonIgnore→ aquest camp no es convertirà.-
@JsonManagedReference→ indica que mostrarem la informació d'aquest objecte cap endavant, però no cap enrere. Aquesta anotació es complementarà amb: -
@JsonBackReference→ mostra només la informació referenciada, similar a@JsonIgnore. @JsonIgnoreProperties("property")→ omet aquesta propietat en el camp anotat.
Si marquem:
| Java | |
|---|---|
Mostrarem la informació de la pel·lícula d'un director sense recursió (només els atributs propis).
Podeu trobar més informació aquí.
3.3. El repositori
La capa de repositori és responsable de gestionar l'accés a les dades. Això fa possible separar la lògica de negoci de l'accés a les dades, permetent, per exemple, poder modificar la base de dades del sistema sense afectar la lògica.
En aquesta capa hem de distingir dos tipus d'accés:
- Accés a dades del model de dades propi del sistema que es farà a través de l'accés DAO (Data Access Object). Per a aquest tipus d'accés, utilitzarem el framework JPA (Java Persistence API) a través de Spring Data.
- Accés a sistemes externs a través de connectors (webservices, APIs, etc.)
Per aquesta raó, crearem una interfície en la qual tindrem les operacions que exposarem. Aquesta interfície serà ClientRepository.java i es crearà en el paquet repository.
Amb Spring, estalviarem molta feina, ja que simplement definirem una interfície de treball, en la qual indicarem el tipus de repositori que volem crear, la classe sobre la qual treballarà i el tipus de dades que funciona com a identificador d'aquesta classe:
Spring proporciona la Repository Interface, de la qual hereta CrudRepository, que inclou la definició de les operacions bàsiques CRUD. D'aquest últim hereta PagingAndSortingRepository, que afegeix funcions de paginació i ordenació, i finalment tenim JPARepository, que inclou operacions específiques per a JPA.
La importància de la definició genèrica de Repository<Class,Type> és que tots els objectes que recuperarà són d'aquesta classe, i el tipus indica el tipus de la clau primària d'aquesta classe. Seguint el nostre exemple, la definició del repositori seria:
| Java | |
|---|---|
Amb això, Spring ja ens permet accedir a la base de dades i realitzar operacions bàsiques. Els següents mètodes estan implementats per defecte, i no necessitarem implementar-los, només definir-los:
- Recuperar dades:
findAll(),findById(Id),findById(Iterable<Id>): recupera una o totes les ocurrències d'un identificador o una col·lecció d'identificadors.- Eliminar dades:
delete(Object),deleteAll(),deleteById(Id),deleteAllById(Iterable<Id>): elimina per objecte, identificador o tots.- comptar i comprovar:
count(),existsById()- guardar objectes:
save(Object),save(Iterable<Object>): guarda l'objecte(s)
Consell
Si necessitem un altre mètode, l'hem de definir, i després crear una nova classe per implementar aquesta interfície i implementar el mètode.
3.4. El servei
La capa de servei gestiona la lògica de negoci de la nostra aplicació. Aquesta lògica de negoci està separada de la lògica web, que es troba en el controlador.
Quan definim classes que implementen serveis per a la lògica de negoci, s'han de seguir les següents regles:
- Definir una interfície que tindrà els encapçalaments dels mètodes que es volen publicar. D'aquesta manera fem ús del patró Facade i exposem els mètodes del servei per a ser utilitzats.
- Definim una classe (tingueu en compte que una interfície pot tenir diverses classes que la implementin) que implementi la interfície, de manera que puguem implementar tots els mètodes del servei seguint la lògica de negoci requerida.
- L'anotació
@Serviceindica a Spring que reconegui la classe com un servei (similar a l'anotació@Controllerque hem estudiat en la secció anterior). - Utilitzarem
@Autowiredper injectar el servei en el controlador (ho veurem més endavant). - Utilitzarem
@Autowiredper injectar el DAO amb el qual treballarem en el servei. - Tingueu en compte que un mètode de servei definirà una operació a nivell de negoci, per exemple, donar un missatge de benvinguda. Els mètodes de servei estaran compostos per altres operacions més petites, que es definiran en la capa de repositori.
Amb tot això, començarem a definir la capa de servei. Així que comencem creant una interfície anomenada ClientService.java en el paquet service.
| Java | |
|---|---|
Notem que en aquest cas definim 4 operacions bàsiques amb ClienteDTO, ja que són els objectes que gestionarà el controlador.
Un cop tenim la interfície, creem una nova classe anomenada ClientServiceImpl.java, que s'encarregarà d'implementar els mètodes que es declaren a la interfície. Després, fem que la nova classe implemente la interfície creada i després fem que implementi tots els mètodes de la interfície per defecte. També indiquem l'anotació @Service a la classe. Serà com segueix:
Com podeu veure, el servei s'encarrega d'invocar els mètodes del repositori i de realitzar algunes comprovacions si és necessari. A més, obtenim objectes Cliente i els transformem en ClienteDTO per utilitzar-los per retornar al controlador.
Important
El mètode findClienteById() retorna una nova classe envoltori (wrapper <>) Optional<Cliente>. Aquesta classe encapsula un Objecte que existirà o no, proporcionant mètodes per comprovar-ho i actuar en conseqüència:
isPresent()→ retorna un booleà.orElse(anotherObject)→ si no existeix, retorna un altre Objecte en lloc de l'Optional.get()→ retorna l'objecte existent.
L'objectiu principal és evitar la famosa NullPointerException. Podeu trobar més informació aquí
3.5. El controlador
Ara definirem la capa de més alt nivell, la capa de controladors, on exposarem els serveis de l'aplicació.
El controlador serà l'encarregat de respondre a les sol·licituds dels usuaris amb l'aplicació. Inclourà els serveis, i en cas de crear una aplicació MVC, podrà invocar motors de plantilles, com Thymeleaf. Com hem comentat aquí, ho implementarem en una sola classe (sense seguir el patró anterior).
El controlador invocarà el servei associat a aquesta sol·licitud i retornarà les dades obtingudes o la resposta al mateix client. Hem de marcar la classe amb l'estereotip @Controller. En el cas dels serveis REST, també hem d'indicar que els retorns dels mètodes de la classe es serialitzen a JSON, i ho aconseguim amb @ResponseBody. Des de Spring 4, les dues anotacions s'han fusionat en una, mitjançant @RestController. Només deixarem @Controller per a projectes on retornem una vista (HTML + CSS + JS).
El servei que crearem tindrà un comportament complet pel que fa al manteniment de Cliente, així com la seva recomanació, de manera que podrem llistar clients, veure la seva informació, registrar clients, actualitzar dades de clients i eliminar clients, o ser les operacions CRUD que es coneixen.
Així que començarem creant un controlador anomenat ClienteController.java. Aquest controlador implementarà les 4 operacions corresponents a CRUD: crear, llegir, actualitzar i eliminar. A part d'aquestes 4, crearem una operació que mostrarà la pàgina inicial de l'aplicació, amb un enllaç al manteniment de clients.
| Java | |
|---|---|
El controlador normalment defineix quatre operacions CRUD, però podem afegir totes les que necessitem. Vegem-ho.
3.5.1. Index
Aquest mètode mapatge l'índex arrel de la nostra aplicació web. Per exemple, podem enviar el nom de l'aplicació i el mòdul. Aquestes variables es poden definir en el nostre application.properties i es poden carregar amb l'anotació @Value i una cadena ${property_name} dins.
| Java | |
|---|---|
El controlador d'índex normalment mostra informació general sobre la pàgina principal, com segueix:
| Java | |
|---|---|
4. Operacions de lectura (GET)
És hora de recuperar dades del nostre servidor, i normalment les operacions GET són els mètodes més demandats. Implementarem diversos mètodes que podrien retornar un o molts objectes de la classe desitjada, en les nostres notes, la classe Cliente.
4.1. Obtenir tots
No es necessita cap opció de filtre, perquè volem obtenir tots els clients. Quan rebem aquesta sol·licitud get, hem de cridar al mètode listAll en el nostre servei, que crida, de fet, a findAll en el nostre repositori.
| Java | |
|---|---|
A part del missatge de registre, simplement recuperem les dades del servei i les retornem a la resposta.
4.2. Obtenir Un
Aquesta és la versió més específica, i normalment en la sol·licitud busquem un objecte a partir del seu ID. Llavors, hem d'obtenir un paràmetre en la nostra sol·licitud, i utilitzarem l'ID en el camí, utilitzant l'anotació @PathVariable.
| Java | |
|---|---|
La sol·licitud serà /clientes/7, per exemple, i llavors, en el nostre mètode del controlador, el paràmetre idCliente es configurarà amb el valor 7. Cridem getClienteById(7) i, òbviament, obtenim el ClienteDTO encapsulant el Cliente sol·licitat, si existeix.
Podem millorar els mètodes per al cas que no hi hagi resultats (Cliente no existeix) o s'hagi produït un error, retornant i encapsulant els resultats en un ResponseEntity<Cliente>. Aquesta classe envoltori retorna el resultat a la sol·licitud, però permet afegir un argument que serà el codi d'estat http. Aquest codi pot ser capturat a l'aplicació client per al maneig d'errors. L'algoritme serà alguna cosa així:
| Java | |
|---|---|
aplicat a la nostra aplicació d'exemple, i fusionant els dos últims exemples de codi, obtindrem el següent:
4.3. Gestió d'excepcions
El controlador gestiona diverses excepcions, i el programa continua executant-se tret que es produeixi un gran error. Ara presentem un mètode que donarà una resposta quan es produeixi una excepció. Anotarem aquest mètode amb @ExceptionHandler, passant la classe d'excepció que hem de capturar.
We can handle several exceptions in one unique method, marking it with a collection of exceptions, for instance as:
5. Operacions de guardat (POST)
La primera operació que necessitem fer és guardar un nou Cliente a la nostra base de dades. Anem a explicar-ho amb un exemple. El controlador rebrà un nou objecte DTO enviat per l'aplicació client (utilitzarem Postman, com es pot veure a l'Apèndix 1):
tingues en compte que:
- La sol·licitud està mapejada amb
@PostMapping. Aquí tens un comentari amb opcions addicionals. - L'objecte rebut per l'aplicació està dins d'un objecte JSON en el cos de la sol·licitud, marcat com
@RequestBody. En aquesta unitat suposarem que les dades arriben ben formatejades. - Les dades rebudes es passen al servei i es retorna un nou
Cliente(emmagatzemat). - Aquest client es retorna al client.
En aquesta imatge pots veure les dades enviades a l'API i el valor retornat. Tingues en compte que Cuentas i Direcciones no estan presents en aquestes dades. Veurem més endavant com afegir aquestes dades addicionals.
Advertència
Hi ha moltes validacions a fer per verificar la integritat de les dades. Aquí pots trobar un article sobre com validar les dades rebudes de les aplicacions client.
6. Operacions d'actualització (PUT)
Per actualitzar un objecte de la base de dades, necessitem rebre l'objecte actualitzat (i complet) en la sol·licitud. No podem guardar-lo immediatament, perquè podria no existir. Per aquesta raó, hem de comprovar l'existència, i si aquesta és positiva, llavors guardem l'objecte rebut, que actualitzarà la versió anterior a la base de dades. Vegem l'exemple:
7. Operacions d'eliminació (DELETE)
Eliminar és una operació molt senzilla, perquè només necessitem l'identificador de l'objecte que volem eliminar, i després el podem obtenir del camí, dins de la variable de camí. Llavors, hem de cridar l'operació d'eliminació del servei.
| Java | |
|---|---|
8. Exercici. Completa el servidor
Hem acabat de crear el nostre controlador sobre la classe Cliente, però podem millorar la nostra aplicació afegint més serveis i configuracions al servidor. Aquests conceptes s'estudiaran en les següents seccions.
Recomanem completar el controlador, serveis i repositoris de Cuenta i Direccion amb operacions per defecte.
A més, pots afegir opcions per afegir una Cuenta a un Cliente o eliminar-la, i el mateix amb Direccion.