感興趣的朋友,可以關(guān)注微信服務(wù)號“猿學(xué)堂社區(qū)”,或加入“猿學(xué)堂社區(qū)”微信交流群
版權(quán)聲明:本文由作者自行翻譯,未經(jīng)作者授權(quán),不得隨意轉(zhuǎn)發(fā)
我們將從第一次迭代開始,最簡單的代碼可能是使用Vert.x編寫一個Wiki。而下一次迭代將在代碼庫中引入更多的簡潔以及適當(dāng)?shù)臏y試,我們將看到基于Vert.x的快速原型是一個既簡單又現(xiàn)實(shí)的目標(biāo)。
現(xiàn)階段,Wiki將使用HTML頁面的服務(wù)端渲染以及通過JDBC鏈接進(jìn)行數(shù)據(jù)持久化。為了完成這些,我們將使用以下庫。
- Vert.x Web,雖然Vert.x核心庫支持HTTP服務(wù)器的創(chuàng)建,但是它未提供優(yōu)雅的API來處理路由、請求荷載處理等。
- Vert.x JDBC client,提供一套JDBC的異步API。
- Apache FreeMarker,用于渲染服務(wù)端頁面,它是一個簡單的模板引擎。
- Txtmark,用于將Markdown文本渲染為HTML,允許以Markdown編輯Wiki頁面。
2.1 引導(dǎo)一個Maven項(xiàng)目
該指南選擇使用Apache Maven作為構(gòu)建工具,主要因?yàn)樗c主要的集成開發(fā)環(huán)境集成的非常好。你同樣可以使用其它的構(gòu)建工具,如Gradle。
Vert.x社區(qū)在提供了一個可以克隆的模板項(xiàng)目結(jié)構(gòu)。由于你也可能希望使用(Git)版本控制,因此最快的途徑是克隆倉庫、刪除./git目錄,然后創(chuàng)建一個新的Git倉庫:
git clone https://github.com/vert-x3/vertx-maven-starter.git vertx-wiki
cd vertx-wiki
rm -rf .git
git init
該項(xiàng)目提供了一個樣例Verticle以及一個單元測試。你可以安全的刪除src/目錄下的所有.java文件來修改Wiki,但是在這么做之前,您可以先測試一下項(xiàng)目是否成功構(gòu)建和運(yùn)行。
mvn package exec:java
你將注意到Maven項(xiàng)目的pom.xml文件做了兩件有意思的事情:
- 它使用Maven Shade Plugin創(chuàng)建了一個包含所有需要依賴的單個JAR的歸檔文件,后綴為-fat.jar,也稱為“FatJar”。
- 它使用Exec Maven Plugin來提供exec:java目標(biāo)(goal),通過Vert.x的io.vertx.core.Launcher類依次啟動應(yīng)用。這實(shí)際上等價(jià)于使用Vert.x發(fā)行版中提供的vertx命令行工具運(yùn)行。
最后,你會注意到redeploy.sh和redeploy.bat腳本的存在,你可以相應(yīng)的使用它們來自動編譯和重新部署變更的代碼。注意,這樣做需要確保腳本中的VERTICLE變量與使用的主Verticle匹配。
另外,F(xiàn)abric8項(xiàng)目提供了一個Vert.x Maven插件。它包含了初始化、構(gòu)建、打包及運(yùn)行一個Vert.x項(xiàng)目的goal。
與克隆Git starter倉庫一樣生成一個類似項(xiàng)目:
mkdir vertx-wiki
cd vertx-wiki
mvn io.fabric8:vertx-maven-plugin:1.0.7:setup -DvertxVersion=3.5.0
git init
2.2 添加需要的依賴
添加到Maven pom.xml文件中的第一批依賴項(xiàng)是用于Web處理和渲染的:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.github.rjeschke</groupId>
<artifactId>txtmark</artifactId>
<version>0.13</version>
</dependency>
正如vertx-web-templ-freemarker的名字所示,Vert.x Web對流行的模板引擎提供了可插拔的支持: Handlebars, Jade, MVEL, Pebble, Thymeleaf以及Freemarker。
第二部分依賴是JDBC數(shù)據(jù)庫訪問需要的:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-jdbc-client</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.3.4</version>
</dependency>
Vert.x JDBC客戶端庫提供了任何JDBC兼容數(shù)據(jù)庫的訪問,當(dāng)然我們的項(xiàng)目需要在類路徑有一個JDBC驅(qū)動。
HSQLDB是一款知名的使用Java編寫的關(guān)系數(shù)據(jù)庫。它在作為嵌入式數(shù)據(jù)庫使用,從而避免需要獨(dú)立運(yùn)行第三方數(shù)據(jù)庫服務(wù)器的時(shí)候非常受歡迎。它還在單元測試以及集成測試方面比較受歡迎,因?yàn)樗峁┝艘粋€(輕快的)內(nèi)存存儲。
HSQLDB作為一個嵌入式數(shù)據(jù)庫,非常適合我們的入門。它存儲數(shù)據(jù)到本地文件,由于HSQLDB庫的JAR提供了一個JDBC驅(qū)動,Vert.x JDBC配置將非常直接。
Vert.x還提供了專用的MySQL和PostgreSQL客戶端庫。
當(dāng)然,你可以使用通用的Vert.x JDBC客戶端連接MySQL或PostgreSQL數(shù)據(jù)庫,但是與阻塞的JDBC API相比,通過使用這兩個數(shù)據(jù)庫服務(wù)器的網(wǎng)絡(luò)協(xié)議,這些庫提供了更好的性能。
Vert.x也提供了處理流行的非關(guān)系型數(shù)據(jù)庫MongoDB和Redis的庫。廣泛的社區(qū)還提供了與其它存儲系統(tǒng)的集成,如Apache Cassandra、OrientDB或ElasticSearch。
2.3 Verticle剖析
我們Wiki的Verticle由一個單獨(dú)的io.vertx.guides.wiki.MainVerticle類組成。這個類擴(kuò)展自io.vertx.core.AbstractVerticle(主要提供的Verticle的基類):
- 復(fù)寫生命周期的start和stop方法。
- 一個名為vertx的保護(hù)屬性,它是該Verticle被部署到的Vert.x環(huán)境的引用。
- 配置對象的訪問器,允許向Verticle傳遞外部配置。
開始,我們的Verticle只需要按下面復(fù)寫start方法:
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Future<Void> startFuture) throws Exception {
startFuture.complete();
}
}
存在兩種形式的start(和stop)方法:一個是無參的,另一個有一個future對象引用。無參的變體方法意味著Verticle初始化或者內(nèi)務(wù)階段總是成功,除非拋出一個異常。包含future對象參數(shù)的變體方法提供了一個更細(xì)粒度的方法來在最后指示操作是否成功。事實(shí)上,一些初始化或清理代碼可能需要異步操作,因此通過future對象進(jìn)行報(bào)告理所當(dāng)然符合異步風(fēng)格。
2.4 關(guān)于future對象和回調(diào)的一兩句話
Vert.x future不是JDK的future:它們可以組裝以及以非阻塞的方式查詢。它們應(yīng)用于異步任務(wù)的簡單協(xié)調(diào),尤其是Verticle部署和檢查它們是否部署成功。
Vert.x核心API基于異步事件通知回調(diào)。經(jīng)驗(yàn)豐富的開發(fā)人員自然會想到這打開了稱為“回調(diào)地獄”的大門,多個層次的嵌套回調(diào)使得代碼難以理解,如該虛構(gòu)代碼所示:
foo.a(1, res1 -> {
if (res1.succeeded()) {
bar.b("abc", 1, res2 -> {
if (res.succeeded()) {
baz.c(res3 -> {
dosomething(res1, res2, res3, res4 -> {
// (...)
});
});
}
});
}
});
雖然核心API被設(shè)計(jì)成支持Promise和Future,但選擇回調(diào)實(shí)際上是有意思的,因?yàn)樗试S使用不同的編程抽象。Vert.x是一個總體上非教條的(un-opinionated)項(xiàng)目,而且回調(diào)允許不同模型的實(shí)現(xiàn),以更好的應(yīng)對異步編程:響應(yīng)式擴(kuò)展(通過RxJava)、Promise和Future、fiber(使用字節(jié)碼手段),等。
由于在利用其它諸如RxJava等抽象之前Vert.x所有API都是面向回調(diào)的,本指南在第一部分只使用回調(diào),以確保讀者熟悉Vert.x的核心概念。從回調(diào)開始,在異步代碼的多部分之間畫一條界線也是比較容易的。一旦回調(diào)不總是易于閱讀的,這個問題在樣例代碼中變得明顯,我們將引入RxJava支持來展示如何通過考慮處理事件Stream以更好的表示同樣的異步代碼。
2.5 Wiki Verticle初始化階段
為了使我們的wiki運(yùn)行,我們需要執(zhí)行一個兩階段初始化:
- 我們需要建立一個JDBC數(shù)據(jù)庫鏈接,還要確保數(shù)據(jù)庫結(jié)構(gòu)準(zhǔn)備就緒。
- 我們需要為Web應(yīng)用啟動一個HTTP Server。
每個階段都可能失?。ㄈ鏗TTP Server的TCP端口已經(jīng)被占用),因此它們不應(yīng)并行執(zhí)行,因?yàn)閃eb應(yīng)用代碼首先需要數(shù)據(jù)庫訪問來工作。
為了使我們的代碼更整潔,我們?yōu)槊總€階段定義一個方法,采取返回一個future/promise對象的模式通知每個階段什么時(shí)候完成,以及它是否成功。
private Future<Void> prepareDatabase() {
Future<Void> future = Future.future();
// (...)
return future;
}
private Future<Void> startHttpServer() {
Future<Void> future = Future.future();
// (...)
return future;
}
通過每個方法返回一個future對象,start方法的實(shí)現(xiàn)變?yōu)橐粋€組裝(composition):
@Override
public void start(Future<Void> startFuture) throws Exception {
Future<Void> steps = prepareDatabase().compose(v -> startHttpServer());
steps.setHandler(startFuture.completer());
}
當(dāng)prepareDatabase的future成功完成時(shí),startHttpServer被調(diào)用,steps完成依賴于startHttpServer返回future的結(jié)果。如果prepareDatabase遇到錯誤,startHttpServer永遠(yuǎn)不會調(diào)用,這種情況下,steps future處于failed狀態(tài),提示描述錯誤的異常并完成。
最后steps完成:setHandler定義了一個handler,它在完成時(shí)調(diào)用。我們這種情況,我們只想使用steps完成startFuture,使用completer方法獲取一個handler。這等價(jià)于:
Future<Void> steps = prepareDatabase().compose(v -> startHttpServer());
steps.setHandler(ar -> { ①
if (ar.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(ar.cause());
}
});
① ar是AsyncResult<Void>類型。AsyncResult<T>用于傳遞一個異步處理的結(jié)果,并且可以在成功時(shí)提供一個T類型的值,或者在處理失敗時(shí)提供一個失敗異常。
2.5.1 數(shù)據(jù)庫初始化
Wiki的數(shù)據(jù)庫結(jié)構(gòu)有唯一的一張表Pages組成,包含以下列:
| 列名 | 類型 | 描述 |
|---|---|---|
| Id | 整型 | 主鍵 |
| Name | 字符型 | Wiki頁面的名稱,必須唯一 |
| Content | 文本 | 一個Wiki頁面的Markdown文本 |
數(shù)據(jù)庫操作通常是創(chuàng)建、讀、更新、刪除操作。一開始,我們先簡單的將相應(yīng)的SQL查詢存儲在MainVerticle類的靜態(tài)屬性中。注意,它們按照HSQLDB理解的SQL方言編寫,但是其它關(guān)系數(shù)據(jù)庫可能并不一定支持:
private static final String SQL_CREATE_PAGES_TABLE = "create table if not exists Pages (Id integer identity primary key,
Name varchar(255) unique, Content clob)";
private static final String SQL_GET_PAGE = "select Id, Content from Pages where Name = ?"; ①
private static final String SQL_CREATE_PAGE = "insert into Pages values (NULL, ?, ?)";
private static final String SQL_SAVE_PAGE = "update Pages set Content = ? where Id = ?";
private static final String SQL_ALL_PAGES = "select Name from Pages";
private static final String SQL_DELETE_PAGE = "delete from Pages where Id = ?";
① 查詢中的?是執(zhí)行查詢時(shí)傳遞數(shù)據(jù)的占位符,Vert.x JDBC客戶端可以由此阻止SQL注入。
應(yīng)用程序Verticle需要保持一個JDBCClient對象的引用(來自io.vertx.ext.jdbc包)作為對數(shù)據(jù)庫的鏈接。我們通過使用MainVerticle中的一個屬性實(shí)現(xiàn),同時(shí)我們還創(chuàng)建了一個通用的logger,來自org.slf4j包:
private JDBCClient dbClient;
private static final Logger LOGGER = LoggerFactory.getLogger(MainVerticle.class);
最后但是同樣重要,這是prepareDatabase方法的完整實(shí)現(xiàn)。它嘗試獲取一個JDBC client鏈接,然后執(zhí)行一個SQL查詢創(chuàng)建Pages表(除非它已經(jīng)存在)。
private Future<Void> prepareDatabase() {
Future<Void> future = Future.future();
dbClient = JDBCClient.createShared(vertx, new JsonObject() ①
.put("url", "jdbc:hsqldb:file:db/wiki") ②
.put("driver_class", "org.hsqldb.jdbcDriver") ③
.put("max_pool_size", 30)); ④
dbClient.getConnection(ar -> { ⑤
if (ar.failed()) {
LOGGER.error("Could not open a database connection", ar.cause());
future.fail(ar.cause()); ⑥
} else {
SQLConnection connection = ar.result(); ⑦
connection.execute(SQL_CREATE_PAGES_TABLE, create -> {
connection.close(); ⑧
if (create.failed()) {
LOGGER.error("Database preparation error", create.cause());
future.fail(create.cause());
} else {
future.complete(); ⑨
}
});
}
});
return future;
}
① createShared方法用于創(chuàng)建一個共享的鏈接,它在vertx實(shí)例已知的Verticle之間共享,這一般來說是件好事。
② JDBC客戶端鏈接通過傳遞一個Vert.x JSON對象來構(gòu)造。此處url是JDBC URL。
③ 就像url,driver_class指定了使用的JDBC驅(qū)動,并指向驅(qū)動類。
④ max_pool_size是并發(fā)鏈接的數(shù)量。我們此處選擇30,這只是一個隨意的數(shù)字。
⑤ 獲得鏈接是一個異步操作,并且返回給我們一個AsyncResult<SQLConnection>對象。它接下來必須被測試,看一下鏈接是否能建立(AsyncResult實(shí)際上是Future的super-interface)。
⑥ 如果SQL鏈接不能獲取,future的方法完成為fail,AsyncResult通過cause方法提供了異常信息。
⑦ SQLConnection是成功的AsyncResult的結(jié)果,我們可以使用它來執(zhí)行一個SQL查詢。
⑧ 在檢查SQL查詢成功與否之前,我們必須通過調(diào)用close釋放它,否則JDBC客戶端連接池最終會耗盡。
⑨ 我們使用一個success完成future對象的方法。
Vert.x項(xiàng)目提供的SQL數(shù)據(jù)庫模塊現(xiàn)在沒提供任何超越SQL查詢的東西(例如一個對象映射器),因?yàn)樗鼈兗杏谔峁?shù)據(jù)庫的異步訪問。
盡管如此,它并未禁止使用來自社區(qū)的更先進(jìn)的模塊,我們特別推薦檢出項(xiàng)目諸如用于Vert.x的jOOq生成器或者POJO映射器。
2.5.2 關(guān)于日志的注記
前面的子章節(jié)還引入了一個logger,我們選擇的是SLFJ庫。Vert.x對于日志也是非教條的(unopinionated):你可以選擇任何流行的Java日志庫。我們推薦使用SLF4J,因?yàn)樗荍ava生態(tài)系統(tǒng)中一個流行的日志抽象和統(tǒng)一庫。
我們還推薦使用Logback作為logger實(shí)現(xiàn)。集成SLF4J和Logback可以通過添加兩個依賴完成,或者只添加logback-classic以指向兩個庫(順便說一下,它們來自同一作者)。
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
默認(rèn)情況下,SLF4J輸出來自Vert.x、Netty、C3PO和Wiki應(yīng)用的很多日志事件到控制臺。我們可以通過添加一個src/main/resources/logback.xml配置文件以減少冗余信息(查看https://logback.qos.ch/了解更多):
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<logger name="com.mchange.v2" level="warn" />
<logger name="io.netty" level="warn" />
<logger name="io.vertx" level="info" />
<logger name="io.vertx.guides.wiki" level="debug" />
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
最后但是同樣重要,HSQLDB在嵌入式的情況下不能與logger很好的集成。默認(rèn)情況下,它嘗試重新配置日志系統(tǒng)來替代,因此在執(zhí)行應(yīng)用時(shí),我們必須通過傳遞一個-Dhsqldb.reconfig_logging=false屬性給Java虛擬機(jī)來禁用它。
2.5.3 HTTP Server初始化
HTTP Server使用vertx-web項(xiàng)目輕易的為接收的HTTP請求定義分發(fā)路由(dispatching routes)。實(shí)際上,Vert.x核心API可以啟動HTTP Server,并監(jiān)聽進(jìn)入的鏈接,但是它未提供任何能力,比如說依賴于請求URL或者處理請求體指定不同的Handler。這是Router角色,它依賴于URL、HTTP方法等,分發(fā)請求到不同的處理Handler。
初始化包括設(shè)置請求路由器,然后啟動HTTP Server:
private Future<Void> startHttpServer() {
Future<Void> future = Future.future();
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);
server.requestHandler(router::accept) ⑤
.listen(8080, ar -> { ⑥
if (ar.succeeded()) {
LOGGER.info("HTTP server running on port 8080");
future.complete();
} else {
LOGGER.error("Could not start a HTTP server", ar.cause());
future.fail(ar.cause());
}
});
return future;
}
① vertx上下文對象提供了創(chuàng)建HTTP服務(wù)器、客戶端、TCP/UDP服務(wù)器和客戶端等的方法。
② Router類來自vertx-web: io.vertx.ext.web.Router。
③ 路由有它們自己的Handler,它們可以通過URL和/或HTTP方法定義。為了簡化Handler,Java lambda是一個選擇,但是對于更復(fù)雜的Handler,引用私有方法作為替代是個好主意。注意URL可以支持參數(shù)變量:/wiki/:page將匹配一個請求如/wiki/Hello,這種情況下一個page參數(shù)可供使用,值為Hello。
④ 這使得所有HTTP POST請求都通過一個第一個Handler,此處是io.vertx.ext.web.handler.BodyHandler。這個Handler自動從HTTP請求解碼請求體(如表單提交),接下來可以將其作為Vert.x緩沖對象來操作。
⑤ router對象可以被用作HTTP服務(wù)器Handler,它接下來分發(fā)請求到上面定義的其它Handler。
⑥ 啟動HTTP服務(wù)器是一個異步操作,因此一個AsyncResult<HttpServer>需要檢測是否成功。順便一提,8080參數(shù)指定了server使用的TCP端口。
2.6 HTTP路由處理(router handler)
startHttpServer方法的HTTP Router實(shí)例依據(jù)URL模式和HTTP方法指向不同的Handler。每個Handler處理HTTP請求,執(zhí)行一個數(shù)據(jù)庫查詢,并且從FreeMarker模板渲染HTML。
2.6.1 Index頁面的Handler
index頁面提供了一個列表指向所有的Wiki記錄,同時(shí)有一個html域(field)來創(chuàng)建一個新Wiki:
它的實(shí)現(xiàn)是一個直截了當(dāng)?shù)膕elect * SQL查詢,然后數(shù)據(jù)傳遞到FreeMarker引擎渲染HTML響應(yīng)。
indexHandler方法的代碼如下:
private final FreeMarkerTemplateEngine templateEngine = FreeMarkerTemplateEngine.create();
private void indexHandler(RoutingContext context) {
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.query(SQL_ALL_PAGES, res -> {
connection.close();
if (res.succeeded()) {
List<String> pages = res.result() ①
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList());
context.put("title", "Wiki home"); ②
context.put("pages", pages);
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(res.cause()); ⑤
}
});
} else {
context.fail(car.cause());
}
});
}
① SQL查詢結(jié)果作為JsonArray和JsonObject實(shí)例返回。
② RoutingContext實(shí)例可以被用于設(shè)置任意鍵值數(shù)據(jù),這些鍵值接下來可以從模板中或者鏈?zhǔn)絩outer handler中獲取。
③ 渲染模板是一個異步操作,這導(dǎo)致我們采用通常的AsyncResult處理模式。
④ AsyncResult在成功的情況下包含模板,渲染為一個String,我們可以使用這個值結(jié)束(end)HTTP響應(yīng)流。
⑤ 失敗的情況下,RoutingContext的fail方法提供了一個明智(sensible)的方法返回HTTP 500錯誤到HTTP客戶端。
FreeMarker模板位于src/main/resources/templates目錄。index.ftl模板代碼如下:
<#include "header.ftl">
<div class="row">
<div class="col-md-12 mt-1">
<div class="float-xs-right">
<form class="form-inline" action="/create" method="post">
<div class="form-group">
<input type="text" class="form-control" id="name" name="name"
placeholder="New page name">
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
<h1 class="display-4">${context.title}</h1>
</div>
<div class="col-md-12 mt-1">
<#list context.pages>
<h2>Pages:</h2>
<ul>
<#items as page>
<li>
<a href="/wiki/${page}">${page}</a>
</li>
</#items>
</ul>
<#else>
<p>The wiki is currently empty!</p>
</#list>
</div>
</div>
<#include "footer.ftl">
存儲在RoutingContext對象中的Key/Value數(shù)據(jù),可以通過Freemarker的context變量使用。
由于大量的模板有通用的header和footer,我們提取下面的代碼到header.ftl和footer.ftl中:
header.ftl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="stylesheet"
integrity="sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi"
crossorigin="anonymous">
<title>${context.title} | A Sample Vert.x-powered Wiki</title>
</head>
<body>
<div class="container">
footer.ftl
</div> <!-- .container -->
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"
integrity="sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7"
crossorigin="anonymous"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js"
integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8"
crossorigin="anonymous"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js"
integrity="sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK"
crossorigin="anonymous"></script>
</body>
</html>
2.6.2 Wiki頁面渲染Handler
這個Handler處理HTTP GET請求,渲染W(wǎng)iki 頁面,如:
頁面還提供了一個按鈕來在Markdown中編輯內(nèi)容。編輯沒有獨(dú)立的Handler和模板,我們簡單的依靠JavaScript和CSS來在按鈕點(diǎn)擊時(shí)切換編輯器開和關(guān):
pageRenderingHandler方法的代碼如下:
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 page = context.request().getParam("page"); ①
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.queryWithParams(SQL_GET_PAGE, new JsonArray().add(page), fetch -> { ②
connection.close();
if (fetch.succeeded()) {
JsonArray row = fetch.result().getResults()
.stream()
.findFirst()
.orElseGet(() -> new JsonArray().add(-1).add(EMPTY_PAGE_MARKDOWN));
Integer id = row.getInteger(0);
String rawContent = row.getString(1);
context.put("title", page);
context.put("id", id);
context.put("newPage", fetch.result().getResults().size() == 0 ? "yes" : "no");
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(fetch.cause());
}
});
} else {
context.fail(car.cause());
}
});
}
① URL參數(shù)(/wiki/:name)可以通過context請求對象訪問。
② 傳遞參數(shù)值給SQL查詢通過一個JsonArray完成,元素按照SQL查詢中?符號的順序。
③ Processor類來自我們使用的txtmark Markdown渲染庫。
page.ftl FreeMarker模板代碼如下:
<#include "header.ftl">
<div class="row">
<div class="col-md-12 mt-1">
<span class="float-xs-right">
<a class="btn btn-outline-primary" href="/" role="button"
aria-pressed="true">Home</a>
<button class="btn btn-outline-warning" type="button"
data-toggle="collapse" data-target="#editor" aria-expanded="false"
aria-controls="editor">Edit</button>
</span>
<h1 class="display-4">
<span class="text-muted">{</span>
${context.title}
<span class="text-muted">}</span>
</h1>
</div>
<div class="col-md-12 mt-1 clearfix">
${context.content}
</div>
<div class="col-md-12 collapsable collapse clearfix" id="editor">
<form action="/save" method="post">
<div class="form-group">
<input type="hidden" name="id" value="${context.id}">
<input type="hidden" name="title" value="${context.title}">
<input type="hidden" name="newPage" value="${context.newPage}">
<textarea class="form-control" id="markdown" name="markdown"
rows="15">${context.rawContent}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<#if context.id != -1>
<button type="submit" formaction="/delete"
class="btn btn-danger float-xs-right">Delete</button>
</#if>
</form>
</div>
<div class="col-md-12 mt-1">
<hr class="mt-1">
<p class="small">Rendered: ${context.timestamp}</p>
</div>
</div>
<#include "footer.ftl">
2.6.3 頁面創(chuàng)建Handler
index頁面提供了一個html域(field)來創(chuàng)建一個新的Wiki頁面,它所在的HTML表單指向的URL由這個Handler管理。這個Handler的處理策略不是在數(shù)據(jù)庫中實(shí)際創(chuàng)建一個新的記錄,而是簡單的帶著需要創(chuàng)建的名稱定向到一個Wiki頁面URL。由于Wiki頁面不存在,pageRenderingHandler方法將為新頁面使用一個默認(rèn)的文本,最后用戶可以通過編輯并且保存來創(chuàng)建這個頁面。
它的Handler是pageCreateHandler方法,它的實(shí)現(xiàn)是通過一個303狀態(tài)碼進(jìn)行的重定向,:
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();
}
2.6.4 頁面保存Handler
pageUpdateHandler方法用于在保存一個Wiki頁面時(shí)處理HTTP POST請求。這在更新一個已存在的頁面(發(fā)出一條SQL更新查詢)或保存一個新的頁面(發(fā)出一條SQL插入查詢)時(shí)發(fā)生:
private void pageUpdateHandler(RoutingContext context) {
String id = context.request().getParam("id"); ①
String title = context.request().getParam("title");
String markdown = context.request().getParam("markdown");
boolean newPage = "yes".equals(context.request().getParam("newPage")); ②
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
String sql = newPage ? SQL_CREATE_PAGE : SQL_SAVE_PAGE;
JsonArray params = new JsonArray(); ③
if (newPage) {
params.add(title).add(markdown);
} else {
params.add(markdown).add(id);
}
connection.updateWithParams(sql, params, res -> { ④
connection.close();
if (res.succeeded()) {
context.response().setStatusCode(303); ⑤
context.response().putHeader("Location", "/wiki/" + title);
context.response().end();
} else {
context.fail(res.cause());
}
});
} else {
context.fail(car.cause());
}
});
}
① 表單參數(shù)通過一個HTTP POST請求發(fā)送,并且可以通過RoutingContext對象訪問。注意如果在Router配置鏈中沒有BodyHandler,那么這些值將不是有效的,表單提交的荷載(payload)需要手動從HTTP POST請求荷載中解碼。
② 我們通過FreeMarker模板page.ftl中渲染的一個hidden表單域(newPage)來判斷我們是在更新已存在的頁面還是保存一個新頁面。
③ 再一次,使用一個JsonArray傳遞值來準(zhǔn)備(prepareing)帶參數(shù)的SQL查詢。
④ updateWithParams方法用于insert/update/delete SQL查詢。
⑤ 成功時(shí),我們簡單的重定向到被編輯的頁面。
2.6.5 頁面刪除Handler
pageDeletionHandler方法的實(shí)現(xiàn)是明確的:給定一個Wiki記錄的標(biāo)識,它發(fā)出一個delete SQL查詢,并重定向到Wiki的index頁面:
private void pageDeletionHandler(RoutingContext context) {
String id = context.request().getParam("id");
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.updateWithParams(SQL_DELETE_PAGE, new JsonArray().add(id), res -> {
connection.close();
if (res.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/");
context.response().end();
} else {
context.fail(res.cause());
}
});
} else {
context.fail(car.cause());
}
});
}
2.7 運(yùn)行應(yīng)用
到這一步,我們有了一個可工作的、自包含的Wiki應(yīng)用。
為了運(yùn)行它,我們首先需要使用Maven構(gòu)建它:
$mvn clean package
由于構(gòu)建產(chǎn)生了一個包含了所有需要依賴的JAR(包含Vert.x和一個JDBC數(shù)據(jù)庫),因此運(yùn)行Wiki簡單如:
$ java -jar target/wiki-step-1-1.2.0-fat.jar
你接下來可以將你喜歡的瀏覽器指向http://localhost:8080,享受使用這個Wiki。