上一节我们构建了wiki应用,但是还存在如下问题:
1.http请求处理和数据库连接代码交错在一个相同的方法内,同时
2.很多配置项硬编程写在代码里(例如 数据库连接字符串)(ps接下来应该会讲怎么写配置文件和读取配置文件,我是边看边翻译的)
接下来我们将部署两个verticle,一个处理http请求,一个封装数据库持久性。这两个verticle将不会直接引用彼此,他们将通过event bus 调用彼此,这提供了一个简单而有效的解耦(ps高内聚低耦合大概就是这样吧)。
发送到event bus的信息需要格式化成json,虽然vert.x也支持其他格式通信,但是!json就是好,而且vert.x支持多语言,json也支持多语言,在多语言间用json传递消息也很方便。
新的start如下
public class HttpServerVerticle extends AbstractVerticle {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpServerVerticle.class);
public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"; (1)
public static final String CONFIG_WIKIDB_QUEUE = "wikidb.queue";
private String wikiDbQueue = "wikidb.queue";
@Override
public void start(Future<Void> startFuture) throws Exception {
wikiDbQueue = config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"); (2)
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.get("/").handler(this::indexHandler);
router.get("/wiki/:page").handler(this::pageRenderingHandler);
router.post().handler(BodyHandler.create());
router.post("/save").handler(this::pageUpdateHandler);
router.post("/create").handler(this::pageCreateHandler);
router.post("/delete").handler(this::pageDeletionHandler);
int portNumber = config().getInteger(CONFIG_HTTP_SERVER_PORT, 8080); (3)
server
.requestHandler(router::accept)
.listen(portNumber, ar -> {
if (ar.succeeded()) {
LOGGER.info("HTTP server running on port " + portNumber);
startFuture.complete();
} else {
LOGGER.error("Could not start a HTTP server", ar.cause());
startFuture.fail(ar.cause());
}
});
}
1.我们定义了公开类型的http请求端口和event bus 上的数据库连接的名字
2.AbstractVerticle#config()
可以访问定义好的配置,第二个参数是默认参数。
3.配置参数不仅可以是String类型还可以是int,boolean,json等
这个类的其余部分主要是处理http的,数据库连接代码被发送event bus消息取代。
下边是indexHandler
方法
private final FreeMarkerTemplateEngine templateEngine = FreeMarkerTemplateEngine.create();
private void indexHandler(RoutingContext context) {
DeliveryOptions options = new DeliveryOptions().addHeader("action", "all-pages"); (2)
vertx.eventBus().send(wikiDbQueue, new JsonObject(), options, reply -> { (1)
if (reply.succeeded()) {
JsonObject body = (JsonObject) reply.result().body(); (3)
context.put("title", "Wiki home");
context.put("pages", body.getJsonArray("pages").getList());
templateEngine.render(context, "templates", "/index.ftl", ar -> {
if (ar.succeeded()) {
context.response().putHeader("Content-Type", "text/html");
context.response().end(ar.result());
} else {
context.fail(ar.cause());
}
});
} else {
context.fail(reply.cause());
}
});
}
1.vertx提供了event bus的访问,我们可以通过event bus发送请求到数据库verticle.
2.递送配置允许我们指定头、数据类型、超时。
3.成功请求包含返回数据
正如我们所见,一个event bus 消息由body,和随意指定返回值的选项构成。如果请求的方法不存在就没有响应。
我们指定数据类型为json,同时在header中指定要执行的方法。
这个verticle的剩余部分包含路由处理,这也是通过event bus来读取和存储数据。
private static final String EMPTY_PAGE_MARKDOWN =
"# A new page\n" +
"\n" +
"Feel-free to write in Markdown!\n";
private void pageRenderingHandler(RoutingContext context) {
String requestedPage = context.request().getParam("page");
JsonObject request = new JsonObject().put("page", requestedPage);
DeliveryOptions options = new DeliveryOptions().addHeader("action", "get-page");
vertx.eventBus().send(wikiDbQueue, request, options, reply -> {
if (reply.succeeded()) {
JsonObject body = (JsonObject) reply.result().body();
boolean found = body.getBoolean("found");
String rawContent = body.getString("rawContent", EMPTY_PAGE_MARKDOWN);
context.put("title", requestedPage);
context.put("id", body.getInteger("id", -1));
context.put("newPage", found ? "no" : "yes");
context.put("rawContent", rawContent);
context.put("content", Processor.process(rawContent));
context.put("timestamp", new Date().toString());
templateEngine.render(context, "templates","/page.ftl", ar -> {
if (ar.succeeded()) {
context.response().putHeader("Content-Type", "text/html");
context.response().end(ar.result());
} else {
context.fail(ar.cause());
}
});
} else {
context.fail(reply.cause());
}
});
}
private void pageUpdateHandler(RoutingContext context) {
String title = context.request().getParam("title");
JsonObject request = new JsonObject()
.put("id", context.request().getParam("id"))
.put("title", title)
.put("markdown", context.request().getParam("markdown"));
DeliveryOptions options = new DeliveryOptions();
if ("yes".equals(context.request().getParam("newPage"))) {
options.addHeader("action", "create-page");
} else {
options.addHeader("action", "save-page");
}
vertx.eventBus().send(wikiDbQueue, request, options, reply -> {
if (reply.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/wiki/" + title);
context.response().end();
} else {
context.fail(reply.cause());
}
});
}
private void pageCreateHandler(RoutingContext context) {
String pageName = context.request().getParam("name");
String location = "/wiki/" + pageName;
if (pageName == null || pageName.isEmpty()) {
location = "/";
}
context.response().setStatusCode(303);
context.response().putHeader("Location", location);
context.response().end();
}
private void pageDeletionHandler(RoutingContext context) {
String id = context.request().getParam("id");
JsonObject request = new JsonObject().put("id", id);
DeliveryOptions options = new DeliveryOptions().addHeader("action", "delete-page");
vertx.eventBus().send(wikiDbQueue, request, options, reply -> {
if (reply.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/");
context.response().end();
} else {
context.fail(reply.cause());
}
});
}
数据库verticle
连接数据库需要一个数据库驱动和配置,我们已经在第一节硬编码在了代码里。
配置数据库方言
虽然硬编码也可以获取配置参数,倒是我们可以更上一层楼,通过配置文件获取配置参数(ps下面把方法甩给饥渴的你,反正我是等了很久了)
查询将从作为配置参数或缺省资源传递的文件中加载,这种方法的优势是vertcle可以自适应jdbc驱动和SQL方言。
verticle类的序文主要由配置关键定义组成。
public class WikiDatabaseVerticle extends AbstractVerticle {
public static final String CONFIG_WIKIDB_JDBC_URL = "wikidb.jdbc.url";
public static final String CONFIG_WIKIDB_JDBC_DRIVER_CLASS = "wikidb.jdbc.driver_class";
public static final String CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE = "wikidb.jdbc.max_pool_size";
public static final String CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE = "wikidb.sqlqueries.resource.file";
public static final String CONFIG_WIKIDB_QUEUE = "wikidb.queue";
private static final Logger LOGGER = LoggerFactory.getLogger(WikiDatabaseVerticle.class);
// (...)
sql查询存储在配置文件,在这里。
src/main/resources/db-queries.properties
create-pages-table=create table if not exists Pages (Id integer identity primary key, Name varchar(255) unique, Content clob) get-page=select Id, Content from Pages where Name = ? create-page=insert into Pages values (NULL, ?, ?) save-page=update Pages set Content = ? where Id = ? all-pages=select Name from Pages delete-page=delete from Pages where Id = ?
WikiDatabaseVerticle
类加载sql方言,通过map使他们可用。
private enum SqlQuery {
CREATE_PAGES_TABLE,
ALL_PAGES,
GET_PAGE,
CREATE_PAGE,
SAVE_PAGE,
DELETE_PAGE
}
private final HashMap<SqlQuery, String> sqlQueries = new HashMap<>();
private void loadSqlQueries() throws IOException {
String queriesFile = config().getString(CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE);
InputStream queriesInputStream;
if (queriesFile != null) {
queriesInputStream = new FileInputStream(queriesFile);
} else {
queriesInputStream = getClass().getResourceAsStream("/db-queries.properties");
}
Properties queriesProps = new Properties();
queriesProps.load(queriesInputStream);
queriesInputStream.close();
sqlQueries.put(SqlQuery.CREATE_PAGES_TABLE, queriesProps.getProperty("create-pages-table"));
sqlQueries.put(SqlQuery.ALL_PAGES, queriesProps.getProperty("all-pages"));
sqlQueries.put(SqlQuery.GET_PAGE, queriesProps.getProperty("get-page"));
sqlQueries.put(SqlQuery.CREATE_PAGE, queriesProps.getProperty("create-page"));
sqlQueries.put(SqlQuery.SAVE_PAGE, queriesProps.getProperty("save-page"));
sqlQueries.put(SqlQuery.DELETE_PAGE, queriesProps.getProperty("delete-page"));
}
我们使用枚举来避免使用String常量在以后的代码中。start verticle方法如下:
private JDBCClient dbClient;
@Override
public void start(Future<Void> startFuture) throws Exception {
/*
* Note: this uses blocking APIs, but data is small...
*/
loadSqlQueries(); (1)
dbClient = JDBCClient.createShared(vertx, new JsonObject()
.put("url", config().getString(CONFIG_WIKIDB_JDBC_URL, "jdbc:hsqldb:file:db/wiki"))
.put("driver_class", config().getString(CONFIG_WIKIDB_JDBC_DRIVER_CLASS, "org.hsqldb.jdbcDriver"))
.put("max_pool_size", config().getInteger(CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 30)));
dbClient.getConnection(ar -> {
if (ar.failed()) {
LOGGER.error("Could not open a database connection", ar.cause());
startFuture.fail(ar.cause());
} else {
SQLConnection connection = ar.result();
connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE), create -> { (2)
connection.close();
if (create.failed()) {
LOGGER.error("Database preparation error", create.cause());
startFuture.fail(create.cause());
} else {
vertx.eventBus().consumer(config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"), this::onMessage); (3)
startFuture.complete();
}
});
}
});
}
1.有趣的是,我们打破了一个重要原则:避免阻塞api,但是没有异步api可以获取资源。我们可以用executeBlocking方法来把I/O阻塞操作从event loop上卸载到一个子线程上,但是那没有明显的好处,因为这个方法占很少的资源。
2.这是一个使用SQL查询的示例。
3.consumer方法注册了一个event bus 目的地处理。(ps暂时不懂)
调度请求
定义一个onMessage方法来处理event bus消息。
public enum ErrorCodes {
NO_ACTION_SPECIFIED,
BAD_ACTION,
DB_ERROR
}
public void onMessage(Message<JsonObject> message) {
if (!message.headers().contains("action")) {
LOGGER.error("No action header specified for message with headers {} and body {}",
message.headers(), message.body().encodePrettily());
message.fail(ErrorCodes.NO_ACTION_SPECIFIED.ordinal(), "No action header specified");
return;
}
String action = message.headers().get("action");
switch (action) {
case "all-pages":
fetchAllPages(message);
break;
case "get-page":
fetchPage(message);
break;
case "create-page":
createPage(message);
break;
case "save-page":
savePage(message);
break;
case "delete-page":
deletePage(message);
break;
default:
message.fail(ErrorCodes.BAD_ACTION.ordinal(), "Bad action: " + action);
}
}
我们定义了一个ErrorCodes 枚举,用来返回错误消息。通过这种方法,很方便的返回错误消息给发送者,而且最重要的她是异步的。
减少JDBC引用
到目前为止,我们已经看到了完整的SQL查询交互。
1.恢复一个连接
2.执行请求
3.释放连接
这将导致在异步操作中需要进行大量错误处理。例如:
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
connection.close();
if (res.succeeded()) {
List<String> pages = res.result()
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList());
message.reply(new JsonObject().put("pages", new JsonArray(pages)));
} else {
reportQueryError(message, res.cause());
}
});
} else {
reportQueryError(message, car.cause());
}
});
从vert.x 3.5.0开始,JDBC支持 one-shot操作,连接被用来执行SQL然后在内部释放。上边的代码可以改为如下:
dbClient.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
if (res.succeeded()) {
List<String> pages = res.result()
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList());
message.reply(new JsonObject().put("pages", new JsonArray(pages)));
} else {
reportQueryError(message, res.cause());
}
});
对于只执行一个查询的操作这很有用。
需要注意的是,执行多个sql操作还是重复使用连接比较好。
剩下的代码是onMessage要调用的方法。
private void fetchAllPages(Message<JsonObject> message) {
dbClient.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
if (res.succeeded()) {
List<String> pages = res.result()
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList());
message.reply(new JsonObject().put("pages", new JsonArray(pages)));
} else {
reportQueryError(message, res.cause());
}
});
}
private void fetchPage(Message<JsonObject> message) {
String requestedPage = message.body().getString("page");
JsonArray params = new JsonArray().add(requestedPage);
dbClient.queryWithParams(sqlQueries.get(SqlQuery.GET_PAGE), params, fetch -> {
if (fetch.succeeded()) {
JsonObject response = new JsonObject();
ResultSet resultSet = fetch.result();
if (resultSet.getNumRows() == 0) {
response.put("found", false);
} else {
response.put("found", true);
JsonArray row = resultSet.getResults().get(0);
response.put("id", row.getInteger(0));
response.put("rawContent", row.getString(1));
}
message.reply(response);
} else {
reportQueryError(message, fetch.cause());
}
});
}
private void createPage(Message<JsonObject> message) {
JsonObject request = message.body();
JsonArray data = new JsonArray()
.add(request.getString("title"))
.add(request.getString("markdown"));
dbClient.updateWithParams(sqlQueries.get(SqlQuery.CREATE_PAGE), data, res -> {
if (res.succeeded()) {
message.reply("ok");
} else {
reportQueryError(message, res.cause());
}
});
}
private void savePage(Message<JsonObject> message) {
JsonObject request = message.body();
JsonArray data = new JsonArray()
.add(request.getString("markdown"))
.add(request.getString("id"));
dbClient.updateWithParams(sqlQueries.get(SqlQuery.SAVE_PAGE), data, res -> {
if (res.succeeded()) {
message.reply("ok");
} else {
reportQueryError(message, res.cause());
}
});
}
private void deletePage(Message<JsonObject> message) {
JsonArray data = new JsonArray().add(message.body().getString("id"));
dbClient.updateWithParams(sqlQueries.get(SqlQuery.DELETE_PAGE), data, res -> {
if (res.succeeded()) {
message.reply("ok");
} else {
reportQueryError(message, res.cause());
}
});
}
private void reportQueryError(Message<JsonObject> message, Throwable cause) {
LOGGER.error("Database query error", cause);
message.fail(ErrorCodes.DB_ERROR.ordinal(), cause.getMessage());
}
从主 verticle 部署其他verticle
我们仍要保留MainVerticle类,但是她不再包含业务逻辑代码,她现在的工作是引导程序和部署其他verticle.
这部分代码由一个数据库实例和两个http实例构成。
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Future<Void> startFuture) throws Exception {
Future<String> dbVerticleDeployment = Future.future(); (1)
vertx.deployVerticle(new WikiDatabaseVerticle(), dbVerticleDeployment.completer()); (2)
dbVerticleDeployment.compose(id -> { (3)
Future<String> httpVerticleDeployment = Future.future();
vertx.deployVerticle(
"io.vertx.guides.wiki.HttpServerVerticle", (4)
new DeploymentOptions().setInstances(2), (5)
httpVerticleDeployment.completer());
return httpVerticleDeployment; (6)
}).setHandler(ar -> { (7)
if (ar.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(ar.cause());
}
});
}
}
1.部署verticle是异步操作,所有我们需要使用Future.使用String类型是因为一旦verticle成功部署将获得一个标识。
2.第一个参数用来创建一个实例,并通过这个对象引用deploy方法。completer返回完成了她的future.
3.使用compose可以连续的构建,第一个构建完成后将会调用这个。
4.这个字符串名字将指定哪个类被部署。对于其他JVM语言,基于字符串的约定允许指定模块/脚本。
5.DeploymentOption
类可以指定一个数字类型的参数,用来指定启动几个实例。
6.组合函数返回下一个future,它的完成将触发复合操作的完成。
7.我们定义一个handler最终完成MainVerticle的启动future.
精明的读者可能会想知道,我们如何在同一个TCP端口上部署HTTP服务器的代码两次,而不会有任何一个实例出现错误,由端口已经占用引起的。其他一些web框架,我们需要选择不同的TCP端口,并用一个前端HTTP代理来在端口之间执行负载均衡。
vert.x并不需要这样。因为一个tcp端口可以在多个verticle间共享。进来的连接以简单的循环方式从接收线程分发。