感兴趣的朋友,可以关注微信服务号“猿学堂社区”,或加入“猿学堂社区”微信交流群
版权声明:本文由作者自行翻译,未经作者授权,不得随意转发。
使用我们已经讲到的vertx-web模块公开Web HTTP/JSON API非常简单。我们将使用以下URL方案公开Web API:
- GET /api/pages 给出一个包含所有wiki页面名称和标识的文档
- POST /api/pages 从一个文档创建新的wiki页
- PUT /api/pages/:id 从一个文档更新wiki页面
- 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)或者测试最后超时。