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

感興趣的朋友竭鞍,可以關注微信服務號“猿學堂社區(qū)”恋沃,或加入“猿學堂社區(qū)”微信交流群

版權聲明:本文由作者自行翻譯顺呕,未經作者授權枫攀,不得隨意轉發(fā)

我們將從第一次迭代開始括饶,最簡單的代碼可能是使用Vert.x編寫一個Wiki。而下一次迭代將在代碼庫中引入更多的簡潔以及適當的測試脓豪,我們將看到基于Vert.x的快速原型是一個既簡單又現實的目標。

現階段忌卤,Wiki將使用HTML頁面的服務端渲染以及通過JDBC鏈接進行數據持久化扫夜。為了完成這些,我們將使用以下庫驰徊。

  1. Vert.x Web笤闯,雖然Vert.x核心庫支持HTTP服務器的創(chuàng)建,但是它未提供優(yōu)雅的API來處理路由棍厂、請求荷載處理等颗味。
  2. Vert.x JDBC client,提供一套JDBC的異步API牺弹。
  3. Apache FreeMarker浦马,用于渲染服務端頁面,它是一個簡單的模板引擎张漂。
  4. Txtmark晶默,用于將Markdown文本渲染為HTML,允許以Markdown編輯Wiki頁面航攒。

2.1 引導一個Maven項目

該指南選擇使用Apache Maven作為構建工具磺陡,主要因為它與主要的集成開發(fā)環(huán)境集成的非常好。你同樣可以使用其它的構建工具漠畜,如Gradle币他。

