Using the vertx-web
modules we've already covered, exposing a Web HTTP/JSON API is straightforward . We will expose the web API using the following URL scheme:
GET /api/pages
Given a document will all wiki page names and identifiers,POST /api/pages
Create a new wiki page from the documentation,PUT /api/pages/:id
Update wiki pages from documentation,DELETE /api/pages/:id
Delete a wiki page.
Here is a screenshot of interacting with the API using the HTTPie command line tool :
Web sub-router
We will add new route handlers HttpServerVerticle
. While we can add handlers to existing routers, we can also take advantage of sub-routers . They allow a router to be installed as a sub-router of another router, which is useful for organizing and/or reusing handlers.
Here is the code for the API router:
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)
This is where we install the router, so requests starting from the
/api
path will be directed toapiRouter
.
handler
Below is the code for the different API router handlers.
root resource
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());
}
});
}
We just remap the database result in the page info input object.
The resulting JSON array becomes the value for the key in the
pages
response payload .JsonObject#encode()
Gives a concise representation ofString
JSON data .
get a page
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());
});
}
create a page
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());
}
});
}
This handler and other handlers need to process incoming JSON documents. The following
validateJsonPageDocument
methods are helpers for performing validation and early error reporting so that the rest of the processing assumes the presence of certain JSON entries:
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;
}
update page
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
对象组合而不是嵌套回调; 最后的作文必须完成异步的未来或者测试最终会超时。