28.04.20226 min

Cezary HudzikJava DeveloperAsseco Poland S.A.

Dokumentacja oraz generacja aplikacji REST Spring przy użyciu Openapi

Sprawdź, jak generować aplikację REST przy użyciu specyfikacji OpenAPI, Springa, Swaggera oraz Gradle’a.

Dokumentacja oraz generacja aplikacji REST Spring przy użyciu Openapi

W tym artykule chciałbym przedstawić generację aplikacji REST przy użyciu specyfikacji OpenAPI, Springa, Swaggera oraz Gradle’a.

OpenAPI w wersji 3.0, jest formatem opisu API dla REST API. Plik OpenApi (w formacie yaml/yml albo json) umożliwia opis całego projektowanego API.


Do wygenerowania API wykorzystamy następujące technologie:

  1. Spring w wersji 2.3.2.RELEASE. Dla pracy przy innych wersjach należy zwrócić szczególną uwagę na kompatybilność bibliotek Swaggera z wersją Springa. Podana konfiguracja będzie poprawnie generowała pliki UI jak i kod aplikacji, jednakże nie udostępni automatycznie interfejsu graficznego.
  2. Swaggger, narzędzie open-source, odpowiedzialne za budowanie dokumentacji oraz klas. Będziemy wykorzystywać wbudowane narzędzia, SwaggerUI oraz SwaggerCodegen.
  3. Gradle w wersji 7.4, narzędzie do automatycznego budowania oprogramowania. Wykorzystuje język DSL (Domain Specific Language).


Konfiguracja

W pliku build.gradle potrzebujemy zdefiniować następujące elementy:

Plugin odpowiedzialny za generację kodów:

plugins {

   ...

   id 'org.hidetake.swagger.generator' version '2.19.2'

}


Następnie dodajemy do dependency brakujące zależności:

dependencies {

   ...

   swaggerCodegen 'org.openapitools:openapi-generator-cli:3.3.4'

   swaggerUI 'org.webjars:swagger-ui:3.52.5'

   implementation "io.springfox:springfox-swagger2:2.9.2"

}


Dependency nie musi być w pliku build.gradle. Zazwyczaj jest wydzielane do osobnego pliku.


Definicje zadania generacji kodów:

 

swaggerSources {

   main {

       inputFile = file('main.yaml')

       code {

           language = 'spring'

           components = [

                   'models',

                   'apis'

           ]

           configFile = file('main.json')

       }

       ui {

           doLast {

               copy {

                   from 'index.html'

                   into outputDir

               }

           }

       }

   }

}


InputFile- wskazuje na plik OpenAPI na podstawie którego zostanie wygenerowany kod.


Podzadanie code

określa konfigurację dla generatora kodu aplikacji. Przykładowa konfiguracja z pliku main.json:

{

 "dateLibrary": "java11",

 "delegatePattern": true,

 "library": "spring-boot",

 "apiPackage": "pl.main.api",

 "modelPackage": "pl.main.model",

 "invokerPackage": "pl.main",

 "systemProperties": {

   "supportingFiles": "ApiUtil.java"

 }

}


Definiujemy nazwy pakietów, w których zostaną wygenerowane kody. Z istotniejszych konfiguracji wykorzystamy "delegatePattern": true.


Podzadanie ui

określa konfigurację dla generatora interfejsu użytkownika aplikacji (dokumentacji)

Człon “main” odpowiada za generacje kodów dla jednej “aplikacji” opisanej w pliku. OpenAPI. W przypadku konieczności wygenerowania kodów innych aplikacji (dla przykładu klientów REST), trzon main powinien zostać powtórzony ze zmienioną nazwą, wraz z odpowiednią konfiguracją:

