51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring REST Docs 与 OpenAPI 的比较

1、概览 {#1概览}

Spring REST DocsOpenAPI 3.0 是为 REST API 创建 API 文档的两种方法。

在本教程中,我们将探讨它们的相对优缺点。

2、前世今生 {#2前世今生}

Spring REST Docs 是由 Spring 社区开发的一个框架,用于 为RESTful API 创建准确的文档。它采用了测试驱动的方法,文档可用 Spring MVC tests、Spring Webflux 的 WebTestClient 或 REST-Assured 形式编写。

运行测试的结果会生成 AsciiDoc 文件,可以使用 Asciidoctor 将它们组合在一起,生成描述 API 的 HTML 页面。由于它遵循 TDD 方法,Spring REST Docs 自动带来了许多优势,例如减少代码错误、减少重复工作和更快的反馈周期等。

而,OpenAPI 是一种诞生于 Swagger 2.0 的规范。截至本文撰写时,其最新版本为 3.0,并有许多已知的 实现

与其他规范一样,OpenAPI 也为其实现制定了一些基本规则。简而言之,所有 OpenAPI 实现都应该以 JSON 或 YAML 格式的 JSON 对象生成文档。

还有 许多工具 可以接收 JSON/YAML,并输出 UI 界面来可视化和导航 API。这在验收测试时非常有用。在这里的代码示例中,我们将使用 Springdoc - 一个用于 OpenAPI 3 和 Spring Boot 的框架。

在详细了解两者之前,让我们先快速设置一个要生成文档的API。

3、REST API {#3rest-api}

让我们使用 Spring Boot 创建一个基本的 CRUD API。

3.1、Repository {#31repository}

在这里,我们使用的 FooRepository 是一个继承了 PagingAndSortingRepository 的简单接口:

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

// 构造函数、get、set 方法省略

}

我们还将使用 schema.sqldata.sql 加载 repository。

3.2、Controller {#32controller}

接下来,让我们来看看 controller,为简洁起见,省略其实现细节:

@RestController
@RequestMapping("/foo")
public class FooController {
@Autowired
FooRepository repository;

@GetMapping public ResponseEntity&lt;List&lt;Foo&gt;&gt; getAllFoos() { // implementation }

@GetMapping(value = &quot;{id}&quot;) public ResponseEntity&lt;Foo&gt; getFooById(@PathVariable(&quot;id&quot;) Long id) { // implementation }

@PostMapping public ResponseEntity&lt;Foo&gt; addFoo(@RequestBody @Valid Foo foo) { // implementation }

@DeleteMapping(&quot;/{id}&quot;) public ResponseEntity&lt;Void&gt; deleteFoo(@PathVariable(&quot;id&quot;) long id) { // implementation }

@PutMapping(&quot;/{id}&quot;) public ResponseEntity&lt;Foo&gt; updateFoo(@PathVariable(&quot;id&quot;) long id, @RequestBody Foo foo) { // implementation }

}

3.3、Application {#33application}

最后是启动类:

@SpringBootApplication()
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. OpenAPI / Springdoc {#4-openapi--springdoc}

现在,让我们看看 Springdoc 如何为我们的 Foo REST API 添加文档。

它将生成一个 JSON 对象和基于该对象的 API UI 界面。

4.1、基础的 UI {#41基础的-ui}

首先,我们只需添加几个 Maven 依赖项:springdoc-openapi-data-rest 用于生成 JSON,springdoc-openapi-ui 用于渲染用户界面。

该工具将内省我们 API 的代码,并读取 controller 方法的注解。在此基础上,它将生成 API JSON,并在 http://localhost:8080/api-docs/ 上发布。它还将在 http://localhost:8080/swagger-ui-custom.html 上提供基本的 UI 界面:

openapi 文档界面

正如我们所看到的,无需添加任何代码,我们就能获得一个漂亮的可视化 API 文档,甚至包括 Foo schema。使用 "Try it out" 按钮,我们甚至可以执行操作并查看结果。

现在,如果我们想为 API 添加一些真正的文档呢?关于 API 的所有内容、所有操作的含义、应输入的内容以及预期的响应?

我们将在下一节讨论这个问题。

4.2、详细的 UI {#42详细的-ui}

让我们先看看如何为 API 添加一般性的描述。

为此,我们将在 Boot 应用程序中添加一个 OpenAPI Bean:

@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。我们在另一个 controller FooBarController 中进行描述,它与我们的 FooController 类似:

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

}

现在让我们看看 UI 效果:

OpenAPI 更详细的文档

因此,有了这些最基本的配置,我们 API 的用户现在就可以了解它的内容、使用方法和预期结果。我们所要做的就是编译代码和运行应用。

  1. Spring REST Docs {#5-spring-rest-docs}

REST docs 是一种完全不同的 API 文档。如前所述,其过程是测试驱动的,输出是静态 HTML 页面的形式。

在本示例中,我们将使用 Spring MVC Tests 创建文档片段。

首先,我们需要在 pom 中添加 spring-restdocs-mockmvc 依赖项和 asciidoc Maven 插件。

5.1、JUnit5 Test {#51junit5-test}

现在让我们来看看包含文档的 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(&quot;/foo/{id}&quot;, 1)) .andExpect(status().isOk()) .andDo(document(&quot;getAFoo&quot;, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), pathParameters(parameterWithName(&quot;id&quot;).description(&quot;id of foo to be searched&quot;)), responseFields(fieldWithPath(&quot;id&quot;) .description(&quot;The id of the foo&quot; + collectionToDelimitedString(desc.descriptionsForProperty(&quot;id&quot;), &quot;. &quot;)), fieldWithPath(&quot;title&quot;).description(&quot;The title of the foo&quot;), fieldWithPath(&quot;body&quot;).description(&quot;The body of the foo&quot;)))); }

// more test methods to cover other mappings

}

运行该测试后,我们将在 targets/generated-snippets 目录中获得多个文件,其中包含给定 API 操作的相关信息。特别是,whenGetFooById_thenSuccessful 会在该目录下的 getAFoo 文件夹中生成 8 个 adoc。

下面是一个 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 {#52fooapiadoc}

现在,我们需要一个主文件,将所有这些片段整合在一起,形成一个结构良好的 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 后,我们在 target/generated-docs 文件夹中得到了最终的 HTML 文件 fooapi.html

这就是它在浏览器中打开时的样子:

Spring Rest Docs 文档

6、孰优孰劣 {#6孰优孰劣}

现在,我们已经了解了这两种实现,让我们来总结一下它们的优缺点。

使用 springdoc 时,我们必须使用注解,这会使 rest controller 的代码变得杂乱无章,降低其可读性。此外,文档与代码紧密耦合。

毋庸置疑,维护文档是另一项挑战 - 如果 API 中的某些内容发生了变化,程序员是否会始终记得更新相应的 OpenAPI 注解?

另一方面,REST Docs 既不像其他 UI 界面那样引人注目,也不能用于验收测试。但它也有自己的优势。

值得注意的是,Spring MVC 测试的成功完成不仅为我们提供了测试片段,还像其他单元测试一样验证了我们的 API。这就迫使我们根据 API 的修改(如果有的话)对文档进行相应的修改。此外,文档代码与实现是完全独立的。

但反过来说,我们也不得不编写更多的代码来生成文档。首先是测试本身,它可以说与 OpenAPI 注解一样冗长;其次是主 adoc 文件。

生成最终的 HTML 还需要更多步骤 - 先运行测试,然后再运行插件。Springdoc 只要求我们运行 main 方法。

7、总结 {#7总结}

在本教程中,我们了解了基于 OpenAPI 的 springdoc 与 Spring REST Docs 之间的区别,以及如何使用它们为基本的 CRUD API 生成文档。

总之,二者各有利弊,使用其中一种还是另一种取决于我们的具体需求。


参考:http://localhost:1313/spring-rest-docs-vs-openapi/

赞(7)
未经允许不得转载:工具盒子 » Spring REST Docs 与 OpenAPI 的比较