Using OpenAPI and Swagger UI

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

在本指南中,我们将创建一个简单的 REST 应用程序,以展示如何快速显示您的 API 规范以及如何从用户界面中受益以对其进行测试。

Solution

我们建议你按照后续章节中的说明,逐步创建应用程序。但是,你可以直接跳到已完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

此解决方案位于 openapi-swaggerui-quickstart directory

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

Expose a REST Resource

我们将创建 Fruit bean 和 FruitResource REST 资源(如果您想了解有关如何使用 Quarkus 构建 REST API 的更多详细信息,请随时查阅 Writing JSON REST services guide)。

package org.acme.openapi.swaggerui;

public class Fruit {

    public String name;
    public String description;

    public Fruit() {
    }

    public Fruit(String name, String description) {
        this.name = name;
        this.description = description;
    }
}
package org.acme.openapi.swaggerui;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;

@Path("/fruits")
public class FruitResource {

    private Set<Fruit> fruits = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));

    public FruitResource() {
        fruits.add(new Fruit("Apple", "Winter fruit"));
        fruits.add(new Fruit("Pineapple", "Tropical fruit"));
    }

    @GET
    public Set<Fruit> list() {
        return fruits;
    }

    @POST
    public Set<Fruit> add(Fruit fruit) {
        fruits.add(fruit);
        return fruits;
    }

    @DELETE
    public Set<Fruit> delete(Fruit fruit) {
        fruits.removeIf(existingFruit -> existingFruit.name.contentEquals(fruit.name));
        return fruits;
    }
}

您还可以创建一个测试:

package org.acme.openapi.swaggerui;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import jakarta.ws.rs.core.MediaType;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;

@QuarkusTest
public class FruitResourceTest {

    @Test
    public void testList() {
        given()
                .when().get("/fruits")
                .then()
                .statusCode(200)
                .body("$.size()", is(2),
                        "name", containsInAnyOrder("Apple", "Pineapple"),
                        "description", containsInAnyOrder("Winter fruit", "Tropical fruit"));
    }

    @Test
    public void testAdd() {
        given()
                .body("{\"name\": \"Pear\", \"description\": \"Winter fruit\"}")
                .header("Content-Type", MediaType.APPLICATION_JSON)
                .when()
                .post("/fruits")
                .then()
                .statusCode(200)
                .body("$.size()", is(3),
                        "name", containsInAnyOrder("Apple", "Pineapple", "Pear"),
                        "description", containsInAnyOrder("Winter fruit", "Tropical fruit", "Winter fruit"));

        given()
                .body("{\"name\": \"Pear\", \"description\": \"Winter fruit\"}")
                .header("Content-Type", MediaType.APPLICATION_JSON)
                .when()
                .delete("/fruits")
                .then()
                .statusCode(200)
                .body("$.size()", is(2),
                        "name", containsInAnyOrder("Apple", "Pineapple"),
                        "description", containsInAnyOrder("Winter fruit", "Tropical fruit"));
    }
}

Expose OpenAPI Specifications

Quarkus 提供符合 MicroProfile OpenAPI 规范的 Smallrye OpenAPI 扩展,以便生成您的 API OpenAPI v3 specification

您只需要将 openapi 扩展添加到您的 Quarkus 应用程序:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到构建文件中:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-openapi")

现在,我们准备运行我们的应用程序:

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

应用程序启动后,您可以向默认 /q/openapi 终结点发出请求:

$ curl http://localhost:8080/q/openapi
openapi: 3.0.3
info:
  title: Generated API
  version: "1.0"
paths:
  /fruits:
    get:
      responses:
        200:
          description: OK
          content:
            application/json: {}
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fruit'
      responses:
        200:
          description: OK
          content:
            application/json: {}
    delete:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fruit'
      responses:
        200:
          description: OK
          content:
            application/json: {}
components:
  schemas:
    Fruit:
      properties:
        description:
          type: string
        name:
          type: string

如果您不喜欢默认终结点位置 /q/openapi,可以通过将以下配置添加到 application.properties 中来更改它:

quarkus.smallrye-openapi.path=/swagger

你可以使用 `format`查询参数申请 JSON 格式的 OpenAPI。例如:

/q/openapi?format=json

CTRL+C 停止应用程序。

Providing Application Level OpenAPI Annotations

有一些 MicroProfile OpenAPI 注解描述了全局 API 信息,例如以下这些:

  • API Title

  • API Description

  • Version

  • Contact Information

  • License

所有这些信息(以及更多信息)都可以通过在 Jakarta REST Application`类中使用相应的 OpenAPI 注解包含在你的 Java 代码中。由于 Quarkus 中不需要 Jakarta REST `Application`类,你可能会需要创建一个。它可以简单地是一个扩展 `jakarta.ws.rs.core.Application`的空类。此空类随后可以使用各种 OpenAPI 注解(例如 `@OpenAPIDefinition)进行注解。例如:

