Vert.x Java开发指南——第七章 公开Web API

本文介绍如何使用Vert.x Web模块创建RESTful API,包括GET、POST、PUT和DELETE操作,并展示了如何通过子路由器组织API路由。同时提供了API的单元测试案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第七章 公开Web API

版权声明:本文为博主自主翻译,转载请标明出处。 https://blog.youkuaiyun.com/elinespace/article/details/80559368

相应代码位于本指南仓库的step-6目录下

使用我们已经讲到的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)或者测试最后超时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值