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<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)或者测试最后超时。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容