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

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

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

Создание документации вручную — процесс трудоёмкий и часто рутинный. В этой статье рассмотрим основные возможности Swagger для документирования REST API в приложении на Spring Boot.

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

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

Java 21
Spring Boot 3.3.5 | 2.7.18
SpringDoc 2.6.0 | 1.8.0
Swagger 2.2.25

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

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

Что такое Swagger?

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

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

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

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

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

Существует множество статей, описывающих генерацию Swagger UI с помощью библиотеки SpringFox. Однако я решил использовать SpringDoc по нескольким причинам.

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

SpringDoc оказался оптимальным выбором — он активно развивается и поддерживает новые версии Spring Boot, включая Spring Boot 3. Переход с SpringFox на SpringDoc прошёл быстро и успешно.

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

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

Для начала документирования API необходимо сначала создать его. Вы можете пропустить эту главу.

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

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

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, PointController и SecretController.

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));
    }

}

PointController: Управляет баллами пользователей. В этом контроллере реализован метод, который добавляет или удаляет баллы у пользователей.

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;
    }

}

SecretController: Содержит метод destroy, который может удалить всех пользователей из системы.

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.6.0</version>
</dependency>

Актуальная версия в Maven Central: SpringDoc Starter

Если используется WebFlux, потребуется другая зависимость:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
    <version>2.6.0</version>
</dependency>

Актуальная версия в Maven Central: SpringDoc Starter WebFlux

Документация SpringDoc для SpringBoot 3: https://springdoc.org
SpringBoot 2

Эти зависимости предназначены для проектов на Spring Boot 3. Если ваш проект работает на Spring Boot 2, добавьте следующие зависимости:

<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.2.25</version>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.8.0</version>
</dependency>

Актуальные вресии в Maven Central: Swagger, SpringDoc

После добавления зависимостей документация будет доступна по адресу: http://localhost:8080/swagger-ui/index.html, а спецификация OAS — по адресу: http://localhost:8080/v3/api-docs.

Swagger автоматически обнаруживает все контроллеры, отображая доступные HTTP-методы для каждого. Для каждого HTTP-метода доступна информация о статусе ответа, типе содержимого и параметрах.

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

Swagger также позволяет вызывать методы API через пользовательский интерфейс. Можно вызвать каждый метод с помощью пользовательского интерфейса.

Однако, по умолчанию, документация может выглядеть недостаточно информативно. Чтобы улучшить её, создадим конфигурационный класс Swagger — OpenApiConfig:

@OpenAPIDefinition(
        info = @Info(
                title = "Loyalty System Api",
                description = "API системы лояльности", 
                version = "1.0.0",
                contact = @Contact(
                        name = "Struchkov Mark",
                        email = "mark@struchkov.dev",
                        url = "https://mark.struchkov.dev"
                )
        )
)
public class OpenApiConfig {
    // Конфигурация для Swagger
}
  • title — название приложения;
  • version — версия API;
  • contact — информация об ответственных за API.

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

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

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

Чтобы сделать документацию понятнее, переопределим описания контроллеров, используя аннотацию @Tag:

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

Кроме того, @Tag поддерживает параметр externalDocs, который позволяет добавить ссылку на внешнюю документацию, связанную с контроллером. Это может быть полезным, если API требует ссылки на общие технические документы, дополнительные спецификации или инструкции. Пример:

@Tag(
    name = "User Controller",
    description = "Контроллер для управления пользователями",
    externalDocs = @ExternalDocumentation(
        description = "Ссылка на общую документацию",
        url = "https://example.com/docs/user-controller"
    )
)
public class UserController {
    // ... Контент контроллера ...
}

Скрытие контроллера

Если нужно скрыть контроллер из документации Swagger, можно воспользоваться аннотацией @Hidden. Например, спрячем SecretController:

@Hidden
@Tag(name = "Секретный контролер", description = "Позволяет удалить всех пользователей")
public class SecretController {
// Контент контроллера
}
@Hidden скрывает элементы только из интерфейса Swagger, но не делает их недоступными для вызова. Для обеспечения безопасности используйте методы аутентификации и авторизации, такие как JWT-токены, OAuth2 или API-ключи, которые обеспечат доступ только для авторизованных пользователей.

@Hidden позволяет скрыть из Swagger не только контроллеры целиком, но также отдельные эндпойнты.