@OpenAPIDefinition(
    tags = {
            @Tag(name="widget", description="Widget operations."),
            @Tag(name="gasket", description="Operations related to gaskets")
    },
    info = @Info(
        title="Example API",
        version = "1.0.1",
        contact = @Contact(
            name = "Example API Support",
            url = "http://exampleurl.com/contact",
            email = "techsupport@example.com"),
        license = @License(
            name = "Apache 2.0",
            url = "https://www.apache.org/licenses/LICENSE-2.0.html"))
)
public class ExampleApiApplication extends Application {
}

另一个选择是使用配置添加此全局 API 信息,这是 SmallRye 提供的一项功能,不属于规范的一部分。通过这种方式,你无需使用 Jakarta REST `Application`类,并且可以针对每个环境对 API 采用不同的命名。

例如,将以下内容添加到 `application.properties`中:

quarkus.smallrye-openapi.info-title=Example API
%dev.quarkus.smallrye-openapi.info-title=Example API (development)
%test.quarkus.smallrye-openapi.info-title=Example API (test)
quarkus.smallrye-openapi.info-version=1.0.1
quarkus.smallrye-openapi.info-description=Just an example service
quarkus.smallrye-openapi.info-terms-of-service=Your terms here
quarkus.smallrye-openapi.info-contact-email=techsupport@example.com
quarkus.smallrye-openapi.info-contact-name=Example API Support
quarkus.smallrye-openapi.info-contact-url=http://exampleurl.com/contact
quarkus.smallrye-openapi.info-license-name=Apache 2.0
quarkus.smallrye-openapi.info-license-url=https://www.apache.org/licenses/LICENSE-2.0.html

这将提供与上述 `@OpenAPIDefinition`示例类似的信息。

Enhancing the OpenAPI Schema with Filters

你可以使用一个或多个过滤器更改生成的 OpenAPI 模式。这些过滤器可以在构建时或运行时运行。

/**
 * Filter to add custom elements
 */
@OpenApiFilter(OpenApiFilter.RunStage.BUILD) (1)
public class MyBuildTimeFilter implements OASFilter { (2)

    private IndexView view;

    public MyBuildTimeFilter(IndexView view) { (3)
        this.view = view;
    }

    @Override
    public void filterOpenAPI(OpenAPI openAPI) { (4)
        Collection<ClassInfo> knownClasses = this.view.getKnownClasses();
        Info info = OASFactory.createInfo();
        info.setDescription("Created from Annotated Buildtime filter with " + knownClasses.size() + " known indexed classes");
        openAPI.setInfo(info);
    }

}
1 使用 `@OpenApiFilter`和运行阶段 (BUILD、RUN、BOTH) 标注方法
2 实现 OASFilter 并覆盖任何方法
3 对于构建阶段过滤器,索引可以传入(可选)
4 获取生成的 `OpenAPI`模式,并根据需要进行优化

请记住,在模式上设置字段将覆盖所生成的模式,你可能会想要获取和添加到(即修改)。还要注意,生成的值可能是空值,因此你必须进行检查。

Runtime filters

默认情况下,运行时过滤器在启动时运行(在最终的 OpenAPI 文档创建时)。你可以更改运行时过滤器,使其在每次请求时运行,从而使 openapi 文档具有动态性。要执行此操作,你需要设置以下属性: quarkus.smallrye-openapi.always-run-filter=true

Loading OpenAPI Schema From Static Files

除了从注解扫描中动态创建 OpenAPI 模式外,Quarkus 还支持提供静态 OpenAPI 文档。要提供的静态文件必须是符合 OpenAPI specification的有效文档。符合 OpenAPI 规范的 OpenAPI 文档本身就是一个有效的 JSON 对象,可以用 `yaml`或 `json`格式表示。

为了在操作中看到这一点,我们会在 `META-INF/openapi.yaml`中为 `/fruits`端点放置 OpenAPI 文档。如果你喜欢的话,Quarkus 也支持备用 OpenAPI document paths

openapi: 3.0.1
info:
  title: Static OpenAPI document of fruits resource
  description: Fruit resources Open API documentation
  version: "1.0"

servers:
  - url: http://localhost:8080/q/openapi
    description: Optional dev mode server description

paths:
  /fruits:
    get:
      responses:
        200:
          description: OK - fruits list
          content:
            application/json: {}
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fruit'
      responses:
        200:
          description: new fruit resource created
          content:
            application/json: {}
    delete:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fruit'
      responses:
        200:
          description: OK - fruit resource deleted
          content:
            application/json: {}
components:
  schemas:
    Fruit:
      properties:
        description:
          type: string
        name:
          type: string

默认情况下,对 `/q/openapi`发出的请求将提供来自静态文件和应用程序端点代码生成的模型的组合 OpenAPI 文档。但是,可以通过在 `application.properties`中添加 `mp.openapi.scan.disable=true`配置将此项更改为仅提供静态 OpenAPI 文档。

现在,对 `/q/openapi`端点发出的请求将提供静态 OpenAPI 文档,而不是生成的文档。

About OpenAPI document paths

