Tworząc aplikację w Spring Boocie, która udostępnia API przez web serwisy, czasem potrzebujemy wejść z nią w jakąś interakcję. Czy to w celach testowych, czy po prostu uruchomić nasz serwis, aby wykonał jakąś czynność. Możemy w tym celu użyć różnych narzędzi jak Postman, curl czy chociażby przeglądarki jeśli chcemy wykonać zapytanie GET. Jednak te narzędzia nie są świadome, jakich danych oczekuje nasz serwis oraz jakie endpointy są w nim dostępne. Możemy oczywiście w tym celu stworzyć sobie jakiś interfejs użytkownika, przez który będziemy mogli podejmować interakcję z aplikacją, jednak wymaga to jakiegoś nakładu pracy. I tutaj z pomocą może nam przyjść Swagger UI. Po prostu dołączamy go do projektu, a on sam przeskanuje aplikację i wygeneruje nam stronę www, przez którą będziemy mogli podejmować interakcję z naszą aplikacją. Dodatkowo możemy udokumentować API, dodając odpowiednie adnotacje w kodzie.
Kod źródłowy do wpisu znajduje się jak zawsze w repozytorium GitHuba pod adresem: https://github.com/mloza/spring-boot-swagger-ui.
Tworzymy apikację
Na początek stwórzmy sobie aplikację w Spring Boocie. Tworzymy nowy projekt Mavenowy i dodajemy zależności. Nasz pom powinien wyglądać jak poniżej.
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>pl.mloza</groupId> | |
<artifactId>swagger-ui</artifactId> | |
<version>1.0-SNAPSHOT</version> | |
<properties> | |
<maven.compiler.source>15</maven.compiler.source> | |
<maven.compiler.target>15</maven.compiler.target> | |
</properties> | |
<dependencies> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
<version>2.4.0</version> | |
</dependency> | |
<dependency> | |
<groupId>io.springfox</groupId> | |
<artifactId>springfox-boot-starter</artifactId> | |
<version>3.0.0</version> | |
</dependency> | |
</dependencies> | |
</project> |
Następnie możemy stworzyć przykładowe endpointy. Dla uproszczenia całą aplikację umieściłem w jednej klasie. Wygląda ona jak poniżej.
package pl.mloza; | |
import org.springframework.boot.SpringApplication; | |
import org.springframework.boot.autoconfigure.SpringBootApplication; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.PostMapping; | |
import org.springframework.web.bind.annotation.RequestBody; | |
import org.springframework.web.bind.annotation.RestController; | |
import pl.mloza.database.Entry; | |
import pl.mloza.rest.AddEntryRequest; | |
import pl.mloza.rest.AddEntryResponse; | |
import pl.mloza.rest.ListEntryResponse; | |
import java.util.ArrayList; | |
import java.util.List; | |
@SpringBootApplication | |
@RestController | |
public class Main { | |
private final List<Entry> database = new ArrayList<>(); | |
public static void main(String[] args) { | |
SpringApplication.run(Main.class, args); | |
} | |
public Main() { | |
database.add(new Entry("one", "one")); | |
database.add(new Entry("two", "two")); | |
database.add(new Entry("three", "three")); | |
} | |
@GetMapping("/api/list") | |
public ListEntryResponse listObjects() { | |
return new ListEntryResponse(database); | |
} | |
@PostMapping("/api/add") | |
public AddEntryResponse addEntry(@RequestBody AddEntryRequest addEntryRequest) { | |
database.add(new Entry(addEntryRequest.getKey(), addEntryRequest.getValue())); | |
return new AddEntryResponse("Success"); | |
} | |
} |
I to wszystko. Teraz wystarczy uruchomić aplikację i przejść pod adres: http://localhost:8080/swagger-ui/. Naszym oczom powinna się ukazać strona z listą endpointów, które możemy wywołać.
Dokumentacja kodu
Wspomniałem wcześniej, że możemy dokumentować kod za pomocą swaggera. Możemy dodać adnotacje do naszego kodu opisujące działanie poszczególnych endpointów. Swagger automatycznie pobierze treść adnotacji i dołączy je do UI. Zobaczymy to na przykładzie.
@Operation( | |
summary = "List all entries from database", | |
description = "Returns all entries that was saved to our database" | |
) | |
@ApiResponses(value = { | |
@ApiResponse(responseCode = "200", description = "Entry was added to database"), | |
@ApiResponse(responseCode = "500", description = "Something went wrong, entry was not added") | |
}) | |
@GetMapping("/api/list") | |
public ListEntryResponse listObjects() { | |
return new ListEntryResponse(database); | |
} | |
@Operation( | |
summary = "Add entry to database", | |
description = "Save entry to database. Keys and values can be duplicated." | |
) | |
@ApiResponses(value = { | |
@ApiResponse(responseCode = "200", description = "Entry was added to database"), | |
@ApiResponse(responseCode = "500", description = "Something went wrong, entry was not added") | |
}) | |
@PostMapping("/api/add") | |
public AddEntryResponse addEntry(@RequestBody AddEntryRequest addEntryRequest) { | |
database.add(new Entry(addEntryRequest.getKey(), addEntryRequest.getValue())); | |
return new AddEntryResponse("Success"); | |
} | |
@Operation( | |
summary = "Delete entry from database", | |
description = "Deletes entry from database. If entries are duplicated, we delete only one instance." + | |
" Returns deleted entry" | |
) | |
@ApiResponses(value = { | |
@ApiResponse(responseCode = "200", description = "Entry was removed from database"), | |
@ApiResponse(responseCode = "500", description = "Something went wrong, entry was not added") | |
}) | |
@DeleteMapping("/api/remove/{key}") | |
public RemoveEntryResponse deleteEntry(@PathVariable String key) { | |
Optional<Entry> entry = database.stream().filter(e -> e.getKey().equals(key)).findFirst(); | |
return entry.map(e -> { | |
database.remove(e); | |
return new RemoveEntryResponse(e); | |
}).orElse(new RemoveEntryResponse()); | |
} |
Widzimy 3 nowe adnotacje:
- @Operation – Służy ona do opisu działania funkcji, summary pojawia się przy endpoincie (1 na screenshocie), descrption widoczne jest dopiero po kliknięciu i rozwinięciu endpointu (2 na screenshocie)
- @ApiResponses i @ApiResponse – opisuje odpowiedzi, które może zwrócić serwis (3 na screenshocie)
Poniżej możecie zobaczyć jak to wygląda w przeglądarce.
Poza podanymi przez nas opisami widzimy również model danych, których oczekuje serwis oraz tych, które zostaną zwrócone w odpowiedzi (odpowiednio 4 i 5 na obrazku). Klikając przycisk Execute możemy wywołać zapytanie do serwisu. Poniżej zostanie wyświetlona odpowiedź, którą zwróci serwis.
Konfiguracja Swaggera
Aby zmienić domyślne opisy na początku strony, musimy stworzyć bean z konfiguracją swaggera. Najprostsza konfiguracja może wyglądać jak poniżej.
@Configuration | |
public class SwaggerConfiguration { | |
public ApiInfo apiInfo() { | |
return new ApiInfoBuilder() | |
.title("API Demo") | |
.description("Demo application API") | |
.version("1.0.0") | |
.build(); | |
} | |
@Bean | |
public Docket docket() { | |
return new Docket(DocumentationType.OAS_30) | |
.apiInfo(apiInfo()) | |
.enable(true) | |
.select() | |
.paths(PathSelectors.any()) | |
.build(); | |
} | |
} |
Podsumowanie
Swagger umożliwia nam szybkie stworzenie UI do interakcji z naszą aplikacją oraz dokumentacji poszczególnych endpointów. Może być wykorzystywany do testowania lub gdy potrzebujemy czasu do czasu podjąć jakąś interakcję z aplikacją. Możemy też w ten sposób wystawić API dla frontendu, dzięki temu będą wiedzieli jakie operacje mogą wykonać na webserwisie. Swagger udostępnia również narzędzia do generowania kodu klienta i stubu dla serwera, jednak z tego jeszcze nie korzystałem. Jeśli mieliście okazję tego używać, daj znać w komentarzach jak się z tym pracuje ?