和第一节相比,第二节向前迈了很大一步,我们使用异步消息调用event bus上的独立的可配置的verticle.我们也看到了可以部署多个实例配置负载均衡和充分的利用多核cpu.
在本节,我们将学习如何设计和使用vert.x 服务。service的主要优势是他定义了一个接口来执行verticle暴露的的一些操作。
我们利用代码生成所有的event bus消息通信,取代我们自己生成就像上节那样做的。
我们还将把代码重构为不同的Java包。
step-3/src/main/java/ └── io └── vertx └── guides └── wiki ├── MainVerticle.java ├── database │ ├── ErrorCodes.java │ ├── SqlQuery.java │ ├── WikiDatabaseService.java │ ├── WikiDatabaseServiceImpl.java │ ├── WikiDatabaseVerticle.java │ └── package-info.java └── http └── HttpServerVerticle.java
io.vertx.guides.wiki
现在包含 the main verticle,
io.vertx.guides.wiki.database
the database verticle and service, and
io.vertx.guides.wiki.http
the HTTP server verticle.
Maven 配置更改
首先,我们需要向项目中添加以下两个依赖项。很明显我们需要vertx-service-proxy
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<scope>provided</scope>
</dependency>
接下来还需要调整编译插件,以使用代码生成,这是通过javac注释处理器完成的:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<useIncrementalCompilation>false</useIncrementalCompilation>
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
<generatedSourcesDirectory>${project.basedir}/src/main/generated</generatedSourcesDirectory>
<compilerArgs>
<arg>-AoutputDirectory=${project.basedir}/src/main</arg>
</compilerArgs>
</configuration>
</plugin>
需要注意的是自动生成的代码在src/main/generated中,一些IDE会自动获取这些代码的路径。
更新maven-clean-plugin以移除这些自动生成的代码也是一个好主意。
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<filesets>
<fileset>
<directory>${project.basedir}/src/main/generated</directory>
</fileset>
</filesets>
</configuration>
</plugin>
数据库服务接口
定义一个服务接口就像定义一个Java接口一样简单,除了有一些规则需要遵守代码生成,并且确保在Vert.x中与其他代码的互操作性。
接口定义的开始是:
@ProxyGen
public interface WikiDatabaseService {
@Fluent
WikiDatabaseService fetchAllPages(Handler<AsyncResult<JsonArray>> resultHandler);
@Fluent
WikiDatabaseService fetchPage(String name, Handler<AsyncResult<JsonObject>> resultHandler);
@Fluent
WikiDatabaseService createPage(String title, String markdown, Handler<AsyncResult<Void>> resultHandler);
@Fluent
WikiDatabaseService savePage(int id, String markdown, Handler<AsyncResult<Void>> resultHandler);
@Fluent
WikiDatabaseService deletePage(int id, Handler<AsyncResult<Void>> resultHandler);
// (...)
1.ProxyGen注解用于触发该服务客户端的代理代码生成
2. Fluent
注解是可选的,but allows fluent interfaces where operations can be chained by returning the service instance。 这对代码生成很有用,当服务被其他JVM语言使用。
3.参数类型需要是字符串,Java基本类型,JSON对象或数组,任何枚举类型,或java.util集合(List / Set / Map)。 支持任意Java类的唯一方法是将它们作为Vert.x数据对象,用@DataObject注释。 传递其他类型的最后机会是服务引用类型。
4.由于服务提供异步结果,因此服务方法的最后一个参数需要是Handler <AsyncResult <T >>,其中T是适用于上述代码生成的任何类型。
服务接口提供静态方法来创建实际服务实现和客户端代码的事件总线代理,这是一种很好的做法。
我们将create定义为简单地委托给实现类及其构造函数:
static WikiDatabaseService create(JDBCClient dbClient, HashMap<SqlQuery, String> sqlQueries, Handler<AsyncResult<WikiDatabaseService>> readyHandler) {
return new WikiDatabaseServiceImpl(dbClient, sqlQueries, readyHandler);
}
Vert.x代码生成器创建代理类并通过后缀VertxEBProxy对其进行命名。 这些代理类的构造函数需要对Vert.x上下文的引用以及事件总线上的目标地址:
static WikiDatabaseService createProxy(Vertx vertx, String address) {
return new WikiDatabaseServiceVertxEBProxy(vertx, address);
}
数据库服务实现
服务实现是前一个WikiDatabaseVerticle类代码的直接端口。 本质区别在于构造函数(用于报告初始化结果)和服务方法(用于报告操作成功)中的异步结果处理程序的支持。
class WikiDatabaseServiceImpl implements WikiDatabaseService {
private static final Logger LOGGER = LoggerFactory.getLogger(WikiDatabaseServiceImpl.class);
private final HashMap<SqlQuery, String> sqlQueries;
private final JDBCClient dbClient;
WikiDatabaseServiceImpl(JDBCClient dbClient, HashMap<SqlQuery, String> sqlQueries, Handler<AsyncResult<WikiDatabaseService>> readyHandler) {
this.dbClient = dbClient;
this.sqlQueries = sqlQueries;
dbClient.getConnection(ar -> {
if (ar.failed()) {
LOGGER.error("Could not open a database connection", ar.cause());
readyHandler.handle(Future.failedFuture(ar.cause()));
} else {
SQLConnection connection = ar.result();
connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE), create -> {
connection.close();
if (create.failed()) {
LOGGER.error("Database preparation error", create.cause());
readyHandler.handle(Future.failedFuture(create.cause()));
} else {
readyHandler.handle(Future.succeededFuture(this));
}
});
}
});
}
@Override
public WikiDatabaseService fetchAllPages(Handler<AsyncResult<JsonArray>> resultHandler) {
dbClient.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
if (res.succeeded()) {
JsonArray pages = new JsonArray(res.result()
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList()));
resultHandler.handle(Future.succeededFuture(pages));
} else {
LOGGER.error("Database query error", res.cause());
resultHandler.handle(Future.failedFuture(res.cause()));
}
});
return this;
}
@Override
public WikiDatabaseService fetchPage(String name, Handler<AsyncResult<JsonObject>> resultHandler) {
dbClient.queryWithParams(sqlQueries.get(SqlQuery.GET_PAGE), new JsonArray().add(name), 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));
}
resultHandler.handle(Future.succeededFuture(response));
} else {
LOGGER.error("Database query error", fetch.cause());
resultHandler.handle(Future.failedFuture(fetch.cause()));
}
});
return this;
}
@Override
public WikiDatabaseService createPage(String title, String markdown, Handler<AsyncResult<Void>> resultHandler) {
JsonArray data = new JsonArray().add(title).add(markdown);
dbClient.updateWithParams(sqlQueries.get(SqlQuery.CREATE_PAGE), data, res -> {
if (res.succeeded()) {
resultHandler.handle(Future.succeededFuture());
} else {
LOGGER.error("Database query error", res.cause());
resultHandler.handle(Future.failedFuture(res.cause()));
}
});
return this;
}
@Override
public WikiDatabaseService savePage(int id, String markdown, Handler<AsyncResult<Void>> resultHandler) {
JsonArray data = new JsonArray().add(markdown).add(id);
dbClient.updateWithParams(sqlQueries.get(SqlQuery.SAVE_PAGE), data, res -> {
if (res.succeeded()) {
resultHandler.handle(Future.succeededFuture());
} else {
LOGGER.error("Database query error", res.cause());
resultHandler.handle(Future.failedFuture(res.cause()));
}
});
return this;
}
@Override
public WikiDatabaseService deletePage(int id, Handler<AsyncResult<Void>> resultHandler) {
JsonArray data = new JsonArray().add(id);
dbClient.updateWithParams(sqlQueries.get(SqlQuery.DELETE_PAGE), data, res -> {
if (res.succeeded()) {
resultHandler.handle(Future.succeededFuture());
} else {
LOGGER.error("Database query error", res.cause());
resultHandler.handle(Future.failedFuture(res.cause()));
}
});
return this;
}
}
代理代码生成工作前需要最后一步:服务包需要注释一个package-info.java来定义Vert.x模块:
@ModuleGen(groupPackage = "io.vertx.guides.wiki.database", name = "wiki-database")
package io.vertx.guides.wiki.database;
import io.vertx.codegen.annotations.ModuleGen;
暴露数据库服务
由于大部分数据库处理代码已移至WikiDatabaseServiceImpl,因此WikiDatabaseVerticle类现在由2个方法组成:注册服务的start方法和加载SQL查询的实用程序方法:
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";
@Override
public void start(Future<Void> startFuture) throws Exception {
HashMap<SqlQuery, String> sqlQueries = loadSqlQueries();
JDBCClient 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)));
WikiDatabaseService.create(dbClient, sqlQueries, ready -> {
if (ready.succeeded()) {
ProxyHelper.registerService(WikiDatabaseService.class, vertx, ready.result(), CONFIG_WIKIDB_QUEUE); (1)
startFuture.complete();
} else {
startFuture.fail(ready.cause());
}
});
}
/*
* Note: this uses blocking APIs, but data is small...
*/
private HashMap<SqlQuery, String> 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();
HashMap<SqlQuery, String> sqlQueries = new HashMap<>();
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"));
return sqlQueries;
}
}
1.我们在这里注册服务
注册服务需要接口类,Vert.x上下文,实现和事件总线目标。
WikiDatabaseServiceVertxEBProxy生成的类处理在事件总线上接收消息,然后将它们分派到WikiDatabaseServiceImpl。 它的作用实际上与我们在前一节中所做的非常接近:使用操作标头发送消息以指定要调用的方法,并且参数以JSON编码。
获取数据库服务代理
重构Vert.x服务的最后一步是调整HTTP服务器Verticle以获取数据库服务的代理并将其用于处理程序而不是事件总线。
首先,我们需要在verticle启动时创建代理:
private WikiDatabaseService dbService;
@Override
public void start(Future<Void> startFuture) throws Exception {
String wikiDbQueue = config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"); (1)
dbService = WikiDatabaseService.createProxy(vertx, wikiDbQueue);
HttpServer server = vertx.createHttpServer();
// (...)
1.我们只需确保使用与WikiDatabaseVerticle发布的服务相同的事件总线目标。
然后,我们需要用对数据库服务的调用来替换对事件总线的调用:
private void indexHandler(RoutingContext context) {
dbService.fetchAllPages(reply -> {
if (reply.succeeded()) {
context.put("title", "Wiki home");
context.put("pages", reply.result().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());
}
});
}
private void pageRenderingHandler(RoutingContext context) {
String requestedPage = context.request().getParam("page");
dbService.fetchPage(requestedPage, reply -> {
if (reply.succeeded()) {
JsonObject payLoad = reply.result();
boolean found = payLoad.getBoolean("found");
String rawContent = payLoad.getString("rawContent", EMPTY_PAGE_MARKDOWN);
context.put("title", requestedPage);
context.put("id", payLoad.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");
Handler<AsyncResult<Void>> handler = reply -> {
if (reply.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/wiki/" + title);
context.response().end();
} else {
context.fail(reply.cause());
}
};
String markdown = context.request().getParam("markdown");
if ("yes".equals(context.request().getParam("newPage"))) {
dbService.createPage(title, markdown, handler);
} else {
dbService.savePage(Integer.valueOf(context.request().getParam("id")), markdown, handler);
}
}
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) {
dbService.deletePage(Integer.valueOf(context.request().getParam("id")), reply -> {
if (reply.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/");
context.response().end();
} else {
context.fail(reply.cause());
}
});
}
WikiDatabaseServiceVertxProxyHandler生成的类将转发呼叫作为事件总线消息进行处理。
说明:直接通过事件总线消息直接使用Vert.x服务仍然是完全可能的,因为这是Proxy生成的。