swaggerSources {

   main {

       ...

   }

   client {

       ...

}


Aby wygenerowane pliki były widoczne w źródłach, musimy je dodać poprzez:

sourceSets {

   main {

       java {

           srcDirs = ['src/main/java', "${swaggerSources.main.code.outputDir}/src/main/java"]

       }

 
       resources {

           srcDirs = ['src/main/resources', "${swaggerSources.main.code.outputDir}/src/main/resources"]

       }

   }

}


Kończąc naszą konfigurację, możemy wymusić odpalenie zadań generacji kodu pod task build:

build.dependsOn swaggerSources.main.code, swaggerSources.main.ui


Następnie dodajemy wygenerowany folder do statycznych zasobów w czasie odpalania aplikacji bootRun  lub bootJar. Bez tego dokumentacja nie zostanie udostępniona na natywnym porcie.

bootRun {

   dependsOn "generateSwaggerUIMain"

   systemProperty "spring.resources.static-locations", "file:build/swagger-ui-main"

}

 
bootJar {

   dependsOn "generateSwaggerUIMain"

   from("${projectDir}/build/swagger-ui-main") {

       into "BOOT-INF/classes/static"

   }

}


Specyfikacja OpenAPI

Poprawnie napisana specyfikacja umożliwia łatwe wprowadzanie zmian w strukturze aplikacji. Czystą, łatwą do zrozumienia dokumentację. Znacząco przyspiesza pracę, poprzez generowanie gotowego kodu, na podstawie krótkich definicji.

W tym przypadku będę używał pliku yaml. W nagłówku pliku znajdziemy OpenAPI wraz z wersją i informacją o produkcie, na którą składa się: wersja aplikacji, tytuł aplikacji, opis. W niższych linijkach definiujemy serwer API.

Zazwyczaj definiowany jest jeden serwer, w naszym przypadku zdefiniowałem dwa. Wszystkie ścieżki opisane w endpointach będą relatywne wobec adresu serwerów.

openapi: 3.0.0

info:

 version: 1.0.0

 title: Main API

 description: Testowa aplikacja prezentująca OpenAPI

 
servers:

 - url: localhost/api/v1

   description: Adres do wywołań lokalnych

 - url: main.com

description: Inny adres do wywołań zewnętrznych


Dany początek pliku będzie skutkował następującą generacją UI:

 

Przykładowa definicja endpointu będzie wyglądała następująco:

paths:

 '/user/{uuid}':

   get:

     tags:

       - Użytkownicy

     operationId: getUser

     summary: Pobranie użytkownika

     description: |

       Operacja zwraca dane użytkownika

     parameters:

       - in: path

         name: uuid

         required: true

         schema:

           description: UUID

           type: string

     responses:

       200:

         description: Pobrano użytkownika

         content:

           'application/json':

             schema:

               $ref: '#/components/schemas/User

       404:

         description: Nie znaleziono użytkownika

       500:

         description: Błąd servera

         content:

           'application/json':

             schema:

$ref: '#/components/schemas/ErrorInfo'


Sekcja paths odpowiada za definiowanie wywołań aplikacji. Podajemy adres relatywny. Metodę, pod jaką będzie dostępny dany endpoint (get). Znacznik tags pozwala nam pogrupować metody w podkatalogi, wpływa to na prezentację wywołań w dokumentacji oraz nazwę klasy, w której zostanie wygenerowany kod.

OperationId określa nazwę metody, jaką będziemy musieli nadpisać, aby zaimplementować logikę. Sekcja parameters definiuje jakie parametry będziemy przyjmować. W tym wypadku wykorzystujemy parametr określony w ścieżce o nazwie uuid.

Parametr można zdefiniować w tym miejscu, jednakże z powodów czytelności i powtarzalności lepiej jest go wydzielić do opisu modelu. Sekcja responses opisuje odpowiedzi.

Podano możliwe zwracane statusy (200,400,500). Dla statusu 200 podajemy jaki obiekt będzie zwracany przy poprawnym wywołaniu. W tym przypadku będzie to model opisany w schematach “User”.

Istnieje możliwość definicji własnej odpowiedzi przy błędnym wywołaniu, tak samo, jak w innych przypadkach wskazujemy na odpowiedni model. Pozostaje nam zdefiniować wykorzystywane modele w sekcji components.


Sekcja components

components:

 parameters:

   uuid:

     name: uuid

     in: path

     description: Unikalny identyfikator dokumentu

     required: true

     schema:

       type: string

format: uuid


Parametry definiowane są w osobnej sekcji od modeli. Zdefiniowałem parametr UUID⁣, którego oczekujemy przy wywołaniu adresu “{server}/user/{uuid}”. Pominę dokładny opis parametrów. Jak widać, sam język jest dość intuicyjny i łatwo przyswajalny.


Definicja schematów następuje na tej samej “wysokości” co parametrów:

schemas:

 User:

   type: object

   description: Użytkownik

   required:

     - login

   properties:

     login:

       description: Login użytkownika

       type: string

     active:

       description: True określa konto aktywne

       type: boolean

     additionalInformations:

       type: array

       items:

         $ref: '#/components/schemas/AdditionalInformations'

 
 
 ErrorInfo:

   description: Informacja o błędzie

   type: object

   properties:

     timestamp:

       description: Data i czas utworzenia odpowiedzi

       type: string

       format: date-time

     statusCode:

       description: Kod statusu HTTP odpowiedzi

       type: integer

     message:

       description: Opis błędu

type: string


W tym wypadku również nie ma co się rozpisywać nad definicjami opisanymi powyżej. Opisałem dla obiektu User trzy pola: login (string), active (boolean), additionalInformations (obiekt klasy additionalInformations opisany niżej).

additionalInformations:

 type: object

 description: Dodatkowe informacje

 properties:

   comment:

     description: Komentarz użytkownika

type: string


Security

Kończąc naszą specyfikację, dodajmy jeszcze linijkę odpowiadającą za security. Z powodów czytelności powinniśmy ją dodać na samej górze pliku, pod sekcją info:

security:

- jwtToken: []


Doda nam to w interfejsie przycisk do autoryzacji:

 

Umieszczenie sekcji security będzie skutkowało dołączeniem tokenu do wszystkich wywołań. W sytuacji, gdy chcemy zabezpieczyć tylko wybrane endpointy, możemy ją umieścić w odpowiedniej sekcji paths.


Zbudowanie aplikacji

Tak opisaną i skonfigurowaną aplikację możemy poleceniem build zbudować. Otrzymamy wygenerowane kody w katalogu build:

W katalogu swagger-code-main przechowywane są wygenerowane klasy javy, zaś w katalogu swagger-ui-main znajduje się dokumentacja. Przy uruchomieniu aplikacji poprzez bootRun otrzymamy dostęp naszego interfejsu. W tym przypadku wchodząc na http://localhost:8080/, wyświetlona zostanie strona swaggera:

Na samym początku konfigurowania OpenAPI zdefiniowaliśmy delegatePattern na true. Zachęcam do tego podejścia. Skutkuje on wygenerowaniem innej struktury plików:

W tym patternie, gdy chcemy zaimplementować logikę, tworzymy klasę UserApiDelegateImp, która będzie implementacją UserApiDelegate i w tej klasie przekazujemy logikę na napisane przez nas serwisy.

package pl.devtech.main.backend.delegate;

 
import org.springframework.http.ResponseEntity;

import pl.main.api.UserApiDelegate;

import pl.main.model.User;

 
public class UserApiDelegateImp implements UserApiDelegate {

 
   @Override

   public ResponseEntity<User> getUser(String uuid) {

       return UserApiDelegate.super.getUser(uuid);

   }

}


W ten prosty sposób poznaliśmy podstawowe możliwości wykorzystania OpenAPI w projekcie. Rozwinięcia możliwych typów danych, rozwinięć konfiguracji można znaleźć w dokumentacji pod adresem: swagger.io/docs/specification/about/

<p>Loading...</p>