Spring REST Docs 与 OpenAPI 的比较

1、概览

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

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

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

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

3.1、Repository

在这里,我们使用的 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

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

@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、Application

最后是启动类:

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

4. OpenAPI / Springdoc

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

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

4.1、基础的 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

让我们先看看如何为 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 = "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
}

现在让我们看看 UI 效果:

OpenAPI 更详细的文档

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

5. Spring REST Docs

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

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

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

5.1、JUnit5 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("/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
}

运行该测试后,我们将在 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

现在,我们需要一个主文件,将所有这些片段整合在一起,形成一个结构良好的 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、孰优孰劣

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

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

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

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

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

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

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

7、总结

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

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


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