Quarkus 支持多种途径来存储 OpenAPI 文档。我们建议你将其放在 META-INF/openapi.yml 下。备选途径包括:

  • META-INF/openapi.yaml

  • META-INF/openapi.yml

  • META-INF/openapi.json

  • WEB-INF/classes/META-INF/openapi.yml

  • WEB-INF/classes/META-INF/openapi.yaml

  • WEB-INF/classes/META-INF/openapi.json

在开发过程中支持静态 OpenAPI 文档的实时重新加载。Quarkus 将实时获取对你 OpenAPI 文档的修改。

Changing the OpenAPI version

默认情况下,当文档生成时,将使用 OpenAPI 版本 3.0.3。如果你使用上面提到的静态文件,将会使用该文件中的版本。你还可以使用以下配置在 SmallRye 中定义版本:

mp.openapi.extensions.smallrye.openapi=3.0.2

如果你的 API 通过需要特定版本的网关,这可能很有用。

Auto-generation of Operation Id

Operation Id 可以使用 @Operation 注释进行设置,并且在使用工具从架构生成客户端存根时,在许多情况下很有用。操作 ID 通常用于客户端存根中的方法名。在 SmallRye 中,你可以使用以下配置自动生成此操作 ID:

mp.openapi.extensions.smallrye.operationIdStrategy=METHOD

现在,你无需使用 @Operation 注释。生成文档时,方法名将用于操作 ID。

Table 1. The strategies available for generating the Operation Id
Property value Description

METHOD

Use the method name.

CLASS_METHOD

使用类名(不带包)加上方法。

PACKAGE_CLASS_METHOD

使用类名加上方法名。

Use Swagger UI for development

在构建 API 时,开发人员希望快速测试它们。 Swagger UI 是一个非常棒的工具,允许可视化和与你的 API 交互。用户界面会自动从 OpenAPI 规范中生成。

Quarkus smallrye-openapi 扩展附带了一个 swagger-ui 扩展,该扩展嵌入了正确配置的 Swagger UI 页面。

默认情况下,Swagger UI 仅在以开发或测试模式启动 Quarkus 时可用。 如果你也想在生产环境中使用,则可以在你的 application.properties 中包含以下配置:

quarkus.swagger-ui.always-include=true

这是一个构建时属性,在应用程序构建后无法在运行时更改。

默认情况下,可以在 /q/swagger-ui 访问 Swagger UI。

你可以通过在 application.properties 中设置 quarkus.swagger-ui.path 属性来更新 /swagger-ui 子路径:

quarkus.swagger-ui.path=my-custom-path

不允许值 /,因为它会阻止应用程序提供任何其他服务。以 '/' 为前缀的值使其成为绝对路径,而不是相对路径。

现在,我们准备运行我们的应用程序:

./mvnw compile quarkus:dev

你可以在应用程序的日志中检查 Swagger UI 路径:

00:00:00,000 INFO  [io.qua.swa.run.SwaggerUiServletExtension] Swagger UI available at /q/swagger-ui

应用程序启动后,您可以转到 [role="bare"][role="bare"]http://localhost:8080/q/swagger-ui 并操作您的 API。

您可以查看 API 的操作和架构。openapi swaggerui guide screenshot01

您还可以与您的 API 交互,以便快速测试它。openapi swaggerui guide screenshot02

CTRL+C 停止应用程序。

Styling

您可以通过提供自己的 logo 和 css 来自定义 swagger ui 的样式。

要提供您自己的 logo,您需要将一个名为 logo.png 的文件放到 src/main/resources/META-INF/branding 中。

这将为所有 UI(不仅仅是 swagger ui)设置 logo,因此在这种情况下还包括 GraphQL-UI 和 Health-UI(如果包含)。

如果您只想将此 logo 应用于 swagger-ui(而不是全局应用于所有 UI),则将该文件称为 smallrye-open-api-ui.png 而不是 logo.png

CSS

要提供您自己的 css 来替代/增强 html 中的样式,您需要将一个名为 style.css 的文件放到 src/main/resources/META-INF/branding 中。

这会将该 css 添加到所有 UI(不仅仅是 swagger ui),因此在这种情况下还包括 GraphQL-UI 和 Health-UI(如果包含)。

如果您只想将此样式应用于 swagger-ui(而不是全局应用于所有 UI),则将该文件称为 smallrye-open-api-ui.css 而不是 style.css

有关样式的更多信息,请阅读此博客文章:[role="bare"][role="bare"]https://quarkus.io/blog/stylish-api/

Cross Origin Resource Sharing

如果您计划在运行于不同域上的单页面应用程序中使用此应用程序,您将需要配置 CORS(跨域资源共享)。请阅读“跨域资源共享”指南的 CORS filter 部分,以获取更多详细信息。

Configuration Reference

OpenAPI

Unresolved include directive in modules/ROOT/pages/openapi-swaggerui.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-smallrye-openapi.adoc[]

Swagger UI

Unresolved include directive in modules/ROOT/pages/openapi-swaggerui.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-swagger-ui.adoc[]