@Hidden
@PutMapping(produces = APPLICATION_JSON_VALUE)
@Operation(summary = "Регистрация пользователя", description = "Позволяет зарегистрировать пользователя")
public HttpStatus registerUser(@RequestBody UserDto userDto) {
    userDto.setPoints(0L);
    repository.put(userDto.getKey(), userDto);
    return HttpStatus.OK;
}

После добавления аннотаций документация становится гораздо понятнее. Теперь добавим описания для каждого метода контроллера.

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

Для описания методов контроллеров используется аннотация @Operation. Она позволяет указать:

  • summary — краткое описание метода.
  • description — более развернутое описание.

Пример аннотации метода для регистрации пользователя:

@Operation(
	summary = "Регистрация пользователя",
	description = "Позволяет зарегистрировать пользователя"
)
public HttpStatus registerUser(@RequestBody UserDto userDto) {
    // Реализация метода
}

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

Для описания параметров эндпойнтов используем аннотацию @Parameter, которая позволяет уточнить детали каждого параметра.

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

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

Для более точного описания значений параметров можно использовать атрибут example. Этот атрибут помогает задать пример значения. Например, в этом коде example = "100" указывает пример значения для параметра point, а example = "PLUS" — для параметра type, что наглядно демонстрирует возможные значения, которые API ожидает получить.

Разметка DTO

Чтобы сделать документацию ещё более понятной, можно добавить человеко-понятные описания для самой DTO и её полей, используя аннотацию @Schema. Даже если переменные в классе названы понятно, дополнительные пояснения помогут пользователям API быстрее ориентироваться в данных.

Пример базовой разметки для UserDto выглядит так:

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

    @Schema(description = "Уникальный идентификатор пользователя", example = "A-124523", accessMode = Schema.AccessMode.READ_ONLY)
    private String key;

    @Schema(description = "ФИО", example = "Иванов Иван Иванович")
    private String name;

    @Schema(description = "Баллы пользователя", example = "0", accessMode = Schema.AccessMode.READ_ONLY)
    private Long points = 0L;

    @Schema(description = "Дата регистрации пользователя", example = "2023-01-01", accessMode = Schema.AccessMode.READ_ONLY)
    private LocalDate registrationDate;

    @Schema(description = "Статус пользователя", allowableValues = {"ACTIVE", "INACTIVE", "BANNED"})
    private String status;

    // Вложенная структура для контактных данных
    @Schema(description = "Контактные данные пользователя")
    private ContactInfo contactInfo;

    // ... другие поля ...
}

Swagger автоматически определяет формат для стандартных типов данных, таких как enum и даты. Однако, для полей со специфичным форматом желательно добавлять примеры значений через параметр example. Например, в данном коде example = "A-124523" показывает, как может выглядеть key, а example = "0" — формат для поля points. Выглядеть это будет так:

Если в DTO есть поля, которые пользователю вводить не нужно , их можно скрыть из интерфейса ввода в Swagger с помощью accessMode = Schema.AccessMode.READ_ONLY. Это обозначает, что поле будет отображаться только для чтения и Swagger не будет запрашивать его при отправке запроса.

Для полей, имеющих ограниченные значения, например, status, можно использовать параметр allowableValues, который укажет пользователям API допустимые значения. Для enum allowableValues Swager сформирует сам.

Кроме того, если DTO содержит вложенные объекты (например, ContactInfo в UserDto), желательно также разметить вложенные классы с помощью @Schema, чтобы Swagger отобразил их структуру и описание всех уровней вложенности.

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

Валидация

О валидации данных я подробно рассказывал в статье “Валидация данных в Spring Boot”. Здесь же хочу показать, что при добавлении валидации для параметров методов контроллеров эти ограничения также автоматически отображаются в Swagger.

Добавим валидацию в метод управления баллами пользователя в PointController. Например, нам нужно, чтобы значение баллов не могло быть отрицательным. Для этого используем аннотацию @Min с минимальным значением 0:

public HttpStatus changePoints(
    // ... другие параметры ...
    @RequestParam("point") @Min(0) @Parameter(description = "Количество баллов") Long point
    // ... другие параметры ...
) {
    // Реализация метода
}

Теперь, благодаря валидации, Swagger автоматически добавляет в спецификацию для поля point ограничение minimum: 0. Пользователи API сразу видят требование к этому параметру.

