Документирование SpringBoot API с помощью Swagger

Веб-приложение содержит API для работы. Документирование API позволяет клиентам API быстрее понять, как использовать сервисы. Даже если API закрыт от внешнего мира, то все равно стоит уделить время спецификации - это поможет вашим новым коллегам быстрее разобраться с системой.

· 8 минуты на чтение
Документирование SpringBoot API с помощью Swagger

Создание документации вручную - утомительный процесс. В этой статье мы рассмотрим основы Swagger и его возможности по документированию REST API в SpringBoot приложении.

Спонсор поста

Используемые версии

Java 17
Spring Boot 3.0.2
Swagger 2.2.8
SpringDoc 1.6.14 | 2.0.2

Изменения статьи

29.06.22: Обновил до Java 17. Обновил все зависимости до актуальных.
11.02.23: Обновил SpringBoot до 3.0.2. Обновил остальные зависимости. Добавил раздел с авторизацией в Swagger UI.

Что такое Swagger?

Swagger позволяет разработчикам описывать структуру своих API и генерировать интерактивную документацию, клиентские библиотеки и серверные модули для реализации API на разных языках.

Swagger предоставляет спецификацию для документирования REST API, которая называется OpenAPI Specification (OAS). Эта спецификация предоставляет четкий и лаконичный способ описания эндпойнтов, их параметров, моделей запросов и ответов и других аспектов API.

В данной статье рассматривается пример генерации OAS на основе уже существующих REST контроллеров. Мы разметим контроллеры и эндпойнты аннотациями, на основе которых будет сгенерирована OAS.

Существуют библиотеки, которые на основе OAS могут сгенерировать интерактивную документацию для API, которая позволит отправлять запросы, и получать ответы. Мы воспользуемся библиотекой SpringDoc.

Почему SpringDoc, а не SpringFox?

В интернете множество статей с использованием библиотеки SpringFox для генерации Swagger UI. Почему тогда я использую какой-то SpringDoc?

SpringFox был заброшен авторами. Последний коммит сделан в 2020 году, а количество issue уже перевалило за 200. В какой-то момент я столкнулся с каким-то багом в SpringFox, который никогда не будет исправлен, и стал искать альтернативы.

Такой альтернативой оказался SpringDoc, который активно развивается и поддерживает SpringBoot 3. Я успешно и быстро перевел свой проект с SpringFox на SpringDoc.

Также стоит упомянуть, что Swagger позволяет сгенерировать непосредственно код клиента или сервера по имеющейся OAS, для этого нужен генератор кода Swagger-Codegen.

Генератором я никогда не пользовался. Генерируемый код получается весьма неказистым и проще написать его самостоятельно.

Демо проект с REST API

Чтобы документировать API, для начала напишем его. Вы можете перейти к следующей главе, чтобы не тратить время.

😺
Репозиторий с примерами: Struchkov Git | GitHub

Добавим примитивные контроллеры и одно DTO. Бизнес суть нашей системы – программа лояльности пользователей.

Для наших примеров достаточно слоя контроллеров, поэтому я позволю себе вольность опустить серверный и репозиторный слой и добавить бизнес логику непосредственно контроллер.

В качестве DTO у нас будет класс UserDto – это пользователь нашей системы. У него пять полей, из которых 3 обязательны.

UserDto.java

public class UserDto {

    private String key;
    private String name;
    private Long points = 0L;
    private Gender gender;
    private LocalDateTime regDate = LocalDateTime.now();

    public UserDto() {
    }

    public UserDto(String key, String name, Gender gender) {
        this.key = key;
        this.name = name;
        this.gender = gender;
    }

    public static UserDto of(String key, String value, Gender gender) {
        return new UserDto(key, value, gender);
    }

    // getters and setters

}
public enum Gender {
    MAN, WOMAN
}

Для взаимодействия с нашей бизнес-логикой, добавим три контроллера: UserController, PointContoller, SecretContoller.

UserController отвечает за добавление, обновление и получение пользователей.

UserController.java

@RestController
@RequestMapping("/api/user")
public class UserController {

    private final Map<String, UserDto> repository;

    public UserController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @PutMapping(produces = APPLICATION_JSON_VALUE)
    public HttpStatus registerUser(@RequestBody UserDto userDto) {
        repository.put(userDto.getKey(), userDto);
        return HttpStatus.OK;
    }

    @PostMapping(produces = APPLICATION_JSON_VALUE)
    public HttpStatus updateUser(@RequestBody UserDto userDto) {
        if (!repository.containsKey(userDto.getKey())) return HttpStatus.NOT_FOUND;
        repository.put(userDto.getKey(), userDto);
        return HttpStatus.OK;
    }

    @GetMapping(value = "{key}", produces = APPLICATION_JSON_VALUE)
    public ResponseEntity<UserDto> getSimpleDto(@PathVariable("key") String key) {
        return ResponseEntity.ok(repository.get(key));
    }

}

PointContoller отвечает за взаимодействие с баллами пользователя. Один метод этого контроллера отвечает за добавление и удаление балов пользователям.

PointContoller.java

@RestController
@RequestMapping("api/user/point")
public class PointController {

