Документирование API в Java приложении с помощью Swagger и OpenAPI 3.0

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


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


    Статья в моем блоге: Документирование API с помощью OpenAPI 3 и Swagger

    Что такое Swagger?


    Swagger автоматически генерирует документацию API в виде json. А проект Springdoc создаст удобный UI для визуализации. Вы не только сможете просматривать документацию, но и отправлять запросы, и получать ответы.


    Также возможно сгенерировать непосредственно клиента или сервер по спецификации API Swagger, для этого нужен генератор кода Swagger-Codegen.


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


    Вы найдете все примеры представленные тут в моем репозитории.


    Создание REST API


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


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


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


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


    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 отвечает за добавление, обновление и получение пользователей.


    @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 отвечает за взаимодействие с баллами пользователя. Один метод этого контроллера отвечает за добавление и удаление балов пользователям.


    @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,
                @RequestPart("point") Long point,
                @RequestPart("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 может удалить всех пользователей.


    @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


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


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

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


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


    Поэтому после добавления зависимостей, у нас уже есть документация. Чтобы убедиться в этом, переходим по адресу: localhost:8080/swagger-ui.html


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


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



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


    Для начала создадим класс конфигурации сваггера SwaggerConfig — имя произвольное.


    @Configuration
    public class SwaggerConfig {
    
        @Bean
        public OpenAPI customOpenAPI() {
            return new OpenAPI()
                    .info(
                            new Info()
                                    .title("Example Swagger Api")
                                    .version("1.0.0")
                    );
        }
    
    }

    • title — это название вашего приложения
    • version — версия вашего API

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


    Добавление авторов


    Добавьте разработчиков API, чтобы было понятно, кто в ответе за это безобразие


    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(
                        new Info()
                                .title("Loyalty System Api")
                                .version("1.0.0")
                                .contact(
                                        new Contact()
                                                .email("me@upagge.ru")
                                                .url("https://uPagge.ru")
                                                .name("Struchkov Mark")
                                )
                );
    }

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


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


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

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


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


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


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

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


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


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


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


    • summary — короткое описание.
    • description — более полное описание.

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

    Метод с аннотацией Operation


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


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


    public HttpStatus changePoints(
        @PathVariable @Parameter(description = "Идентификатор пользователя") String key,
        @RequestPart("point") @Parameter(description = "Количество баллов") Long point,
        @RequestPart("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")

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


    Разметка аннотацией Schema


    Разметка аннотацией Schema


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


    public class UserDto {
    
        @Schema(accessMode = Schema.AccessMode.READ_ONLY)
        private String key;
    
        ...
    
    }

    Валидация


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


    Подробнее о валидации данных в этой статье.


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

    Давайте посмотрим на изменения спецификации. Для поля point появилось замечание minimum: 0.


    Валидация в swagger


    И все это нам не стоило ни малейшего дополнительного усилия.


    Итог


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


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

    Only registered users can participate in poll. Log in, please.

    Документируете свой API?

    • 84.2%Да69
    • 15.8%Нет13

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 22

      +2
      Немножко напрягает, что нужно комментировать код моделей два раза: Javadoc для IDE и `@Schema` для Swagger/OpenAPI:
      /**
       * Сущность пользователя.
       */
      @Schema(description = "Сущность пользователя")
      public class UserDto {
          /** Идентификатор. */
          @Schema(description = "Идентификатор")
          private String key;
      
          // ... ... ... ... ...
      
      }
      

      Есть swagger-doclet и springfox-javadoc, но они годами не обновлялись. enunciate позволяет генерировать Swagger/OpenAPI на основании Javadoc, но он относительно сложен в настройке и не имеет нормальной документации.
        0
        Дублирование — это небольшая плата, которую придеться заплатить. На работе мы обходимся только документированием в swagger, в таком случае.

        Вроде самый актуальный способ создания документации это swagger-annotations и springdoc-openapi-ui. Может я не прав и кто-то подскажет способ лучше :)
          0

          Можно сразу описывать спецификацию swagger/openapi 3 и по ней генерировать модели и контроллеры

        +1
        Для спринга есть springfox
          0
          Как альтернатива да. Но мы в итоге перешли на Springdoc, потому что в те времена springfox не обновлялся, и имел некоторые проблемы. Не знаю как сейчас, возможно все в порядке
            0
            уж лучше быть уверенным, что в один момент не забросят и Springdoc в этом плане как-то надежнее. Хотя и со springfox сейчас уже норм всё.
              0
              Да и никто не гарантирует, что Springdoc в итоге не забросят :D
          +2

          Поправьте в заголовке и тексте статьи — Swagger V3 на OpenAPI 3.0, т.к. последней версией Swagger была вторая, а вы похоже подразумеваете именно OpenAPI-спецификацию третьей версии.

            0
            По идее OpenAPI это спецификация, а реализация уже за Swagger. Но я чутка изменил заголовок)
            0
            А каким образом лучше управлять/группировать автоматизированно такую документацию для группы миксервисов, например? Интересно, как решают такую проблему.
              0

              Zuul+swagger.

                +1
                Вообще, если смотреть с технической точки зрения, то вся конфигурация выдается в виде json. А то, что там есть и визуальная часть это бонус. Но этот бонус можно запустить отдельно, указав ему эндпоинты всех json, и вы получите общую доку по всем микросервисам на одной странице (там вверху выпадайка — выбор конфигурации).
                0
                А можно просто javadoc использовать или это нынче не модно?
                  +2

                  javadoc не говорит о HTTP методе, не особо нужен клиентам на Go/Python/PHP и других.


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

                  0

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


                  OpenAPI предназначен для описания спецификации и содержит возможности для обработки этой спецификации программно. То есть писать код из которого соберётся спека, по меньшей мере странно.


                  Простите за резкое высказывание.

                    0

                    Хм, интересно, а почему существует возможность так сделать
                    Если это настолько тупо.

                      +1

                      Факт существования решения не делает его удачным. Если смотреть с точки зрения упрощения процесса разработки, то придраться можно только к громоздким конструкциям в коде.
                      Но OpenAPI (Swagger) — спецификация Web интерфейса, в котором описаны все соглашения, принимаемые в рамках данного интерфейса.
                      Как минимум, описанию этих соглашений не место в коде, который эти же соглашения реализует. Хотя бы по той причине, что эти вещи лежат на совершенно разных уровнях абстракции.

                    0
                    Немножко не по теме вопрос, но почему registerUser — это Put, а updateUser — это Post?
                      0

                      Обычно post это создание, а put это обновление. То что сейчас наоборот, это рудимент. Изначально в примере я передавал все данные сущности, вместе с идентификатором. В таком случае рекомендуется использовать PUT, потому что он идемпотентен. Обновлю примеры, как будет время :)


                      Да и REST не такой строгий, многие делают как им нравится. Да, есть спецификации, но по факту кто во что горазд. Кто-то испольузет /books/1, а кто-то /book/1.

                        0
                        Спасибо большое за заметку об идемпотентности, но следуя вашей логике получается, что updateUser уже не идемпотентен. Так ли это?
                          +1

                          PUT это не совсем обновление, семантически это присваивание. По спецификации он предназначен для полной замены представления ресурса на новое, а частичные обновления являются нарушением.
                          https://greenbytes.de/tech/webdav/rfc7231.html#PUT

                            0

                            Да, все верно)

                      Only users with full accounts can post comments. Log in, please.