Vert.x社區(qū)在提供了一個可以克隆的模板項目結構。由于你也可能希望使用(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

該項目提供了一個樣例Verticle以及一個單元測試瘾敢。你可以安全的刪除src/目錄下的所有.java文件來修改Wiki辫封,但是在這么做之前,您可以先測試一下項目是否成功構建和運行廉丽。

mvn package exec:java

你將注意到Maven項目的pom.xml文件做了兩件有意思的事情:

  1. 它使用Maven Shade Plugin創(chuàng)建了一個包含所有需要依賴的單個JAR的歸檔文件倦微,后綴為-fat.jar,也稱為“FatJar”正压。
  2. 它使用Exec Maven Plugin來提供exec:java目標(goal)欣福,通過Vert.x的io.vertx.core.Launcher類依次啟動應用。這實際上等價于使用Vert.x發(fā)行版中提供的vertx命令行工具運行焦履。

最后拓劝,你會注意到redeploy.sh和redeploy.bat腳本的存在雏逾,你可以相應的使用它們來自動編譯和重新部署變更的代碼。注意郑临,這樣做需要確保腳本中的VERTICLE變量與使用的主Verticle匹配栖博。

另外,Fabric8項目提供了一個Vert.x Maven插件厢洞。它包含了初始化仇让、構建、打包及運行一個Vert.x項目的goal躺翻。

與克隆Git starter倉庫一樣生成一個類似項目:

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文件中的第一批依賴項是用于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數據庫訪問需要的:

<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兼容數據庫的訪問公你,當然我們的項目需要在類路徑有一個JDBC驅動踊淳。

HSQLDB是一款知名的使用Java編寫的關系數據庫。它在作為嵌入式數據庫使用陕靠,從而避免需要獨立運行第三方數據庫服務器的時候非常受歡迎迂尝。它還在單元測試以及集成測試方面比較受歡迎,因為它提供了一個(輕快的)內存存儲剪芥。

HSQLDB作為一個嵌入式數據庫雹舀,非常適合我們的入門。它存儲數據到本地文件粗俱,由于HSQLDB庫的JAR提供了一個JDBC驅動说榆,Vert.x JDBC配置將非常直接。

Vert.x還提供了專用的MySQL和PostgreSQL客戶端庫寸认。

當然签财,你可以使用通用的Vert.x JDBC客戶端連接MySQL或PostgreSQL數據庫,但是與阻塞的JDBC API相比偏塞,通過使用這兩個數據庫服務器的網絡協(xié)議唱蒸,這些庫提供了更好的性能。

Vert.x也提供了處理流行的非關系型數據庫MongoDB和Redis的庫灸叼。廣泛的社區(qū)還提供了與其它存儲系統(tǒng)的集成神汹,如Apache Cassandra、OrientDB或ElasticSearch古今。

2.3 Verticle剖析

我們Wiki的Verticle由一個單獨的io.vertx.guides.wiki.MainVerticle類組成屁魏。這個類擴展自io.vertx.core.AbstractVerticle(主要提供的Verticle的基類):

  1. 復寫生命周期的start和stop方法。
  2. 一個名為vertx的保護屬性捉腥,它是該Verticle被部署到的Vert.x環(huán)境的引用氓拼。
  3. 配置對象的訪問器,允許向Verticle傳遞外部配置。

開始,我們的Verticle只需要按下面復寫start方法:

public class MainVerticle extends AbstractVerticle {
    @Override
    public void start(Future<Void> startFuture) throws Exception {
        startFuture.complete();
    }
}

存在兩種形式的start(和stop)方法:一個是無參的桃漾,另一個有一個future對象引用坏匪。無參的變體方法意味著Verticle初始化或者內務階段總是成功,除非拋出一個異常撬统。包含future對象參數的變體方法提供了一個更細粒度的方法來在最后指示操作是否成功适滓。事實上,一些初始化或清理代碼可能需要異步操作恋追,因此通過future對象進行報告理所當然符合異步風格凭迹。

2.4 關于future對象和回調的一兩句話

Vert.x future不是JDK的future:它們可以組裝以及以非阻塞的方式查詢。它們應用于異步任務的簡單協(xié)調几于,尤其是Verticle部署和檢查它們是否部署成功蕊苗。

Vert.x核心API基于異步事件通知回調沿后。經驗豐富的開發(fā)人員自然會想到這打開了稱為“回調地獄”的大門沿彭,多個層次的嵌套回調使得代碼難以理解,如該虛構代碼所示:

foo.a(1, res1 -> {
    if (res1.succeeded()) {
        bar.b("abc", 1, res2 -> {
            if (res.succeeded()) {
                baz.c(res3 -> {
                    dosomething(res1, res2, res3, res4 -> {
                        // (...)
                    });
                });
            }
        });
    }
});

雖然核心API被設計成支持Promise和Future尖滚,但選擇回調實際上是有意思的喉刘,因為它允許使用不同的編程抽象。Vert.x是一個總體上非教條的(un-opinionated)項目漆弄,而且回調允許不同模型的實現睦裳,以更好的應對異步編程:響應式擴展(通過RxJava)、Promise和Future撼唾、fiber(使用字節(jié)碼手段)廉邑,等。

由于在利用其它諸如RxJava等抽象之前Vert.x所有API都是面向回調的倒谷,本指南在第一部分只使用回調蛛蒙,以確保讀者熟悉Vert.x的核心概念。從回調開始渤愁,在異步代碼的多部分之間畫一條界線也是比較容易的牵祟。一旦回調不總是易于閱讀的,這個問題在樣例代碼中變得明顯抖格,我們將引入RxJava支持來展示如何通過考慮處理事件Stream以更好的表示同樣的異步代碼诺苹。

2.5 Wiki Verticle初始化階段

為了使我們的wiki運行,我們需要執(zhí)行一個兩階段初始化:

  1. 我們需要建立一個JDBC數據庫鏈接雹拄,還要確保數據庫結構準備就緒玲躯。
  2. 我們需要為Web應用啟動一個HTTP Server。

每個階段都可能失敺嫘场(如HTTP Server的TCP端口已經被占用)垢箕,因此它們不應并行執(zhí)行,因為Web應用代碼首先需要數據庫訪問來工作。

為了使我們的代碼更整潔损姜,我們?yōu)槊總€階段定義一個方法饰剥,采取返回一個future/promise對象的模式通知每個階段什么時候完成,以及它是否成功摧阅。

private Future<Void> prepareDatabase() {
    Future<Void> future = Future.future();
    // (...)
    return future;
}
private Future<Void> startHttpServer() {
    Future<Void> future = Future.future();
    // (...)
    return future;
}