    private final Map<String, UserDto> repository;

    public PointController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @PostMapping("{key}")
    public HttpStatus changePoints(
            @PathVariable String key,
            @RequestParam("point") Long point,
            @RequestParam("type") String type
    ) {
        final UserDto userDto = repository.get(key);
        userDto.setPoints(
                "plus".equalsIgnoreCase(type)
                    ? userDto.getPoints() + point
                    : userDto.getPoints() - point
        );
        return HttpStatus.OK;
    }

}

Метод destroy в SecretContoller может удалить всех пользователей.

SecretContoller.java

@RestController
@RequestMapping("api/secret")
public class SecretController {

    private final Map<String, UserDto> repository;

    public SecretController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @GetMapping(value = "destroy")
    public HttpStatus destroy() {
        repository.clear();
        return HttpStatus.OK;
    }

}

Настраиваем Swagger

😺
Ветка в репозитории: spring-boot-3

Теперь добавим Swagger в наш проект. Для этого добавьте следующие зависимости в pom.xml:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.0.2</version>
</dependency>
Актуальная версия в Maven Central: SpringDoc Starter

Для WebFlux используйте другую зависимость:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
    <version>2.0.2</version>
</dependency>
Актуальная версия в Maven Central: SpringDoc Starter WebFlux

Данные зависимости подходят только для проектов на SpringBoot 3. Если вы используете SpringBoot 2, то вам необходимо добавить другие зависимости:

<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.2.8</version>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.14</version>
</dependency>
Актуальные вресии в Maven Central: Swagger, SpringDoc
Документация SpringDoc для SpringBoot 3: https://springdoc.org/v2/
Документация SpringDoc для SpringBoot 2: https://springdoc.org

Swagger автоматически находит список всех контроллеров. При нажатии на любой из них будут перечислены допустимые методы HTTP (DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT).

Для каждого метода доступные следующие данные: статус ответа, тип содержимого и список параметров.

Поэтому после добавления зависимостей у нас уже есть документация, доступная по ссылке: http://localhost:8080/swagger-ui. А также есть OAS, доступный по адресу: http://localhost:8080/v3/api-docs.

Swagger запущенный с дефолтными настройками
Swagger запущенный с дефолтными настройками

Также можно вызвать каждый метод с помощью пользовательского интерфейса. Откроем метод добавления пользователей.

Пока у нас не очень информативная документация. Давайте исправим это. Для начала создадим класс конфигурации сваггера OpenApiConfig - имя произвольное.

@OpenAPIDefinition(
        info = @Info(
                title = "Loyalty System Api",
                description = "Loyalty System", version = "1.0.0",
                contact = @Contact(
                        name = "Struchkov Mark",
                        email = "mark@struchkov.dev",
                        url = "https://mark.struchkov.dev"
                )
        )
)
public class OpenApiConfig {
    
}
  • title – это название вашего приложения.
  • version – версия вашего API.
  • contact – ответственные за API.

Эти данные больше для визуальной красоты UI документации.

Разметка контроллеров

Переопределим описания контроллеров, чтобы сделать документацию понятнее. Для этого пометим контроллеры аннотацией @Tag.

@Tag(name="Название контроллера", description="Описание контролера")
public class ControllerName {

    // ... ... ... ... ...

} 
Добавили описание контроллеров в Swagger

Скрыть контроллер

У нас есть контроллер, который мы хотим скрыть – SecretController. Аннотация @Hidden поможет нам в этом.

@Hidden
@Tag(name = "Секретный контролер", description = "Позволяет удалить всех пользователей")
public class SecretController {

    // ... ... ... ... ...

}
Аннотация скрывает контроллер только из Swagger. Он все также доступен для вызова. Используйте другие методы для защиты вашего API. Например, авторизацию на основе JWT токена.

Наша документация стала намного понятнее, но давайте добавим описания для каждого метода контроллера.

Разметка методов

Аннотация @Operation описывает возможности методов контроллера. Достаточно определить следующие значения:

  • summary – короткое описание.
  • description – более полное описание.
@Operation(
	summary = "Регистрация пользователя",
	description = "Позволяет зарегистрировать пользователя"
)
public HttpStatus registerUser(@RequestBody UserDto userDto) {

    // ... ... ... ... ...

}

Разметка переменных метода

При помощи аннотации Parameter также опишем переменные в методе, который отвечает за управление баллами пользователей.

public HttpStatus changePoints(
    @PathVariable @Parameter(description = "Идентификатор пользователя") String key,
    @RequestParam("point") @Parameter(description = "Количество баллов") Long point,
    @RequestParam("type") @Parameter(description = "Тип операции") TypeOperation type
) {

    // ... ... ... ... ...

}

С помощью параметра required можно задать обязательные поля для запроса. По умолчанию все поля необязательные.

Разметка DTO

Разработчики стараются называть переменные в классе понятными именами, но не всегда это помогает. Вы можете дать человеко-понятное описание самой DTO и ее переменным с помощью аннотации @Schema.

@Schema(description = "Сущность пользователя")
public class UserDto {

    @Schema(description = "Идентификатор")
    private String key;

