1、概览 {#1概览}
Spring REST Docs 和 OpenAPI 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.sql
和 data.sql
加载 repository。
3.2、Controller {#32controller}
接下来,让我们来看看 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 {#33application}
最后是启动类:
@SpringBootApplication()
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 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 界面:
正如我们所看到的,无需添加任何代码,我们就能获得一个漂亮的可视化 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 = "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 效果:
因此,有了这些最基本的配置,我们 API 的用户现在就可以了解它的内容、使用方法和预期结果。我们所要做的就是编译代码和运行应用。
- 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("/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 {#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
。
这就是它在浏览器中打开时的样子:
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/