通過每個方法返回一個future對象汰蓉,start方法的實現變?yōu)橐粋€組裝(composition):

@Override
public void start(Future<Void> startFuture) throws Exception {
    Future<Void> steps = prepareDatabase().compose(v -> startHttpServer());
    steps.setHandler(startFuture.completer());
}

當prepareDatabase的future成功完成時,startHttpServer被調用棒卷,steps完成依賴于startHttpServer返回future的結果顾孽。如果prepareDatabase遇到錯誤,startHttpServer永遠不會調用比规,這種情況下若厚,steps future處于failed狀態(tài),提示描述錯誤的異常并完成蜒什。

最后steps完成:setHandler定義了一個handler测秸,它在完成時調用。我們這種情況灾常,我們只想使用steps完成startFuture霎冯,使用completer方法獲取一個handler。這等價于:

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>用于傳遞一個異步處理的結果沈撞,并且可以在成功時提供一個T類型的值,或者在處理失敗時提供一個失敗異常雕什。

2.5.1 數據庫初始化

Wiki的數據庫結構有唯一的一張表Pages組成缠俺,包含以下列:

列名 類型 描述
Id 整型 主鍵
Name 字符型 Wiki頁面的名稱,必須唯一
Content 文本 一個Wiki頁面的Markdown文本

數據庫操作通常是創(chuàng)建贷岸、讀壹士、更新、刪除操作凰盔。一開始墓卦,我們先簡單的將相應的SQL查詢存儲在MainVerticle類的靜態(tài)屬性中。注意户敬,它們按照HSQLDB理解的SQL方言編寫落剪,但是其它關系數據庫可能并不一定支持:

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í)行查詢時傳遞數據的占位符,Vert.x JDBC客戶端可以由此阻止SQL注入尿庐。

應用程序Verticle需要保持一個JDBCClient對象的引用(來自io.vertx.ext.jdbc包)作為對數據庫的鏈接忠怖。我們通過使用MainVerticle中的一個屬性實現,同時我們還創(chuàng)建了一個通用的logger抄瑟,來自org.slf4j包:

private JDBCClient dbClient;
private static final Logger LOGGER = LoggerFactory.getLogger(MainVerticle.class);

最后但是同樣重要凡泣,這是prepareDatabase方法的完整實現。它嘗試獲取一個JDBC client鏈接,然后執(zhí)行一個SQL查詢創(chuàng)建Pages表(除非它已經存在)鞋拟。

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實例已知的Verticle之間共享,這一般來說是件好事贺纲。

② JDBC客戶端鏈接通過傳遞一個Vert.x JSON對象來構造航闺。此處url是JDBC URL。

③ 就像url猴誊,driver_class指定了使用的JDBC驅動潦刃,并指向驅動類。

④ max_pool_size是并發(fā)鏈接的數量懈叹。我們此處選擇30乖杠,這只是一個隨意的數字。

⑤ 獲得鏈接是一個異步操作澄成,并且返回給我們一個AsyncResult<SQLConnection>對象胧洒。它接下來必須被測試,看一下鏈接是否能建立(AsyncResult實際上是Future的super-interface)环揽。

⑥ 如果SQL鏈接不能獲取略荡,future的方法完成為fail庵佣,AsyncResult通過cause方法提供了異常信息歉胶。

⑦ SQLConnection是成功的AsyncResult的結果,我們可以使用它來執(zhí)行一個SQL查詢巴粪。

⑧ 在檢查SQL查詢成功與否之前通今,我們必須通過調用close釋放它,否則JDBC客戶端連接池最終會耗盡肛根。

⑨ 我們使用一個success完成future對象的方法辫塌。

Vert.x項目提供的SQL數據庫模塊現在沒提供任何超越SQL查詢的東西(例如一個對象映射器),因為它們集中于提供數據庫的異步訪問派哲。

盡管如此臼氨,它并未禁止使用來自社區(qū)的更先進的模塊,我們特別推薦檢出項目諸如用于Vert.x的jOOq生成器或者POJO映射器芭届。

2.5.2 關于日志的注記

