Vert.x Java开发指南——第七章 公开Web API

感兴趣的朋友,可以关注微信服务号“猿学堂社区”,或加入“猿学堂社区”微信交流群

版权声明:本文由作者自行翻译,未经作者授权,不得随意转发。

使用我们已经讲到的vertx-web模块公开Web HTTP/JSON API非常简单。我们将使用以下URL方案公开Web API:

  1. GET /api/pages 给出一个包含所有wiki页面名称和标识的文档
  2. POST /api/pages 从一个文档创建新的wiki页
  3. PUT /api/pages/:id 从一个文档更新wiki页面
  4. DELETE /api/pages/:id 删除一个wiki页面

下面是使用HTTPie命令行工具与这些API交互的截图:

在这里插入图片描述

7.1 Web子路由器

我们需要添加新的路由处理器到HttpServerVerticle类。虽然我们可以直接向现有的路由器添加处理程序,但我们也可以利用子路由器的优势来处理。它们允许将一个路由器挂载为另一个路由器的子路由器,这对组织和(或)重用handler非常有用。

此处是API路由器的代码:

Router apiRouter = Router.router(vertx);
apiRouter.get("/pages").handler(this::apiRoot);
apiRouter.get("/pages/:id").handler(this::apiGetPage);
apiRouter.post().handler(BodyHandler.create());
apiRouter.post("/pages").handler(this::apiCreatePage);
apiRouter.put().handler(BodyHandler.create());
apiRouter.put("/pages/:id").handler(this::apiUpdatePage);
apiRouter.delete("/pages/:id").handler(this::apiDeletePage);
router.mountSubRouter("/api", apiRouter); ①

① 这是我们挂载API路由器的位置,因此请求以/api开始的路径将定向到apiRouter。

7.2 处理器

接下来是不同的API路由器处理器代码。

7.2.1 根资源

private void apiRoot(RoutingContext context) {
    dbService.fetchAllPagesData(reply -> {
        JsonObject response = new JsonObject();
        if (reply.succeeded()) {
            List pages = reply.result()
                .stream()
                .map(obj -> new JsonObject()
                    .put("id", obj.getInteger("ID")) ①
                    .put("name", obj.getString("NAME")))
                .collect(Collectors.toList());
                response.put("success", true)
                    .put("pages", pages); ②
                context.response().setStatusCode(200);
                context.response().putHeader("Content-Type", "application/json");
                context.response().end(response.encode()); ③
        } else {
            response.put("success", false)
                    .put("error", reply.cause().getMessage());
            context.response().setStatusCode(500);
            context.response().putHeader("Content-Type", "application/json");
            context.response().end(response.encode());
        }
    });
}

① 我们只是在页面信息记录对象中重新映射数据库记录。

② 在响应载荷中,结果JSON数组成为pages键的值。

③ JsonObject#encode()给出了JSON数据的一个紧凑的String展现。

7.2.2 得到一个页面

private void apiGetPage(RoutingContext context) {
    int id = Integer.valueOf(context.request().getParam("id"));
    dbService.fetchPageById(
            id,
            reply -> {
                JsonObject response = new JsonObject();
                if (reply.succeeded()) {
                    JsonObject dbObject = reply.result();
                    if (dbObject.getBoolean("found")) {
                        JsonObject payload = new JsonObject()
                                .put("name", dbObject.getString("name"))
                                .put("id", dbObject.getInteger("id"))
                                .put("markdown",dbObject.getString("content"))
                                .put("html",Processor.process(dbObject.getString("content")));
                        response.put("success", true).put("page", payload);
                        context.response().setStatusCode(200);
                    } else {
                        context.response().setStatusCode(404);
                        response.put("success", false).put("error","There is no page with ID " + id);
                    }
                } else {
                    response.put("success", false).put("error",reply.cause().getMessage());
                    context.response().setStatusCode(500);
                }
                context.response().putHeader("Content-Type",
                        "application/json");
                context.response().end(response.encode());
            });
}

7.2.3 创建一个页面

private void apiCreatePage(RoutingContext context) {
    JsonObject page = context.getBodyAsJson();
    if (!validateJsonPageDocument(context, page, "name", "markdown")) {
        return;
    }
    dbService.createPage(
            page.getString("name"),
            page.getString("markdown"),
            reply -> {
                if (reply.succeeded()) {
                    context.response().setStatusCode(201);
                    context.response().putHeader("Content-Type","application/json");
                    context.response().end(new JsonObject().put("success", true).encode());
                } else {
                    context.response().setStatusCode(500);
                    context.response().putHeader("Content-Type","application/json");
                    context.response().end(new JsonObject()
                            .put("success", false)
                            .put("error",reply.cause().getMessage()).encode());
                }
            }
    );
}

这个处理器和其它处理器都需要处理输入的JSON文档。下面的validateJsonPageDocument方法是一个验证并在早期报告错误的助手,因此处理的剩余部分假定存在某些JSON条目。

private boolean validateJsonPageDocument(RoutingContext context, JsonObject page, String... expectedKeys) {
    if (!Arrays.stream(expectedKeys).allMatch(page::containsKey)) {
        LOGGER.error("Bad page creation JSON payload: " + page.encodePrettily() + " from " + context.request().
                remoteAddress());
        context.response().setStatusCode(400);
        context.response().putHeader("Content-Type", "application/json");
        context.response().end(new JsonObject()
                .put("success", false)
                .put("error", "Bad request payload").encode());
        return false;
    }
    return true;
}

