感興趣的朋友爽雄,可以關注微信服務號“猿學堂社區(qū)”蝠检,或加入“猿學堂社區(qū)”微信交流群
版權聲明:本文由作者自行翻譯,未經作者授權挚瘟,不得隨意轉發(fā)叹谁。
使用我們已經講到的vertx-web模塊公開Web HTTP/JSON API非常簡單饲梭。我們將使用以下URL方案公開Web API:
- GET /api/pages 給出一個包含所有wiki頁面名稱和標識的文檔
- POST /api/pages 從一個文檔創(chuàng)建新的wiki頁
- PUT /api/pages/:id 從一個文檔更新wiki頁面
- DELETE /api/pages/:id 刪除一個wiki頁面
下面是使用HTTPie命令行工具與這些API交互的截圖:
7.1 Web子路由器
我們需要添加新的路由處理器到HttpServerVerticle類。雖然我們可以直接向現(xiàn)有的路由器添加處理程序焰檩,但我們也可以利用子路由器的優(yōu)勢來處理憔涉。它們允許將一個路由器掛載為另一個路由器的子路由器,這對組織和(或)重用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());
}
});
}
① 我們只是在頁面信息記錄對象中重新映射數(shù)據(jù)庫記錄国旷。
② 在響應載荷中,結果JSON數(shù)組成為pages鍵的值顿乒。
③ JsonObject#encode()給出了JSON數(shù)據(jù)的一個緊湊的String展現(xiàn)议街。
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 創(chuàng)建一個頁面
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)包括準備測試環(huán)境涂身。HTTP服務器Verticle依賴數(shù)據(jù)庫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搓蚪,以便使用一個內存數(shù)據(jù)庫進行測試蛤售。
正式的測試用例是一個簡單的場景,此處創(chuàng)造了所有類型的請求妒潭。它創(chuàng)建了一個頁面悴能,獲取它,更新它雳灾,然后刪除它:
@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)或者測試最后超時谎亩。