    // ... ... ... ... ...

}

Сваггер заполнит переменные, формат которых он понимает: enum, даты. Но если некоторые поля DTO имеют специфичный формат, то помогите разработчикам добавив пример.

@Schema(description = "Идентификатор", example = "A-124523")

Выглядеть это будет так:

Но подождите, зачем мы передаем дату регистрации. Да и уникальный ключ чаще всего будет задаваться сервером. Скроем эти поля из swagger с помощью параметра Schema.AccessMode.READ_ONLY:

public class UserDto {

    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
    private String key;

    // ... ... ... ... ...

}
Рандомный блок

Валидация

Про валидацию я подробно рассказывал в статье: "Валидация данных в SpringBoot". Здесь я лишь хочу показать, что валидация параметров методов контроллеров также отображается в Swagger.

Добавим валидацию в метод управления баллами пользователя в PointController. Мы не хотим, чтобы можно было передать отрицательные баллы.

public HttpStatus changePoints(
    // ... ... ... ... ...
    @RequestParam("point") @Min(0) @Parameter(description = "Количество баллов") Long point,
    // ... ... ... ... ...
) {

    // ... ... ... ... ...

}

Давайте посмотрим на изменения спецификации.

Для поля point появилось замечание minimum: 0. И все это нам не стоило ни малейшего дополнительного усилия.

Авторизация в Swager

😺
Ветка в репозитории: jwt-auth

Если ваш API защищен авторизаций и аутентификаций, то вы не сможете просто так вызвать запрос из Swagger. Один из самых распространенных способов авторизации это JWT, о нем я рассказывал в отдельной статье.

Сейчас нам нужно объяснить сваггеру, какая авторизация у нас применяется и какие эндпойнты ей защищены.

Авторизация с использованием JWT

В первом случае рассмотрим старый добрый JWT. Swagger должен получить access-токен и добавлять его в Header запросов.

Начнем с добавления аннотации @SecurityScheme над классом OpenApiConfig:

@OpenAPIDefinition(...)
@SecurityScheme(
        name = "JWT",
        type = SecuritySchemeType.HTTP,
        bearerFormat = "JWT",
        scheme = "bearer"
)
public class OpenApiConfig {

}

Мы описали схему авторизации с использованием JWT. Теперь пометим аннотацией @SecurityRequirement все эндпойнты, которые используют данный способ авторизации. В @SecurityRequirement в атрибуте name указываем название нашей схемы – JWT. Название должно совпадать с названием из аннотации @SecurityScheme.

@PostMapping("{key}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Управление баллами", description = "Позволяет удалить или добавить баллы пользователю")
public HttpStatus changePoints(...) {
	... ... ...
}
Пример помеченного контроллера

Запускаем приложение и открываем Swagger. Видим, что появилась кнопка Authorize. Нажмем на нее и получим возможность установить JWT токен для всех методов. Защищенные методы, которые мы пометили, будут отмечены значком замка.

Устанавливаем токен, после чего нажимаем кнопку Authorize. Переходим к защищенному эндпойнту и вызываем его.

Видим, что Swagger проставил заголовок Authorization, а также сам добавил Bearer к токену. То что нам и было нужно.

Авторизация с использованием Ouath2

😺
Ветка в репозитории: swagger-oauth2

С Oauth2 все оказалось сложнее. Проблема в том, что при авторизации с использованием Oauth2 SpringBoot генерирует JSESSIONID куку, которую сохраняет в браузере. Дальше браузер передает эту куку с каждым запросом, что позволяет SpringBoot приложению понимать кто с ним общается.

Swagger же при Oauth2 авторизации генерирует себе access token, который пытается использовать при запросе. Проблема в том, что он никак не может сам получить значение JSESSIONID, так как его генерирует Spring после успешной Oauth2 авторизации.

Если вы знаете, как решить эту проблему, пожалуйста напишите в комментариях. Я рабочего решения быстро найти не смог.

Поэтому для Ouath2 воспользуемся возможностью сваггера передавать куку.

@OpenAPIDefinition(...)
@SecurityScheme(
        name = "jsessionid",
        in = SecuritySchemeIn.COOKIE,
        type = SecuritySchemeType.APIKEY,
        paramName = "JSESSIONID"
)
public class OpenApiConfig {

}

Помечаем эндпойнты аннотацией:

@SecurityRequirement(name = "jsessionid")

Далее переходим по какому-нибудь урлу нашего API, видим Oauth2 окно авторизации. Проходим авторизацию. Теперь открываем консоль разработчика в браузере и находим раздел с куками.

Вместо настоящего Oauth2 сервера можно использовать mock-server. Который принимает любой email и любой пароль. Удобно использовать при локальной разработке. Конфигурацию подключения можно посмотреть в моей заметке.

Нас интересует кука JSESSIONID, берем ее и вставляем в окно авторизации в Swagger.

Вот и все. Это будет работать.

Итог

Этих знаний вам хватит, чтобы сделать хорошее описание API вашего сервиса. Если нужны более тонкие настройки, то вы без труда сможете разобраться, открыв документацию к аннотациям сваггера.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