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