美文网首页猿学堂社区
Vert.x Java开发指南——第七章 公开Web API

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

作者: 猿学堂 | 来源:发表于2019-11-03 18:54 被阅读0次

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

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

    使用我们已经讲到的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<JsonObject> 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<Void> 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<JsonObject> postRequest = Future.future();
        webClient.post("/api/pages").as(BodyCodec.jsonObject())
                .sendJsonObject(page, ar -> {
                    if (ar.succeeded()) {
                        HttpResponse<JsonObject> postResponse = ar.result();
                        postRequest.complete(postResponse.body());
                    } else {
                        context.fail(ar.cause());
                    }
                });
        Future<JsonObject> getRequest = Future.future();
        postRequest.compose(h -> {
            webClient.get("/api/pages").as(BodyCodec.jsonObject()).send(ar -> {
                if (ar.succeeded()) {
                    HttpResponse<JsonObject> getResponse = ar.result();
                    getRequest.complete(getResponse.body());
                } else {
                    context.fail(ar.cause());
                }
            });
        }, getRequest);
        Future<JsonObject> 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<JsonObject> putResponse = ar.result();
                                            putRequest.complete(putResponse.body());
                                        } else {
                                            context.fail(ar.cause());
                                        }
                                    });
                }, putRequest);
        Future<JsonObject> 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<JsonObject> 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

        本文链接:https://www.haomeiwen.com/subject/qrdlbctx.html