使用我们已经介绍的vertx-web
模块,公开Web HTTP / JSON API非常简单。我们将使用以下URL方案公开Web API:
GET /api/pages
给出一个文档将所有维基页面名称和标识符,POST /api/pages
从文档创建一个新的wiki页面,PUT /api/pages/:id
从文档更新维基页面,DELETE /api/pages/:id
删除一个wiki页面。
以下是使用HTTPie命令行工具与API进行交互的屏幕截图:
Web子路由器
我们将添加新的路由处理程序HttpServerVerticle
。虽然我们可以将处理程序添加到现有路由器,但我们也可以利用子路由器。它们允许路由器作为另一个路由器的子路由器进行安装,这对组织和/或重新使用处理程序非常有用。
以下是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); (1)
这是我们安装路由器的地方,因此从
/api
路径开始的请求将被引导至apiRouter
。
处理程序
以下是不同API路由器处理程序的代码。
根资源
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")) (1)
.put("name", obj.getString("NAME")))
.collect(Collectors.toList());
response
.put("success", true)
.put("pages", pages); (2)
context.response().setStatusCode(200);
context.response().putHeader("Content-Type", "application/json");
context.response().end(response.encode()); (3)
} 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()
给出了String
JSON数据的简洁表示。
获得一个页面
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());
});
}
创建一个页面
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;
}
更新页面
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());
}
}
删除一个页面
private void apiDeletePage(RoutingContext context) {
int id = Integer.valueOf(context.request().getParam("id"));
dbService.deletePage(id, reply -> {
handleSimpleDbReply(context, reply);
});
}
单元测试API
我们在io.vertx.guides.wiki.http.ApiTest
课堂上编写一个基本的测试用例。
序言包括准备测试环境。HTTP服务器Verticle需要运行数据库Verticle,所以我们需要在我们的测试Vert.x上下文中进行部署:
@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") (1)
.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
对象组合而不是嵌套回调; 最后的作文必须完成异步的未来或者测试最终会超时。