前面的子章節(jié)還引入了一個logger储矩,我們選擇的是SLFJ庫。Vert.x對于日志也是非教條的(unopinionated):你可以選擇任何流行的Java日志庫褂乍。我們推薦使用SLF4J持隧,因為它是Java生態(tài)系統(tǒng)中一個流行的日志抽象和統(tǒng)一庫。

我們還推薦使用Logback作為logger實現逃片。集成SLF4J和Logback可以通過添加兩個依賴完成屡拨,或者只添加logback-classic以指向兩個庫(順便說一下,它們來自同一作者)。

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

默認情況下呀狼,SLF4J輸出來自Vert.x裂允、Netty、C3PO和Wiki應用的很多日志事件到控制臺哥艇。我們可以通過添加一個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很好的集成。默認情況下她奥,它嘗試重新配置日志系統(tǒng)來替代瓮增,因此在執(zhí)行應用時,我們必須通過傳遞一個-Dhsqldb.reconfig_logging=false屬性給Java虛擬機來禁用它哩俭。

2.5.3 HTTP Server初始化

HTTP Server使用vertx-web項目輕易的為接收的HTTP請求定義分發(fā)路由(dispatching routes)绷跑。實際上,Vert.x核心API可以啟動HTTP Server凡资,并監(jiān)聽進入的鏈接砸捏,但是它未提供任何能力,比如說依賴于請求URL或者處理請求體指定不同的Handler隙赁。這是Router角色垦藏,它依賴于URL、HTTP方法等伞访,分發(fā)請求到不同的處理Handler掂骏。

初始化包括設置請求路由器,然后啟動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服務器厚掷、客戶端弟灼、TCP/UDP服務器和客戶端等的方法。

② Router類來自vertx-web: io.vertx.ext.web.Router冒黑。

③ 路由有它們自己的Handler田绑,它們可以通過URL和/或HTTP方法定義。為了簡化Handler抡爹,Java lambda是一個選擇掩驱,但是對于更復雜的Handler,引用私有方法作為替代是個好主意冬竟。注意URL可以支持參數變量:/wiki/:page將匹配一個請求如/wiki/Hello欧穴,這種情況下一個page參數可供使用,值為Hello诱咏。

④ 這使得所有HTTP POST請求都通過一個第一個Handler苔可,此處是io.vertx.ext.web.handler.BodyHandler。這個Handler自動從HTTP請求解碼請求體(如表單提交)袋狞,接下來可以將其作為Vert.x緩沖對象來操作焚辅。

⑤ router對象可以被用作HTTP服務器Handler映屋,它接下來分發(fā)請求到上面定義的其它Handler。

⑥ 啟動HTTP服務器是一個異步操作同蜻,因此一個AsyncResult<HttpServer>需要檢測是否成功棚点。順便一提,8080參數指定了server使用的TCP端口湾蔓。

2.6 HTTP路由處理(router handler)

startHttpServer方法的HTTP Router實例依據URL模式和HTTP方法指向不同的Handler瘫析。每個Handler處理HTTP請求,執(zhí)行一個數據庫查詢默责,并且從FreeMarker模板渲染HTML贬循。

2.6.1 Index頁面的Handler

index頁面提供了一個列表指向所有的Wiki記錄,同時有一個html域(field)來創(chuàng)建一個新Wiki:

它的實現是一個直截了當的select * SQL查詢桃序,然后數據傳遞到FreeMarker引擎渲染HTML響應杖虾。

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查詢結果作為JsonArray和JsonObject實例返回。

② RoutingContext實例可以被用于設置任意鍵值數據,這些鍵值接下來可以從模板中或者鏈式router handler中獲取媒熊。

③ 渲染模板是一個異步操作奇适,這導致我們采用通常的AsyncResult處理模式。

④ AsyncResult在成功的情況下包含模板芦鳍,渲染為一個String嚷往,我們可以使用這個值結束(end)HTTP響應流。

⑤ 失敗的情況下柠衅,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數據茄茁,可以通過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請求裙顽,渲染Wiki 頁面,如:

