«1. Обзор

Spring REST Docs и OpenAPI 3.0 — это два способа создания документации API для REST API.

В этом уроке мы рассмотрим их относительные преимущества и недостатки.

2. Краткий обзор происхождения

Spring REST Docs — это структура, разработанная сообществом Spring для создания точной документации для RESTful API. Он использует подход, основанный на тестах, при котором документация написана либо как тесты Spring MVC, WebTestClient Spring Webflux, либо как REST-Assured.

Результат выполнения тестов создается в виде файлов AsciiDoc, которые можно объединить с помощью Asciidoctor для создания HTML-страницы с описанием наших API. Поскольку он следует методу TDD, Spring REST Docs автоматически использует все его преимущества, такие как менее подверженный ошибкам код, сокращение количества переделок и более быстрые циклы обратной связи, и это лишь некоторые из них.

OpenAPI, с другой стороны, является спецификацией, созданной на основе Swagger 2.0. Его последняя версия на момент написания этой статьи — 3.0 и имеет много известных реализаций.

Как и любая другая спецификация, OpenAPI излагает определенные основные правила, которым должны следовать его реализации. Проще говоря, все реализации OpenAPI должны создавать документацию в виде объекта JSON либо в формате JSON, либо в формате YAML.

Также существует множество инструментов, которые используют этот JSON/YAML и создают пользовательский интерфейс для визуализации и навигации по API. Это удобно, например, во время приемочного тестирования. В наших примерах кода здесь мы будем использовать springdoc — библиотеку для OpenAPI 3 с Spring Boot.

Прежде чем подробно рассмотреть их, давайте быстро настроим API для документирования.

3. REST API

Давайте соберем базовый CRUD API, используя Spring Boot.

3.1. Репозиторий

Здесь репозиторий, который мы будем использовать, представляет собой простой интерфейс PagingAndSortingRepository с моделью Foo:

@Repository
public interface FooRepository extends PagingAndSortingRepository<Foo, Long>{}

@Entity
public class Foo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    @Column(nullable = false)
    private String title;
  
    @Column()
    private String body;

    // constructor, getters and setters
}

Мы также загрузим репозиторий, используя schema.sql и данные. sql.

3.2. Контроллер

Далее давайте рассмотрим контроллер, для краткости опуская детали его реализации:

@RestController
@RequestMapping("/foo")
public class FooController {

    @Autowired
    FooRepository repository;

    @GetMapping
    public ResponseEntity<List<Foo>> getAllFoos() {
        // implementation
    }

    @GetMapping(value = "{id}")
    public ResponseEntity<Foo> getFooById(@PathVariable("id") Long id) {
        // implementation
    }

    @PostMapping
    public ResponseEntity<Foo> addFoo(@RequestBody @Valid Foo foo) {
        // implementation
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteFoo(@PathVariable("id") long id) {
        // implementation
    }

    @PutMapping("/{id}")
    public ResponseEntity<Foo> updateFoo(@PathVariable("id") long id, @RequestBody Foo foo) {
        // implementation
    }
}

3.3. Приложение

И, наконец, загрузочное приложение:

@SpringBootApplication()
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. OpenAPI / Springdoc

Теперь давайте посмотрим, как springdoc может добавить документацию к нашему Foo REST API.

Напомним, что он сгенерирует объект JSON и визуализацию пользовательского интерфейса API на основе этого объекта.

4.1. Базовый пользовательский интерфейс

Для начала мы просто добавим пару зависимостей Maven — springdoc-openapi-data-rest для генерации JSON и springdoc-openapi-ui для рендеринга пользовательского интерфейса.

Инструмент проанализирует код нашего API и прочитает аннотации методов контроллера. На этой основе он сгенерирует API JSON, который будет доступен по адресу http://localhost:8080/api-docs/. Он также будет служить базовым пользовательским интерфейсом по адресу http://localhost:8080/swagger-ui-custom.html:

Как мы видим, вообще не добавляя никакого кода, мы получили красивую визуализацию нашего API, верно? вплоть до схемы Foo. Используя кнопку «Попробовать», мы можем даже выполнять операции и просматривать результаты.

А что, если бы мы захотели добавить в API настоящую документацию? С точки зрения того, что такое API, что означают все его операции, что следует вводить и каких ответов ожидать?

Мы рассмотрим это в следующем разделе.

4.2. Подробный пользовательский интерфейс

Давайте сначала посмотрим, как добавить общее описание в API.

Для этого мы добавим bean-компонент OpenAPI в наше загрузочное приложение:

@Bean
public OpenAPI customOpenAPI(@Value("${springdoc.version}") String appVersion) {
    return new OpenAPI().info(new Info()
      .title("Foobar API")
      .version(appVersion)
      .description("This is a sample Foobar server created using springdocs - " + 
        "a library for OpenAPI 3 with spring boot.")
      .termsOfService("http://swagger.io/terms/")
      .license(new License().name("Apache 2.0")
      .url("http://springdoc.org")));
}

Затем, чтобы добавить некоторую информацию к нашим операциям API, мы украсим наши сопоставления несколькими аннотациями, специфичными для OpenAPI.

Давайте посмотрим, как мы можем описать getFooById. Мы сделаем это внутри другого контроллера, FooBarController, который похож на наш FooController:

@RestController
@RequestMapping("/foobar")
@Tag(name = "foobar", description = "the foobar API with documentation annotations")
public class FooBarController {
    @Autowired
    FooRepository repository;