7.2.4 更新一个页面

private void apiUpdatePage(RoutingContext context) {
    int id = Integer.valueOf(context.request().getParam("id"));
    JsonObject page = context.getBodyAsJson();
    if (!validateJsonPageDocument(context, page, "markdown")) {
        return;
    }
    dbService.savePage(id, page.getString("markdown"), reply -> {
        handleSimpleDbReply(context, reply);
    });
}

handleSimpleDbReply方法是一个助手,用于完成请求处理:

private void handleSimpleDbReply(RoutingContext context, AsyncResult reply) {
    if (reply.succeeded()) {
        context.response().setStatusCode(200);
        context.response().putHeader("Content-Type", "application/json");
        context.response().end(new JsonObject().put("success", true).encode());
    } else {
        context.response().setStatusCode(500);
        context.response().putHeader("Content-Type", "application/json");
        context.response().end(new JsonObject()
            .put("success", false)
            .put("error", reply.cause().getMessage()).encode());
    }
}

7.2.5 删除一个页面

private void apiDeletePage(RoutingContext context) {
    int id = Integer.valueOf(context.request().getParam("id"));
    dbService.deletePage(id, reply -> {
        handleSimpleDbReply(context, reply);
    });
}

7.3 单元测试API

我们在io.vertx.guides.wiki.http.ApiTest类中编写一个基础的测试用例。

前导(preamble)包括准备测试环境。HTTP服务器Verticle依赖数据库Verticle,因此我们需要在测试Vert.x上下文中同时部署这两个Verticle:

@RunWith(VertxUnitRunner.class)
public class ApiTest {
    private Vertx vertx;
    private WebClient webClient;
    @Before
    public void prepare(TestContext context) {
        vertx = Vertx.vertx();
        JsonObject dbConf = new JsonObject()
            .put(WikiDatabaseVerticle.CONFIG_WIKIDB_JDBC_URL,                   "jdbc:hsqldb:mem:testdb;shutdown=true") ①
            .put(WikiDatabaseVerticle.CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 4);
        vertx.deployVerticle(new WikiDatabaseVerticle(),
                new DeploymentOptions().setConfig(dbConf), context.asyncAssertSuccess());
        vertx.deployVerticle(new HttpServerVerticle(), context.asyncAssertSuccess());
        webClient = WebClient.create(vertx, new WebClientOptions()
            .setDefaultHost("localhost")
            .setDefaultPort(8080));
    }
    @After
    public void finish(TestContext context) {
        vertx.close(context.asyncAssertSuccess());
    }
    // (...)

① 我们使用了一个不同的JDBC URL,以便使用一个内存数据库进行测试。

正式的测试用例是一个简单的场景,此处创造了所有类型的请求。它创建了一个页面,获取它,更新它,然后删除它:

@Test
public void play_with_api(TestContext context) {
    Async async = context.async();
    JsonObject page = new JsonObject().put("name", "Sample").put(
            "markdown", "# A page");
    Future postRequest = Future.future();
    webClient.post("/api/pages").as(BodyCodec.jsonObject())
            .sendJsonObject(page, ar -> {
                if (ar.succeeded()) {
                    HttpResponse postResponse = ar.result();
                    postRequest.complete(postResponse.body());
                } else {
                    context.fail(ar.cause());
                }
            });
    Future getRequest = Future.future();
    postRequest.compose(h -> {
        webClient.get("/api/pages").as(BodyCodec.jsonObject()).send(ar -> {
            if (ar.succeeded()) {
                HttpResponse getResponse = ar.result();
                getRequest.complete(getResponse.body());
            } else {
                context.fail(ar.cause());
            }
        });
    }, getRequest);
    Future putRequest = Future.future();
    getRequest.compose(
            response -> {
                JsonArray array = response.getJsonArray("pages");
                context.assertEquals(1, array.size());
                context.assertEquals(0, array.getJsonObject(0).getInteger("id"));
                webClient.put("/api/pages/0")
                    .as(BodyCodec.jsonObject())
                    .sendJsonObject(new JsonObject().put("id", 0).put("markdown", "Oh Yeah!"),
                                ar -> {
                                    if (ar.succeeded()) {
                                        HttpResponse putResponse = ar.result();
                                        putRequest.complete(putResponse.body());
                                    } else {
                                        context.fail(ar.cause());
                                    }
                                });
            }, putRequest);
    Future deleteRequest = Future.future();
    putRequest.compose(
            response -> {
                context.assertTrue(response.getBoolean("success"));
                webClient.delete("/api/pages/0")
                        .as(BodyCodec.jsonObject())
                        .send(ar -> {
                            if (ar.succeeded()) {
                                HttpResponse delResponse = ar.result();
                                deleteRequest.complete(delResponse.body());
                            } else {
                                context.fail(ar.cause());
                            }
                        });
            }, deleteRequest);
    deleteRequest.compose(response -> {
        context.assertTrue(response.getBoolean("success"));
        async.complete();
    }, Future.failedFuture("Oh?"));
}

这个测试使用了Future对象组合的方式,而不是嵌入式回调;最后的组合(compose)必须完成这个异步Future(指的是async)或者测试最后超时。

你可能感兴趣的:(Vert.x Java开发指南——第七章 公开Web API)