頁面還提供了一個按鈕來在Markdown中編輯內容宣谈。編輯沒有獨立的Handler和模板愈犹,我們簡單的依靠JavaScript和CSS來在按鈕點擊時切換編輯器開和關:

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參數(/wiki/:name)可以通過context請求對象訪問。

② 傳遞參數值給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的處理策略不是在數據庫中實際創(chuàng)建一個新的記錄,而是簡單的帶著需要創(chuàng)建的名稱定向到一個Wiki頁面URL侥祭。由于Wiki頁面不存在叁执,pageRenderingHandler方法將為新頁面使用一個默認的文本茄厘,最后用戶可以通過編輯并且保存來創(chuàng)建這個頁面。

它的Handler是pageCreateHandler方法谈宛,它的實現是通過一個303狀態(tài)碼進行的重定向次哈,:

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頁面時處理HTTP POST請求。這在更新一個已存在的頁面(發(fā)出一條SQL更新查詢)或保存一個新的頁面(發(fā)出一條SQL插入查詢)時發(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());
        }
    });
}

① 表單參數通過一個HTTP POST請求發(fā)送吆录,并且可以通過RoutingContext對象訪問窑滞。注意如果在Router配置鏈中沒有BodyHandler,那么這些值將不是有效的恢筝,表單提交的荷載(payload)需要手動從HTTP POST請求荷載中解碼哀卫。

② 我們通過FreeMarker模板page.ftl中渲染的一個hidden表單域(newPage)來判斷我們是在更新已存在的頁面還是保存一個新頁面。

③ 再一次撬槽,使用一個JsonArray傳遞值來準備(prepareing)帶參數的SQL查詢聊训。

④ updateWithParams方法用于insert/update/delete SQL查詢。

⑤ 成功時恢氯,我們簡單的重定向到被編輯的頁面带斑。

2.6.5 頁面刪除Handler

pageDeletionHandler方法的實現是明確的:給定一個Wiki記錄的標識,它發(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 運行應用

到這一步勋磕,我們有了一個可工作的、自包含的Wiki應用敢靡。

為了運行它挂滓,我們首先需要使用Maven構建它:

$mvn clean package

由于構建產生了一個包含了所有需要依賴的JAR(包含Vert.x和一個JDBC數據庫),因此運行Wiki簡單如:

$ java -jar target/wiki-step-1-1.2.0-fat.jar

你接下來可以將你喜歡的瀏覽器指向http://localhost:8080啸胧,享受使用這個Wiki赶站。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纺念,隨后出現的幾起案子贝椿,更是在濱河造成了極大的恐慌,老刑警劉巖陷谱,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烙博,死亡現場離奇詭異,居然都是意外死亡烟逊,警方通過查閱死者的電腦和手機渣窜,發(fā)現死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪躯,“玉大人乔宿,你說我怎么就攤上這事》醚” “怎么了详瑞?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵囤官,是天一觀的道長。 經常有香客問我蛤虐,道長党饮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任驳庭,我火速辦了婚禮刑顺,結果婚禮上,老公的妹妹穿的比我還像新娘饲常。我一直安慰自己蹲堂,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布贝淤。 她就那樣靜靜地躺著柒竞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪播聪。 梳的紋絲不亂的頭發(fā)上朽基,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音离陶,去河邊找鬼稼虎。 笑死,一個胖子當著我的面吹牛招刨,可吹牛的內容都是我干的霎俩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼沉眶,長吁一口氣:“原來是場噩夢啊……” “哼打却!你這毒婦竟也來了?” 一聲冷哼從身側響起谎倔,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤柳击,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后传藏,有當地人在樹林里發(fā)現了一具尸體腻暮,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年毯侦,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片具垫。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡侈离,死狀恐怖,靈堂內的尸體忽然破棺而出筝蚕,到底是詐尸還是另有隱情卦碾,我是刑警寧澤铺坞,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站洲胖,受9級特大地震影響济榨,放射性物質發(fā)生泄漏。R本人自食惡果不足惜绿映,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一擒滑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叉弦,春花似錦丐一、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至樱拴,卻和暖如春柠衍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晶乔。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工拧略, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘪弓。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓垫蛆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腺怯。 傳聞我的和親對象是個殘疾皇子袱饭,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容