Можно также указать коды ошибок, которые возвращаются при нарушении правил валидации. Например, добавим описание ответа с кодом 400 (ошибка ввода) с помощью аннотации @ApiResponse:

@Operation(summary = "Изменение баллов пользователя")
@ApiResponse(responseCode = "400", description = "Неверный запрос — количество баллов должно быть неотрицательным")
public HttpStatus changePoints(
    @RequestParam("point") @Min(value = 0, message = "Количество баллов не может быть отрицательным") 
    @Parameter(description = "Количество баллов") Long point
) {
    // Реализация метода
}

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

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

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

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

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

Для авторизации через JWT Swagger должен получать access-токен и автоматически добавлять его в заголовок Authorization каждого запроса. Начнём с описания схемы авторизации в конфигурационном классе Swagger, добавив аннотацию @SecurityScheme над классом OpenApiConfig:

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

}

Здесь мы определили схему авторизации с использованием JWT, указав, что токен имеет формат bearer и будет использоваться в заголовке запроса. Теперь Swagger знает, что для авторизации потребуется токен JWT.

После этого помечаем аннотацией @SecurityRequirement все эндпойнты, которые требуют авторизации. В атрибуте name аннотации @SecurityRequirement указываем название схемы авторизации — в данном случае "JWT", что должно соответствовать значению, указанному в аннотации @SecurityScheme:

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

Пример помеченного контроллера

Запустив приложение и открыв Swagger, мы увидим, что в интерфейсе появилась кнопка Authorize. Нажав на неё, можно ввести JWT-токен, который будет автоматически добавляться ко всем защищённым методам. Методы, защищённые аннотацией @SecurityRequirement, теперь будут отмечены значком замка.

После установки токена, перейдите к защищённому эндпойнту и вызовите его. Swagger добавит заголовок Authorization со значением токена и автоматически проставит перед ним Bearer, как того требует спецификация JWT.

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

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

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

Работа с OAuth2 в Swagger несколько сложнее, чем с JWT, из-за особенностей авторизации в Spring Boot. При OAuth2-авторизации Spring Boot создаёт JSESSIONID — специальную куку, которая сохраняется в браузере и передаётся с каждым запросом. Это позволяет приложению идентифицировать пользователя.

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

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

⁉️
Если вы знаете способы решения этой проблемы, пожалуйста, поделитесь ими — пока мне не удалось найти стабильное решение.

Чтобы использовать JSESSIONID для авторизации, укажем Swagger, что он должен передавать его в виде куки. Добавим аннотацию @SecurityScheme в конфигурационный класс OpenApiConfig, настроив её для передачи JSESSIONID в запросах:

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

}

В этом примере мы указываем, что схема безопасности jsessionid использует куку JSESSIONID для передачи токена. Теперь Swagger знает, что для авторизации будет использоваться именно эта кука.

Затем помечаем защищённые эндпойнты аннотацией @SecurityRequirement, указав в атрибуте name название схемы — jsessionid:

@SecurityRequirement(name = "jsessionid")

Для прохождения авторизации переходим на любой URL вашего API, требующий авторизации, и видим окно авторизации OAuth2. После успешного входа JSESSIONID будет сохранён в браузере.

💡
На этапе локальной разработки вместо реального OAuth2-сервера можно использовать mock-сервер, который принимает любые email и пароль. Такой подход упрощает тестирование, а пример конфигурации mock-сервера можно найти в моей заметке.

Откройте консоль разработчика в браузере: Найдите раздел с куками и отыщите куку JSESSIONID.

Скопируйте значение JSESSIONID: Откройте окно авторизации в Swagger и вставьте значение JSESSIONID в соответствующее поле.

Теперь, когда JSESSIONID установлен, Swagger будет передавать его в каждом запросе к защищённым эндпойнтам, обеспечивая корректную аутентификацию.

Заключение

В этой статье мы рассмотрели, как настроить и документировать API в Spring Boot, используя Swagger для упрощённого взаимодействия с разработчиками и тестирования защищённых эндпойнтов. Мы начали с основ настройки Swagger и аннотаций для документирования контроллеров, DTO и параметров методов. Затем добавили валидацию данных, которая не только усиливает защиту приложения, но и помогает пользователям API понимать ограничения при работе с запросами.

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