    @Operation(summary = "Get a foo by foo id")
    @ApiResponses(value = {
      @ApiResponse(responseCode = "200", description = "found the foo", content = { 
        @Content(mediaType = "application/json", schema = @Schema(implementation = Foo.class))}),
      @ApiResponse(responseCode = "400", description = "Invalid id supplied", content = @Content), 
      @ApiResponse(responseCode = "404", description = "Foo not found", content = @Content) })
    @GetMapping(value = "{id}")
    public ResponseEntity getFooById(@Parameter(description = "id of foo to be searched") 
      @PathVariable("id") String id) {
        // implementation omitted for brevity
    }
    // other mappings, similarly annotated with @Operation and @ApiResponses
}

Теперь давайте посмотрим, как это повлияет на пользовательский интерфейс:

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

5. Весенняя документация по REST

«REST docs — это совершенно другой взгляд на документацию по API. Как описано ранее, процесс управляется тестами, а на выходе создается статическая HTML-страница.

В нашем примере мы будем использовать тесты Spring MVC для создания фрагментов документации.

Вначале нам нужно добавить зависимость spring-restdocs-mockmvc и подключаемый модуль asciidoc Maven в наш файл pom.

5.1. Тест JUnit5

Теперь давайте посмотрим на тест JUnit5, который включает в себя нашу документацию:

@ExtendWith({ RestDocumentationExtension.class, SpringExtension.class })
@SpringBootTest(classes = Application.class)
public class SpringRestDocsIntegrationTest {
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void setup(WebApplicationContext webApplicationContext, 
      RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
          .apply(documentationConfiguration(restDocumentation))
          .build();
    }

    @Test
    public void whenGetFooById_thenSuccessful() throws Exception {
        ConstraintDescriptions desc = new ConstraintDescriptions(Foo.class);
        this.mockMvc.perform(get("/foo/{id}", 1))
          .andExpect(status().isOk())
          .andDo(document("getAFoo", preprocessRequest(prettyPrint()), 
            preprocessResponse(prettyPrint()), 
            pathParameters(parameterWithName("id").description("id of foo to be searched")),
            responseFields(fieldWithPath("id")
              .description("The id of the foo" + 
                collectionToDelimitedString(desc.descriptionsForProperty("id"), ". ")),
              fieldWithPath("title").description("The title of the foo"), 
              fieldWithPath("body").description("The body of the foo"))));
    }

    // more test methods to cover other mappings

}

После запуска этого теста мы получаем несколько файлов в каталоге target/generated-snippets с информацией о данная операция API. В частности, когда GetFooById_thenSuccessful даст нам восемь документов в папке getAFoo в каталоге.

Вот образец http-response.adoc, конечно, содержащий тело ответа:

[source,http,options="nowrap"]
----
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 60

{
  "id" : 1,
  "title" : "Foo 1",
  "body" : "Foo body 1"
}
----

5.2. fooapi.adoc

Теперь нам нужен мастер-файл, который объединит все эти фрагменты вместе, чтобы сформировать хорошо структурированный HTML.

Давайте назовем его fooapi.adoc и посмотрим его небольшую часть:

=== Accessing the foo GET
A `GET` request is used to access the foo read.

==== Request structure
include::{snippets}/getAFoo/http-request.adoc[]

==== Path Parameters
include::{snippets}/getAFoo/path-parameters.adoc[]

==== Example response
include::{snippets}/getAFoo/http-response.adoc[]

==== CURL request
include::{snippets}/getAFoo/curl-request.adoc[]

После выполнения asciidoctor-maven-plugin мы получим окончательный HTML-файл fooapi.html в папке target/generated-docs.

А вот как это будет выглядеть при открытии в браузере:

6. Ключевые выводы

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

В Springdoc аннотации, которые нам приходилось использовать, загромождали код нашего контроллера rest и снижали его читабельность. Кроме того, документация была тесно связана с кодом и должна была попасть в производство.

Излишне говорить, что ведение документации является еще одной проблемой — если что-то в API изменится, всегда ли программист будет помнить об обновлении соответствующей аннотации OpenAPI?

С другой стороны, REST Docs не выглядит так привлекательно, как другой пользовательский интерфейс, и его нельзя использовать для приемочного тестирования. Но у него есть свои преимущества.

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

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

Также требуется больше шагов для создания окончательного HTML — сначала запустить тест, а затем плагин. Springdoc требовал от нас только запуска загрузочного приложения.

7. Заключение

В этом руководстве мы рассмотрели различия между Springdoc на основе OpenAPI и Spring REST Docs. Мы также увидели, как реализовать их для создания документации для базового API CRUD.

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

Как всегда, исходный код доступен на GitHub.