HTTP 与 Web 开发

基于 OpenAPI 契约构建 Web 路由器

本指南将向你展示如何基于 OpenAPI 3 规范契约文档,自动化构建具备请求参数校验与类型提取能力的 Vert.x Web Router(路由分发器)。

你将构建什么

你将构建一个 Vert.x 应用程序,在内存中维护一组宠物(Pets)列表,并通过标准的 Petstore API 接口向外提供服务:https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml

前置要求

  • 文本编辑器或 IDE

  • Java 8 或更高版本

  • Maven

创建工程项目

以下是该项目所使用的 pom.xml 构建配置文件内容:

加载 OpenAPI 规范契约

Vert.x Web API Contract 模块提供了 RouterBuilder,帮助你直接从 OpenAPI 规范文件起步组装 Vert.x Web Router。

在 Verticle 的 start 生命周期方法中加载规范文件:

RouterBuilder.create(this.vertx, "petstore.yaml")
  .onSuccess(routerBuilder -> { (1)
    // You can start building the router using routerBuilder
  }).onFailure(cause -> { (2)
  // Something went wrong during router factory initialization
  startPromise.fail(cause);
});
1 若规范文件解析成功,你将获得一个可直接使用的 RouterBuilder 实例;
2 若加载解析失败,则中止 Verticle 的部署生命周期。

编写路由处理器

现在,你可以通过 operation(operationId).handler() 将具体业务逻辑挂载到 OpenAPI 操作对应的路由处理器上。

针对 listPets 操作(查询宠物列表):

routerBuilder.operation("listPets").handler(routingContext ->
  routingContext
    .response() (1)
    .setStatusCode(200)
    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") (2)
    .end(new JsonArray(getAllPets()).encode()) (3)
);
1 获取响应对象;
2 写入 Content-Type: application/json 响应头;
3 将所有宠物的 JSON 列表序列化并写入响应正文。

针对 createPets 操作(新增宠物):

routerBuilder.operation("createPets").handler(routingContext -> {
  RequestParameters params = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY); (1)
  JsonObject pet = params.body().getJsonObject(); (2)
  addPet(pet);
  routingContext
    .response()
    .setStatusCode(200)
    .end(); (3)
});
1 获取由契约校验器完成解析的参数容器;
2 提取经校验合规的请求体(Body);
3 返回 200 空响应,表示创建成功。

针对 showPetById 操作(按 ID 查询单个宠物):

routerBuilder.operation("showPetById").handler(routingContext -> {
  RequestParameters params = routingContext.get("parsedParameters"); (1)
  Integer id = params.pathParameter("petId").getInteger(); (2)
  Optional<JsonObject> pet = getAllPets()
    .stream()
    .filter(p -> p.getInteger("id").equals(id))
    .findFirst(); (3)
  if (pet.isPresent())
    routingContext
      .response()
      .setStatusCode(200)
      .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
      .end(pet.get().encode()); (4)
  else
    routingContext.fail(404, new Exception("Pet not found")); (5)
});
1 获取解析后的参数容器;
2 提取类型安全的路径参数;
3 在内存数据中匹配目标宠物;
4 若宠物存在,将其以 JSON 形式写回客户端;
5 若宠物不存在,将 RoutingContext 标记失败并触发 404 状态码。

生成路由器与全局异常处理

所有操作处理器声明完成后,即可生成 Router 并挂载全局 "404 Not Found" 与 "400 Bad Request" 错误拦截:

Router router = routerBuilder.createRouter(); (1)
router.errorHandler(404, routingContext -> { (2)
  JsonObject errorObject = new JsonObject() (3)
    .put("code", 404)
    .put("message",
      (routingContext.failure() != null) ?
        routingContext.failure().getMessage() :
        "Not Found"
    );
  routingContext
    .response()
    .setStatusCode(404)
    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
    .end(errorObject.encode()); (4)
});
router.errorHandler(400, routingContext -> {
  JsonObject errorObject = new JsonObject()
    .put("code", 400)
    .put("message",
      (routingContext.failure() != null) ?
        routingContext.failure().getMessage() :
        "Validation Exception"
    );
  routingContext
    .response()
    .setStatusCode(400)
    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
    .end(errorObject.encode());
});

server = vertx.createHttpServer(new HttpServerOptions().setPort(8080).setHost("localhost")); (5)
server.requestHandler(router).listen(); (6)
1 从 RouterBuilder 正式构建出 Vert.x Web Router 实例;
2 挂载 404 找不到资源的全局错误处理器;
3 组装包含异常详情的 JSON 错误对象;
4 将结构化错误信息输出至客户端;
5 实例化底层的 Vert.x HttpServer;
6 将组装好的 Router 挂载到 HttpServer 上并开始监听网络请求。

完整源码

你可以在该指南的 GitHub 仓库中查阅 APIVerticle 的完整源代码。

运行与接口测试

APIVerticle 已自带 main 方法入口,可以直接启动:

  1. 创建 Vertx 运行时上下文;

  2. 部署 APIVerticle。

运行方式:

  1. 在你的 IDE 中直接右键运行 APIVerticle 类;

  2. 或者在终端使用 Maven 启动:mvn compile exec:java。

服务启动后,你可以使用 curl 对各类契约场景进行验证:

# 1. 获取初始宠物列表
$ curl http://localhost:8080/pets
[{"id":1,"name":"Fufi","tag":"ABC"},{"id":2,"name":"Garfield","tag":"ABC"},{"id":3,"name":"Puffa","tag":"ABC"}]

# 2. 按 ID 成功查询宠物
$ curl http://localhost:8080/pets/3
{"id":3,"name":"Puffa","tag":"ABC"}

# 3. 查询不存在的 ID 返回 404
$ curl http://localhost:8080/pets/5
{"code":404,"message":"Pet not found"}

# 4. 创建新宠物
$ curl -X POST -H "Content-type: application/json" --data '{"id":4,"name":"Alan"}' http://localhost:8080/pets

# 5. 缺少必填字段触发 OpenAPI 契约校验失败(返回 400)
$ curl -X POST -H "Content-type: application/json" --data '{"id":4}' http://localhost:8080/pets
{"code":400,"message":"$.name: is missing but it is required"}

# 6. 验证新宠物已落库
$ curl http://localhost:8080/pets
[{"id":1,"name":"Fufi","tag":"ABC"},{"id":2,"name":"Garfield","tag":"ABC"},{"id":3,"name":"Puffa","tag":"ABC"},{"id":4,"name":"Alan"}]

本文总结

本指南向你阐明了:

  1. 如何从 OpenAPI 规范契约自动生成 Vert.x Web 路由器;

  2. 如何提取并信任已通过验证的请求参数;

  3. 如何格式化输出 JSON 响应;

  4. 如何配置全局统一的错误处理策略;

  5. 如何启动 HTTP 服务器并将契约路由投入运行。