Перейти к основному содержимому

Spring Документы REST против OpenAPI

· 8 мин. чтения

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 и data.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 может добавить документацию в наш 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 :

./acea8660e136828a4ee71a6b5808cc0c.png

Как видим, вообще без добавления кода мы получили красивую визуализацию нашего 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
}

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

./6f07fd6bfb3fd74943f4e7419297db4c.png

Таким образом, с этими минимальными конфигурациями пользователь нашего 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. В частности, whenGetFooById_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 .

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

./5509b54f828406fa4a67d83bcec06e1d.png

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

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

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

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

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

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

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

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

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

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

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

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