Wybierz region
pl
  • PL
  • EN
Wydrukuj

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"

}

}

Przy tak skonfigurowanej aplikacji możemy przejść do omawiania specyfikacji 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:

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

Kończąc naszą specyfikacje, 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/


Cezary Hudzik

Ukończył Politechnikę Gdańską na wydziale ETI. Z zamiłowania interesuje się sztucznymi sieciami neuronowymi oraz uczeniem maszynowym. W wolnych chwilach jeździ motocyklem po pomorskich drogach lub pije wino.


Wydrukuj