Vert.x Java開發(fā)指南——第二章 使用Vert.x編寫最小可用Wiki

感興趣的朋友,可以關(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ù)持久化。為了完成這些,我們將使用以下庫。

  1. Vert.x Web,雖然Vert.x核心庫支持HTTP服務(wù)器的創(chuàng)建,但是它未提供優(yōu)雅的API來處理路由、請求荷載處理等。
  2. Vert.x JDBC client,提供一套JDBC的異步API。
  3. Apache FreeMarker,用于渲染服務(wù)端頁面,它是一個簡單的模板引擎。
  4. 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文件做了兩件有意思的事情:

  1. 它使用Maven Shade Plugin創(chuàng)建了一個包含所有需要依賴的單個JAR的歸檔文件,后綴為-fat.jar,也稱為“FatJar”。
  2. 它使用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的基類):

  1. 復(fù)寫生命周期的start和stop方法。
  2. 一個名為vertx的保護(hù)屬性,它是該Verticle被部署到的Vert.x環(huán)境的引用。
  3. 配置對象的訪問器,允許向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í)行一個兩階段初始化:

  1. 我們需要建立一個JDBC數(shù)據(jù)庫鏈接,還要確保數(shù)據(jù)庫結(jié)構(gòu)準(zhǔn)備就緒。
  2. 我們需要為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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容