摘要:本文章是藍圖系列的第一篇教程。是事件驅(qū)動的,同時也是非阻塞的。是一組負責分發(fā)和處理事件的線程。注意,我們絕對不能去阻塞線程,否則事件的處理過程會被阻塞,我們的應(yīng)用就失去了響應(yīng)能力。每個負責處理請求并且寫入回應(yīng)結(jié)果。
本文章是 Vert.x 藍圖系列 的第一篇教程。全系列:
Vert.x Blueprint 系列教程(一) | 待辦事項服務(wù)開發(fā)教程
Vert.x Blueprint 系列教程(二) | 開發(fā)基于消息的應(yīng)用 - Vert.x Kue 教程
Vert.x Blueprint 系列教程(三) | Micro-Shop 微服務(wù)應(yīng)用實戰(zhàn)
本系列已發(fā)布至Vert.x官網(wǎng):Vert.x Blueprint Tutorials
前言在本教程中,我們會使用Vert.x來一步一步地開發(fā)一個REST風格的Web服務(wù) - Todo Backend,你可以把它看作是一個簡單的待辦事項服務(wù),我們可以自由添加或者取消各種待辦事項。
通過本教程,你將會學習到以下的內(nèi)容:
Vert.x 是什么,以及其基本設(shè)計思想
Verticle是什么,以及如何使用Verticle
如何用 Vert.x Web 來開發(fā)REST風格的Web服務(wù)
異步編程風格 的應(yīng)用
如何通過 Vert.x 的各種組件來進行數(shù)據(jù)的存儲操作(如 Redis 和 MySQL)
本教程是Vert.x 藍圖系列的第一篇教程,對應(yīng)的Vert.x版本為3.3.0。本教程中的完整代碼已托管至GitHub。
踏入Vert.x之門朋友,歡迎來到Vert.x的世界!初次聽說Vert.x,你一定會非常好奇:這是啥?讓我們來看一下Vert.x的官方解釋:
Vert.x is a tool-kit for building reactive applications on the JVM.
(⊙o⊙)哦哦。。。翻譯一下,Vert.x是一個在JVM上構(gòu)建 響應(yīng)式 應(yīng)用的 工具集 。這個定義比較模糊,我們來簡單解釋一下:工具集 意味著Vert.x非常輕量,可以嵌入到你當前的應(yīng)用中而不需要改變現(xiàn)有的結(jié)構(gòu);另一個重要的描述是 響應(yīng)式 —— Vert.x就是為構(gòu)建響應(yīng)式應(yīng)用(系統(tǒng))而設(shè)計的。響應(yīng)式系統(tǒng)這個概念在 Reactive Manifesto 中有詳細的定義。我們在這里總結(jié)4個要點:
響應(yīng)式的(Responsive):一個響應(yīng)式系統(tǒng)需要在 合理 的時間內(nèi)處理請求。
彈性的(Resilient):一個響應(yīng)式系統(tǒng)必須在遇到 異常 (崩潰,超時, 500 錯誤等等)的時候保持響應(yīng)的能力,所以它必須要為 異常處理 而設(shè)計。
可伸縮的(Elastic):一個響應(yīng)式系統(tǒng)必須在不同的負載情況下都要保持響應(yīng)能力,所以它必須能伸能縮,并且可以利用最少的資源來處理負載。
消息驅(qū)動:一個響應(yīng)式系統(tǒng)的各個組件之間通過 異步消息傳遞 來進行交互。
Vert.x是事件驅(qū)動的,同時也是非阻塞的。首先,我們來介紹 Event Loop 的概念。Event Loop是一組負責分發(fā)和處理事件的線程。注意,我們絕對不能去阻塞Event Loop線程,否則事件的處理過程會被阻塞,我們的應(yīng)用就失去了響應(yīng)能力。因此當我們在寫Vert.x應(yīng)用的時候,我們要時刻謹記 異步非阻塞開發(fā)模式 而不是傳統(tǒng)的阻塞開發(fā)模式。我們將會在下面詳細講解異步非阻塞開發(fā)模式。
我們的應(yīng)用 - 待辦事項服務(wù)我們的應(yīng)用是一個REST風格的待辦事項服務(wù),它非常簡單,整個API其實就圍繞著 增刪改查 四種操作。所以我們可以設(shè)計以下的路由:
添加待辦事項: POST /todos
獲取某一待辦事項: GET /todos/:todoId
獲取所有待辦事項: GET /todos
更新待辦事項: PATCH /todos/:todoId
刪除某一待辦事項: DELETE /todos/:todoId
刪除所有待辦事項: DELETE /todos
注意我們這里不討論REST風格API的設(shè)計規(guī)范(仁者見仁,智者見智),因此你也可以用你喜歡的方式去定義路由。
下面我們開始開發(fā)我們的項目!High起來~~~
說干就干!Vert.x Core提供了一些較為底層的處理HTTP請求的功能,這對于Web開發(fā)來說不是很方便,因為我們通常不需要這么底層的功能,因此Vert.x Web應(yīng)運而生。Vert.x Web基于Vert.x Core,并且提供一組更易于創(chuàng)建Web應(yīng)用的上層功能(如路由)。
Gradle配置文件首先我們先來創(chuàng)建我們的項目。在本教程中我們使用Gradle作為構(gòu)建工具,當然你也可以使用其它諸如Maven之類的構(gòu)建工具。我們的項目目錄里需要有:
src/main/java 文件夾(源碼目錄)
src/test/java 文件夾(測試目錄)
build.gradle 文件(Gradle配置文件)
. ├── build.gradle ├── settings.gradle ├── src │ ├── main │ │ └── java │ └── test │ └── java
我們首先來創(chuàng)建 build.gradle 文件,這是Gradle對應(yīng)的配置文件:
apply plugin: "java" targetCompatibility = 1.8 sourceCompatibility = 1.8 repositories { mavenCentral() mavenLocal() } dependencies { compile "io.vertx:vertx-core:3.3.0" compile "io.vertx:vertx-web:3.3.0" testCompile "io.vertx:vertx-unit:3.3.0" testCompile group: "junit", name: "junit", version: "4.12" }
你可能不是很熟悉Gradle,這不要緊。我們來解釋一下:
我們將 targetCompatibility 和 sourceCompatibility 這兩個值都設(shè)為1.8,代表目標Java版本是Java 8。這非常重要,因為Vert.x就是基于Java 8構(gòu)建的。
在dependencies中,我們聲明了我們需要的依賴。vertx-core 和 vert-web 用于開發(fā)REST API。
注: 若國內(nèi)用戶出現(xiàn)用Gradle解析依賴非常緩慢的情況,可以嘗試使用開源中國Maven鏡像代替默認的鏡像(有的時候速度比較快)。只要在build.gradle中配置即可:
repositories { maven { url "http://maven.oschina.net/content/groups/public/" } mavenLocal() }
搞定build.gradle以后,我們開始寫代碼!
待辦事項對象首先我們需要創(chuàng)建我們的數(shù)據(jù)實體對象 - Todo 實體。在io.vertx.blueprint.todolist.entity包下創(chuàng)建Todo類,并且編寫以下代碼:
package io.vertx.blueprint.todolist.entity; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.concurrent.atomic.AtomicInteger; @DataObject(generateConverter = true) public class Todo { private static final AtomicInteger acc = new AtomicInteger(0); // counter private int id; private String title; private Boolean completed; private Integer order; private String url; public Todo() { } public Todo(Todo other) { this.id = other.id; this.title = other.title; this.completed = other.completed; this.order = other.order; this.url = other.url; } public Todo(JsonObject obj) { TodoConverter.fromJson(obj, this); // 還未生成Converter的時候需要先注釋掉,生成過后再取消注釋 } public Todo(String jsonStr) { TodoConverter.fromJson(new JsonObject(jsonStr), this); } public Todo(int id, String title, Boolean completed, Integer order, String url) { this.id = id; this.title = title; this.completed = completed; this.order = order; this.url = url; } public JsonObject toJson() { JsonObject json = new JsonObject(); TodoConverter.toJson(this, json); return json; } public int getId() { return id; } public void setId(int id) { this.id = id; } public void setIncId() { this.id = acc.incrementAndGet(); } public static int getIncId() { return acc.get(); } public static void setIncIdWith(int n) { acc.set(n); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Boolean isCompleted() { return getOrElse(completed, false); } public void setCompleted(Boolean completed) { this.completed = completed; } public Integer getOrder() { return getOrElse(order, 0); } public void setOrder(Integer order) { this.order = order; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Todo todo = (Todo) o; if (id != todo.id) return false; if (!title.equals(todo.title)) return false; if (completed != null ? !completed.equals(todo.completed) : todo.completed != null) return false; return order != null ? order.equals(todo.order) : todo.order == null; } @Override public int hashCode() { int result = id; result = 31 * result + title.hashCode(); result = 31 * result + (completed != null ? completed.hashCode() : 0); result = 31 * result + (order != null ? order.hashCode() : 0); return result; } @Override public String toString() { return "Todo -> {" + "id=" + id + ", title="" + title + """ + ", completed=" + completed + ", order=" + order + ", url="" + url + """ + "}"; } privateT getOrElse(T value, T defaultValue) { return value == null ? defaultValue : value; } public Todo merge(Todo todo) { return new Todo(id, getOrElse(todo.title, title), getOrElse(todo.completed, completed), getOrElse(todo.order, order), url); } }
我們的 Todo 實體對象由序號id、標題title、次序order、地址url以及代表待辦事項是否完成的一個標識complete組成。我們可以把它看作是一個簡單的Java Bean。它可以被編碼成JSON格式的數(shù)據(jù),我們在后邊會大量使用JSON(事實上,在Vert.x中JSON非常普遍)。同時注意到我們給Todo類加上了一個注解:@DataObject,這是用于生成JSON轉(zhuǎn)換類的注解。
DataObject 注解
被 @DataObject 注解的實體類需要滿足以下條件:擁有一個拷貝構(gòu)造函數(shù)以及一個接受一個 JsonObject 對象的構(gòu)造函數(shù)。
我們利用Vert.x Codegen來自動生成JSON轉(zhuǎn)換類。我們需要在build.gradle中添加依賴:
compile "io.vertx:vertx-codegen:3.3.0"
同時,我們需要在io.vertx.blueprint.todolist.entity包中添加package-info.java文件來指引Vert.x Codegen生成代碼:
/** * Indicates that this module contains classes that need to be generated / processed. */ @ModuleGen(name = "vertx-blueprint-todo-entity", groupPackage = "io.vertx.blueprint.todolist.entity") package io.vertx.blueprint.todolist.entity; import io.vertx.codegen.annotations.ModuleGen;
Vert.x Codegen本質(zhì)上是一個注解處理器(annotation processing tool),因此我們還需要在build.gradle中配置apt。往里面添加以下代碼:
task annotationProcessing(type: JavaCompile, group: "build") { source = sourceSets.main.java classpath = configurations.compile destinationDir = project.file("src/main/generated") options.compilerArgs = [ "-proc:only", "-processor", "io.vertx.codegen.CodeGenProcessor", "-AoutputDirectory=${destinationDir.absolutePath}" ] } sourceSets { main { java { srcDirs += "src/main/generated" } } } compileJava { targetCompatibility = 1.8 sourceCompatibility = 1.8 dependsOn annotationProcessing }
這樣,每次我們在編譯項目的時候,Vert.x Codegen都會自動檢測含有 @DataObject 注解的類并且根據(jù)配置生成JSON轉(zhuǎn)換類。在本例中,我們應(yīng)該會得到一個 TodoConverter 類,然后我們可以在Todo類中使用它。
Verticle下面我們來寫我們的應(yīng)用組件。在io.vertx.blueprint.todolist.verticles包中創(chuàng)建SingleApplicationVerticle類,并編寫以下代碼:
package io.vertx.blueprint.todolist.verticles; import io.vertx.core.AbstractVerticle; import io.vertx.core.Future; import io.vertx.redis.RedisClient; import io.vertx.redis.RedisOptions; public class SingleApplicationVerticle extends AbstractVerticle { private static final String HTTP_HOST = "0.0.0.0"; private static final String REDIS_HOST = "127.0.0.1"; private static final int HTTP_PORT = 8082; private static final int REDIS_PORT = 6379; private RedisClient redis; @Override public void start(Futurefuture) throws Exception { // TODO with start... } }
我們的SingleApplicationVerticle類繼承了AbstractVerticle抽象類。那么什么是 Verticle 呢?在Vert.x中,一個Verticle代表應(yīng)用的某一組件。我們可以通過部署Verticle來運行這些組件。如果你了解 Actor 模型的話,你會發(fā)現(xiàn)它和Actor非常類似。
當Verticle被部署的時候,其start方法會被調(diào)用。我們注意到這里的start方法接受一個類型為Future
現(xiàn)在我們Verticle的輪廓已經(jīng)搞好了,那么下一步也就很明了了 - 創(chuàng)建HTTP Client并且配置路由,處理HTTP請求。
Vert.x Web與REST API 創(chuàng)建HTTP服務(wù)端并配置路由我們來給start方法加點東西:
@Override public void start(Futurefuture) throws Exception { initData(); Router router = Router.router(vertx); // <1> // CORS support Set allowHeaders = new HashSet<>(); allowHeaders.add("x-requested-with"); allowHeaders.add("Access-Control-Allow-Origin"); allowHeaders.add("origin"); allowHeaders.add("Content-Type"); allowHeaders.add("accept"); Set allowMethods = new HashSet<>(); allowMethods.add(HttpMethod.GET); allowMethods.add(HttpMethod.POST); allowMethods.add(HttpMethod.DELETE); allowMethods.add(HttpMethod.PATCH); router.route().handler(CorsHandler.create("*") // <2> .allowedHeaders(allowHeaders) .allowedMethods(allowMethods)); router.route().handler(BodyHandler.create()); // <3> // TODO:routes vertx.createHttpServer() // <4> .requestHandler(router::accept) .listen(PORT, HOST, result -> { if (result.succeeded()) future.complete(); else future.fail(result.cause()); }); }
(⊙o⊙)…一長串代碼誒。。是不是看著很暈?zāi)??我們來詳細解釋一下?/p>
首先我們創(chuàng)建了一個 Router 實例 (1)。這里的Router代表路由器,相信做過Web開發(fā)的開發(fā)者們一定不會陌生。路由器負責將對應(yīng)的HTTP請求分發(fā)至對應(yīng)的處理邏輯(Handler)中。每個Handler負責處理請求并且寫入回應(yīng)結(jié)果。當HTTP請求到達時,對應(yīng)的Handler會被調(diào)用。
然后我們創(chuàng)建了兩個Set:allowHeaders和allowMethods,并且我們向里面添加了一些HTTP Header以及HTTP Method,然后我們給路由器綁定了一個CorsHandler (2)。route()方法(無參數(shù))代表此路由匹配所有請求。這兩個Set的作用是支持 CORS,因為我們的API需要開啟CORS以便配合前端正常工作。有關(guān)CORS的詳細內(nèi)容我們就不在這里細說了,詳情可以參考這里。我們這里只需要知道如何開啟CORS支持即可。
接下來我們給路由器綁定了一個全局的BodyHandler (3),它的作用是處理HTTP請求正文并獲取其中的數(shù)據(jù)。比如,在實現(xiàn)添加待辦事項邏輯的時候,我們需要讀取請求正文中的JSON數(shù)據(jù),這時候我們就可以用BodyHandler。
最后,我們通過vertx.createHttpServer()方法來創(chuàng)建一個HTTP服務(wù)端 (4)。注意這個功能是Vert.x Core提供的底層功能之一。然后我們將我們的路由處理器綁定到服務(wù)端上,這也是Vert.x Web的核心。你可能不熟悉router::accept這樣的表示,這是Java 8中的 方法引用,它相當于一個分發(fā)路由的Handler。當有請求到達時,Vert.x會調(diào)用accept方法。然后我們通過listen方法監(jiān)聽8082端口。因為創(chuàng)建服務(wù)端的過程可能失敗,因此我們還需要給listen方法傳遞一個Handler來檢查服務(wù)端是否創(chuàng)建成功。正如我們前面所提到的,我們可以使用future.complete來表示過程成功,或者用future.fail來表示過程失敗。
到現(xiàn)在為止,我們已經(jīng)創(chuàng)建好HTTP服務(wù)端了,但我們還沒有見到任何的路由呢!不要著急,是時候去聲明路由了!
配置路由下面我們來聲明路由。正如我們之前提到的,我們的路由可以設(shè)計成這樣:
添加待辦事項: POST /todos
獲取某一待辦事項: GET /todos/:todoId
獲取所有待辦事項: GET /todos
更新待辦事項: PATCH /todos/:todoId
刪除某一待辦事項: DELETE /todos/:todoId
刪除所有待辦事項: DELETE /todos
路徑參數(shù)
在URL中,我們可以通過:name的形式定義路徑參數(shù)。當處理請求的時候,Vert.x會自動獲取這些路徑參數(shù)并允許我們訪問它們。拿我們的路由舉個例子,/todos/19 將 todoId 映射為 19。
首先我們先在 io.vertx.blueprint.todolist 包下創(chuàng)建一個Constants類用于存儲各種全局常量(當然也可以放到其對應(yīng)的類中):
package io.vertx.blueprint.todolist; public final class Constants { private Constants() {} /** API Route */ public static final String API_GET = "/todos/:todoId"; public static final String API_LIST_ALL = "/todos"; public static final String API_CREATE = "/todos"; public static final String API_UPDATE = "/todos/:todoId"; public static final String API_DELETE = "/todos/:todoId"; public static final String API_DELETE_ALL = "/todos"; }
然后我們將start方法中的TODO標識處替換為以下的內(nèi)容:
// routes router.get(Constants.API_GET).handler(this::handleGetTodo); router.get(Constants.API_LIST_ALL).handler(this::handleGetAll); router.post(Constants.API_CREATE).handler(this::handleCreateTodo); router.patch(Constants.API_UPDATE).handler(this::handleUpdateTodo); router.delete(Constants.API_DELETE).handler(this::handleDeleteOne); router.delete(Constants.API_DELETE_ALL).handler(this::handleDeleteAll);
代碼很直觀、明了。我們用對應(yīng)的方法(如get,post,patch等等)將路由路徑與路由器綁定,并且我們調(diào)用handler方法給每個路由綁定上對應(yīng)的Handler,接受的Handler類型為Handler
private void handleRequest(RoutingContext context) { // ... }
我們將在稍后實現(xiàn)這六個方法,這也是我們待辦事項服務(wù)邏輯的核心。
異步方法模式我們之前提到過,Vert.x是 異步、非阻塞的 。每一個異步的方法總會接受一個 Handler 參數(shù)作為回調(diào)函數(shù),當對應(yīng)的操作完成時會調(diào)用接受的Handler,這是異步方法的一種實現(xiàn)。還有一種等價的實現(xiàn)是返回Future對象:
void doAsync(A a, B b, Handlerhandler); // 這兩種實現(xiàn)等價 Future doAsync(A a, B b);
其中,Future 對象代表著一個操作的結(jié)果,這個操作可能還沒有進行,可能正在進行,可能成功也可能失敗。當操作完成時,Future對象會得到對應(yīng)的結(jié)果。我們也可以通過setHandler方法給Future綁定一個Handler,當Future被賦予結(jié)果的時候,此Handler會被調(diào)用。
Futurefuture = doAsync(A a, B b); future.setHandler(r -> { if (r.failed()) { // 處理失敗 } else { // 操作結(jié)果 } });
Vert.x中大多數(shù)異步方法都是基于Handler的。而在本教程中,這兩種異步模式我們都會接觸到。
待辦事項邏輯實現(xiàn)現(xiàn)在是時候來實現(xiàn)我們的待辦事項業(yè)務(wù)邏輯了!這里我們使用 Redis 作為數(shù)據(jù)持久化存儲。有關(guān)Redis的詳細介紹請參照Redis 官方網(wǎng)站。Vert.x給我們提供了一個組件—— Vert.x-redis,允許我們以異步的形式操作Redis數(shù)據(jù)。
Vert.x Redis如何安裝Redis? | 請參照Redis官方網(wǎng)站上詳細的安裝指南。
Vert.x Redis允許我們以異步的形式操作Redis數(shù)據(jù)。我們首先需要在build.gradle中添加以下依賴:
compile "io.vertx:vertx-redis-client:3.3.0"
我們通過RedisClient對象來操作Redis中的數(shù)據(jù),因此我們定義了一個類成員redis。在使用RedisClient之前,我們首先需要與Redis建立連接,并且需要配置(以RedisOptions的形式),后邊我們再講需要配置哪些東西。
我們來實現(xiàn) initData 方法用于初始化 RedisClient 并且測試連接:
private void initData() { RedisOptions config = new RedisOptions() .setHost(config().getString("redis.host", REDIS_HOST)) // redis host .setPort(config().getInteger("redis.port", REDIS_PORT)); // redis port this.redis = RedisClient.create(vertx, config); // create redis client redis.hset(Constants.REDIS_TODO_KEY, "24", Json.encodePrettily( // test connection new Todo(24, "Something to do...", false, 1, "todo/ex")), res -> { if (res.failed()) { System.err.println("[Error] Redis service is not running!"); res.cause().printStackTrace(); } }); }
當我們在加載Verticle的時候,我們會首先調(diào)用initData方法,這樣可以保證RedisClient可以被正常創(chuàng)建。
存儲格式我們知道,Redis支持各種格式的數(shù)據(jù),并且支持多種方式存儲(如list、hash map等)。這里我們將我們的待辦事項存儲在 哈希表(map) 中。我們使用待辦事項的id作為key,JSON格式的待辦事項數(shù)據(jù)作為value。同時,我們的哈希表本身也要有個key,我們把它命名為 VERT_TODO,并且存儲到Constants類中:
public static final String REDIS_TODO_KEY = "VERT_TODO";
正如我們之前提到的,我們利用了生成的JSON數(shù)據(jù)轉(zhuǎn)換類來實現(xiàn)Todo實體與JSON數(shù)據(jù)之間的轉(zhuǎn)換(通過幾個構(gòu)造函數(shù)),在后面實現(xiàn)待辦事項服務(wù)的時候可以廣泛利用。
獲取/獲取所有待辦事項我們首先來實現(xiàn)獲取待辦事項的邏輯。正如我們之前所提到的,我們的處理邏輯方法需要接受一個RoutingContext類型的參數(shù)。我們看一下獲取某一待辦事項的邏輯方法(handleGetTodo):
private void handleGetTodo(RoutingContext context) { String todoID = context.request().getParam("todoId"); // (1) if (todoID == null) sendError(400, context.response()); // (2) else { redis.hget(Constants.REDIS_TODO_KEY, todoID, x -> { // (3) if (x.succeeded()) { String result = x.result(); if (result == null) sendError(404, context.response()); else { context.response() .putHeader("content-type", "application/json") .end(result); // (4) } } else sendError(503, context.response()); }); } }
首先我們先通過getParam方法獲取路徑參數(shù)todoId (1)。我們需要檢測路徑參數(shù)獲取是否成功,如果不成功就返回 400 Bad Request 錯誤 (2)。這里我們寫一個函數(shù)封裝返回錯誤response的邏輯:
private void sendError(int statusCode, HttpServerResponse response) { response.setStatusCode(statusCode).end(); }
這里面,end方法是非常重要的。只有我們調(diào)用end方法時,對應(yīng)的HTTP Response才能被發(fā)送回客戶端。
再回到handleGetTodo方法中。如果我們成功獲取到了todoId,我們可以通過hget操作從Redis中獲取對應(yīng)的待辦事項 (3)。hget代表通過key從對應(yīng)的哈希表中獲取對應(yīng)的value,我們來看一下hget函數(shù)的定義:
RedisClient hget(String key, String field, Handler> handler);
第一個參數(shù)key對應(yīng)哈希表的key,第二個參數(shù)field代表待辦事項的key,第三個參數(shù)代表當獲取操作成功時對應(yīng)的回調(diào)。在Handler中,我們首先檢查操作是否成功,如果不成功就返回503錯誤。如果成功了,我們就可以獲取操作的結(jié)果了。結(jié)果是null的話,說明Redis中沒有對應(yīng)的待辦事項,因此我們返回404 Not Found代表不存在。如果結(jié)果存在,那么我們就可以通過end方法將其寫入response中 (4)。注意到我們所有的RESTful API都返回JSON格式的數(shù)據(jù),所以我們將content-type頭設(shè)為JSON。
獲取所有待辦事項的邏輯handleGetAll與handleGetTodo大體上類似,但實現(xiàn)上有些許不同:
private void handleGetAll(RoutingContext context) { redis.hvals(Constants.REDIS_TODO_KEY, res -> { // (1) if (res.succeeded()) { String encoded = Json.encodePrettily(res.result().stream() // (2) .map(x -> new Todo((String) x)) .collect(Collectors.toList())); context.response() .putHeader("content-type", "application/json") .end(encoded); // (3) } else sendError(503, context.response()); }); }
這里我們通過hvals操作 (1) 來獲取某個哈希表中的所有數(shù)據(jù)(以JSON數(shù)組的形式返回,即JsonArray對象)。在Handler中我們還是像之前那樣先檢查操作是否成功。如果成功的話我們就可以將結(jié)果寫入response了。注意這里我們不能直接將返回的JsonArray寫入response。想象一下返回的JsonArray包括著待辦事項的key以及對應(yīng)的JSON數(shù)據(jù)(字符串形式),因此此時每個待辦事項對應(yīng)的JSON數(shù)據(jù)都被轉(zhuǎn)義了,所以我們需要先把這些轉(zhuǎn)義過的JSON數(shù)據(jù)轉(zhuǎn)換成實體對象,再重新編碼。
我們這里采用了一種響應(yīng)式編程思想的方法。首先我們了解到JsonArray類繼承了Iterable接口(是不是感覺它很像List呢?),因此我們可以通過stream方法將其轉(zhuǎn)化為Stream對象。注意這里的Stream可不是傳統(tǒng)意義上講的輸入輸出流(I/O stream),而是數(shù)據(jù)流(data flow)。我們需要對數(shù)據(jù)流進行一系列的變換處理操作,這就是響應(yīng)式編程的思想(也有點函數(shù)式編程的思想)。我們將數(shù)據(jù)流中的每個字符串數(shù)據(jù)轉(zhuǎn)換為Todo實體對象,這個過程是通過map算子實現(xiàn)的。我們這里就不深入討論map算子了,但它在函數(shù)式編程中非常重要。在map過后,我們通過collect方法將數(shù)據(jù)流“歸約”成List
經(jīng)過了上面兩個業(yè)務(wù)邏輯實現(xiàn)的過程,你應(yīng)該開始熟悉Vert.x了~現(xiàn)在我們來實現(xiàn)創(chuàng)建待辦事項的邏輯:
private void handleCreateTodo(RoutingContext context) { try { final Todo todo = wrapObject(new Todo(context.getBodyAsString()), context); final String encoded = Json.encodePrettily(todo); redis.hset(Constants.REDIS_TODO_KEY, String.valueOf(todo.getId()), encoded, res -> { if (res.succeeded()) context.response() .setStatusCode(201) .putHeader("content-type", "application/json") .end(encoded); else sendError(503, context.response()); }); } catch (DecodeException e) { sendError(400, context.response()); } }
首先我們通過context.getBodyAsString()方法來從請求正文中獲取JSON數(shù)據(jù)并轉(zhuǎn)換成Todo實體對象 (1)。這里我們包裝了一個處理Todo實例的方法,用于給其添加必要的信息(如URL):
private Todo wrapObject(Todo todo, RoutingContext context) { int id = todo.getId(); if (id > Todo.getIncId()) { Todo.setIncIdWith(id); } else if (id == 0) todo.setIncId(); todo.setUrl(context.request().absoluteURI() + "/" + todo.getId()); return todo; }
對于沒有ID(或者為默認ID)的待辦事項,我們會給它分配一個ID。這里我們采用了自增ID的策略,通過AtomicInteger來實現(xiàn)。
然后我們通過Json.encodePrettily方法將我們的Todo實例再次編碼成JSON格式的數(shù)據(jù) (2)。接下來我們利用hset函數(shù)將待辦事項實例插入到對應(yīng)的哈希表中 (3)。如果插入成功,返回 201 狀態(tài)碼 (4)。
201 狀態(tài)碼?
| 正如你所看到的那樣,我們將狀態(tài)碼設(shè)為201,這代表CREATED(已創(chuàng)建)。另外,如果不指定狀態(tài)碼的話,Vert.x Web默認將狀態(tài)碼設(shè)為 200 OK。
同時,我們接收到的HTTP請求首部可能格式不正確,因此我們需要在方法中捕獲DecodeException異常。這樣一旦捕獲到DecodeException異常,我們就返回400 Bad Request狀態(tài)碼。
更新待辦事項如果你想改變你的計劃,你就需要更新你的待辦事項。我們來實現(xiàn)更新待辦事項的邏輯,它有點小復(fù)雜(或者說是,繁瑣?):
// PATCH /todos/:todoId private void handleUpdateTodo(RoutingContext context) { try { String todoID = context.request().getParam("todoId"); // (1) final Todo newTodo = new Todo(context.getBodyAsString()); // (2) // handle error if (todoID == null || newTodo == null) { sendError(400, context.response()); return; } redis.hget(Constants.REDIS_TODO_KEY, todoID, x -> { // (3) if (x.succeeded()) { String result = x.result(); if (result == null) sendError(404, context.response()); // (4) else { Todo oldTodo = new Todo(result); String response = Json.encodePrettily(oldTodo.merge(newTodo)); // (5) redis.hset(Constants.REDIS_TODO_KEY, todoID, response, res -> { // (6) if (res.succeeded()) { context.response() .putHeader("content-type", "application/json") .end(response); // (7) } }); } } else sendError(503, context.response()); }); } catch (DecodeException e) { sendError(400, context.response()); } }
唔。。。一大長串代碼誒。。。我們來看一下。首先我們從 RoutingContext 中獲取路徑參數(shù) todoId (1),這是我們想要更改待辦事項對應(yīng)的id。然后我們從請求正文中獲取新的待辦事項數(shù)據(jù) (2)。這一步也有可能拋出 DecodeException 異常因此我們也需要去捕獲它。要更新待辦事項,我們需要先通過hget函數(shù)獲取之前的待辦事項 (3),檢查其是否存在。獲取舊的待辦事項之后,我們調(diào)用之前在Todo類中實現(xiàn)的merge方法將舊待辦事項與新待辦事項整合到一起 (5),然后編碼成JSON格式的數(shù)據(jù)。然后我們通過hset函數(shù)更新對應(yīng)的待辦事項 (6)(hset表示如果不存在就插入,存在就更新)。操作成功的話,返回 200 OK 狀態(tài)。
這就是更新待辦事項的邏輯~要有耐心喲,我們馬上就要見到勝利的曙光了~下面我們來實現(xiàn)刪除待辦事項的邏輯。
刪除/刪除全部待辦事項刪除待辦事項的邏輯非常簡單。我們利用hdel函數(shù)來刪除某一待辦事項,用del函數(shù)刪掉所有待辦事項(實際上是直接把那個哈希表給刪了)。如果刪除操作成功,返回204 No Content 狀態(tài)。
這里直接給出代碼:
private void handleDeleteOne(RoutingContext context) { String todoID = context.request().getParam("todoId"); redis.hdel(Constants.REDIS_TODO_KEY, todoID, res -> { if (res.succeeded()) context.response().setStatusCode(204).end(); else sendError(503, context.response()); }); } private void handleDeleteAll(RoutingContext context) { redis.del(Constants.REDIS_TODO_KEY, res -> { if (res.succeeded()) context.response().setStatusCode(204).end(); else sendError(503, context.response()); }); }
啊哈!我們實現(xiàn)待辦事項服務(wù)的Verticle已經(jīng)完成咯~一顆賽艇!但是我們該如何去運行我們的Verticle呢?答案是,我們需要 部署并運行 我們的Verticle。還好Vert.x提供了一個運行Verticle的輔助工具:Vert.x Launcher,讓我們來看看如何利用它。
將應(yīng)用與Vert.x Launcher一起打包要通過Vert.x Launcher來運行Verticle,我們需要在build.gradle中配置一下:
jar { // by default fat jar archiveName = "vertx-blueprint-todo-backend-fat.jar" from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } manifest { attributes "Main-Class": "io.vertx.core.Launcher" attributes "Main-Verticle": "io.vertx.blueprint.todolist.verticles.SingleApplicationVerticle" } }
在jar區(qū)塊中,我們配置Gradle使其生成 fat-jar,并指定啟動類。fat-jar 是一個給Vert.x應(yīng)用打包的簡便方法,它直接將我們的應(yīng)用連同所有的依賴都給打包到j(luò)ar包中去了,這樣我們可以直接通過jar包運行我們的應(yīng)用而不必再指定依賴的 CLASSPATH
我們將Main-Class屬性設(shè)為io.vertx.core.Launcher,這樣就可以通過Vert.x Launcher來啟動對應(yīng)的Verticle了。另外我們需要將Main-Verticle屬性設(shè)為我們想要部署的Verticle的類名(全名)。
配置好了以后,我們就可以打包了:
gradle build運行我們的服務(wù)
萬事俱備,只欠東風。是時候運行我們的待辦事項服務(wù)了!首先我們先啟動Redis服務(wù):
redis-server
然后運行服務(wù):
java -jar build/libs/vertx-blueprint-todo-backend-fat.jar
如果沒問題的話,你將會在終端中看到 Succeeded in deploying verticle 的字樣。下面我們可以自由測試我們的API了,其中最簡便的方法是借助 todo-backend-js-spec 來測試。
鍵入 http://127.0.0.1:8082/todos,查看測試結(jié)果:
當然,我們也可以用其它工具,比如 curl :
sczyh30@sczyh30-workshop:~$ curl http://127.0.0.1:8082/todos [ { "id" : 20578623, "title" : "blah", "completed" : false, "order" : 95, "url" : "http://127.0.0.1:8082/todos/20578623" }, { "id" : 1744802607, "title" : "blah", "completed" : false, "order" : 523, "url" : "http://127.0.0.1:8082/todos/1744802607" }, { "id" : 981337975, "title" : "blah", "completed" : false, "order" : 95, "url" : "http://127.0.0.1:8082/todos/981337975" } ]將服務(wù)與控制器分離
啊哈~我們的待辦事項服務(wù)已經(jīng)可以正常運行了,但是回頭再來看看 SingleApplicationVerticle 類的代碼,你會發(fā)現(xiàn)它非常混亂,待辦事項業(yè)務(wù)邏輯與控制器混雜在一起,讓這個類非常的龐大,并且這也不利于我們服務(wù)的擴展。根據(jù)面向?qū)ο蠼怦畹乃枷?,我們需要將控制器部分與業(yè)務(wù)邏輯部分分離。
用Future實現(xiàn)異步服務(wù)下面我們來設(shè)計我們的業(yè)務(wù)邏輯層。就像我們之前提到的那樣,我們的服務(wù)需要是異步的,因此這些服務(wù)的方法要么需要接受一個Handler參數(shù)作為回調(diào),要么需要返回一個Future對象。但是想象一下很多個Handler混雜在一起嵌套的情況,你會陷入 回調(diào)地獄,這是非常糟糕的。因此,這里我們用Future實現(xiàn)我們的待辦事項服務(wù)。
在 io.vertx.blueprint.todolist.service 包下創(chuàng)建 TodoService 接口并且編寫以下代碼:
package io.vertx.blueprint.todolist.service; import io.vertx.blueprint.todolist.entity.Todo; import io.vertx.core.Future; import java.util.List; import java.util.Optional; public interface TodoService { FutureinitData(); // 初始化數(shù)據(jù)(或數(shù)據(jù)庫) Future insert(Todo todo); Future > getAll(); Future
> getCertain(String todoID); Future update(String todoId, Todo newTodo); Future delete(String todoId); Future deleteAll(); }
注意到getCertain方法返回一個Future
既然我們已經(jīng)設(shè)計好我們的異步服務(wù)接口了,讓我們來重構(gòu)原先的Verticle吧!
開始重構(gòu)!我們創(chuàng)建一個新的Verticle。在 io.vertx.blueprint.todolist.verticles 包中創(chuàng)建 TodoVerticle 類,并編寫以下代碼:
package io.vertx.blueprint.todolist.verticles; import io.vertx.blueprint.todolist.Constants; import io.vertx.blueprint.todolist.entity.Todo; import io.vertx.blueprint.todolist.service.TodoService; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.DecodeException; import io.vertx.core.json.Json; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.CorsHandler; import java.util.HashSet; import java.util.Random; import java.util.Set; import java.util.function.Consumer; public class TodoVerticle extends AbstractVerticle { private static final String HOST = "0.0.0.0"; private static final int PORT = 8082; private TodoService service; private void initData() { // TODO } @Override public void start(Futurefuture) throws Exception { Router router = Router.router(vertx); // CORS support Set allowHeaders = new HashSet<>(); allowHeaders.add("x-requested-with"); allowHeaders.add("Access-Control-Allow-Origin"); allowHeaders.add("origin"); allowHeaders.add("Content-Type"); allowHeaders.add("accept"); Set allowMethods = new HashSet<>(); allowMethods.add(HttpMethod.GET); allowMethods.add(HttpMethod.POST); allowMethods.add(HttpMethod.DELETE); allowMethods.add(HttpMethod.PATCH); router.route().handler(BodyHandler.create()); router.route().handler(CorsHandler.create("*") .allowedHeaders(allowHeaders) .allowedMethods(allowMethods)); // routes router.get(Constants.API_GET).handler(this::handleGetTodo); router.get(Constants.API_LIST_ALL).handler(this::handleGetAll); router.post(Constants.API_CREATE).handler(this::handleCreateTodo); router.patch(Constants.API_UPDATE).handler(this::handleUpdateTodo); router.delete(Constants.API_DELETE).handler(this::handleDeleteOne); router.delete(Constants.API_DELETE_ALL).handler(this::handleDeleteAll); vertx.createHttpServer() .requestHandler(router::accept) .listen(PORT, HOST, result -> { if (result.succeeded()) future.complete(); else future.fail(result.cause()); }); initData(); } private void handleCreateTodo(RoutingContext context) { // TODO } private void handleGetTodo(RoutingContext context) { // TODO } private void handleGetAll(RoutingContext context) { // TODO } private void handleUpdateTodo(RoutingContext context) { // TODO } private void handleDeleteOne(RoutingContext context) { // TODO } private void handleDeleteAll(RoutingContext context) { // TODO } private void sendError(int statusCode, HttpServerResponse response) { response.setStatusCode(statusCode).end(); } private void badRequest(RoutingContext context) { context.response().setStatusCode(400).end(); } private void notFound(RoutingContext context) { context.response().setStatusCode(404).end(); } private void serviceUnavailable(RoutingContext context) { context.response().setStatusCode(503).end(); } private Todo wrapObject(Todo todo, RoutingContext context) { int id = todo.getId(); if (id > Todo.getIncId()) { Todo.setIncIdWith(id); } else if (id == 0) todo.setIncId(); todo.setUrl(context.request().absoluteURI() + "/" + todo.getId()); return todo; } }
很熟悉吧?這個Verticle的結(jié)構(gòu)與我們之前的Verticle相類似,這里就不多說了。下面我們來利用我們之前編寫的服務(wù)接口實現(xiàn)每一個控制器方法。
首先先實現(xiàn) initData 方法,此方法用于初始化存儲結(jié)構(gòu):
private void initData() { final String serviceType = config().getString("service.type", "redis"); switch (serviceType) { case "jdbc": service = new JdbcTodoService(vertx, config()); break; case "redis": default: RedisOptions config = new RedisOptions() .setHost(config().getString("redis.host", "127.0.0.1")) .setPort(config().getInteger("redis.port", 6379)); service = new RedisTodoService(vertx, config); } service.initData().setHandler(res -> { if (res.failed()) { System.err.println("[Error] Persistence service is not running!"); res.cause().printStackTrace(); } }); }
首先我們從配置中獲取服務(wù)的類型,這里我們有兩種類型的服務(wù):redis和jdbc,默認是redis。接著我們會根據(jù)服務(wù)的類型以及對應(yīng)的配置來創(chuàng)建服務(wù)。在這里,我們的配置都是從JSON格式的配置文件中讀取,并通過Vert.x Launcher的-conf項加載。后面我們再講要配置哪些東西。
接著我們給service.initData()方法返回的Future對象綁定了一個Handler,這個Handler將會在Future得到結(jié)果的時候被調(diào)用。一旦初始化過程失敗,錯誤信息將會顯示到終端上。
其它的方法實現(xiàn)也類似,這里就不詳細解釋了,直接放上代碼,非常簡潔明了:
/** * Wrap the result handler with failure handler (503 Service Unavailable) */ privateHandler > resultHandler(RoutingContext context, Consumer consumer) { return res -> { if (res.succeeded()) { consumer.accept(res.result()); } else { serviceUnavailable(context); } }; } private void handleCreateTodo(RoutingContext context) { try { final Todo todo = wrapObject(new Todo(context.getBodyAsString()), context); final String encoded = Json.encodePrettily(todo); service.insert(todo).setHandler(resultHandler(context, res -> { if (res) { context.response() .setStatusCode(201) .putHeader("content-type", "application/json") .end(encoded); } else { serviceUnavailable(context); } })); } catch (DecodeException e) { sendError(400, context.response()); } } private void handleGetTodo(RoutingContext context) { String todoID = context.request().getParam("todoId"); if (todoID == null) { sendError(400, context.response()); return; } service.getCertain(todoID).setHandler(resultHandler(context, res -> { if (!res.isPresent()) notFound(context); else { final String encoded = Json.encodePrettily(res.get()); context.response() .putHeader("content-type", "application/json") .end(encoded); } })); } private void handleGetAll(RoutingContext context) { service.getAll().setHandler(resultHandler(context, res -> { if (res == null) { serviceUnavailable(context); } else { final String encoded = Json.encodePrettily(res); context.response() .putHeader("content-type", "application/json") .end(encoded); } })); } private void handleUpdateTodo(RoutingContext context) { try { String todoID = context.request().getParam("todoId"); final Todo newTodo = new Todo(context.getBodyAsString()); // handle error if (todoID == null) { sendError(400, context.response()); return; } service.update(todoID, newTodo) .setHandler(resultHandler(context, res -> { if (res == null) notFound(context); else { final String encoded = Json.encodePrettily(res); context.response() .putHeader("content-type", "application/json") .end(encoded); } })); } catch (DecodeException e) { badRequest(context); } } private Handler > deleteResultHandler(RoutingContext context) { return res -> { if (res.succeeded()) { if (res.result()) { context.response().setStatusCode(204).end(); } else { serviceUnavailable(context); } } else { serviceUnavailable(context); } }; } private void handleDeleteOne(RoutingContext context) { String todoID = context.request().getParam("todoId"); service.delete(todoID) .setHandler(deleteResultHandler(context)); } private void handleDeleteAll(RoutingContext context) { service.deleteAll() .setHandler(deleteResultHandler(context)); }
是不是和之前的Verticle很相似呢?這里我們還封裝了兩個Handler生成器:resultHandler 和 deleteResultHandler。這兩個生成器封裝了一些重復(fù)的代碼,可以減少代碼量。
嗯。。。我們的新Verticle寫好了,那么是時候去實現(xiàn)具體的業(yè)務(wù)邏輯了。這里我們會實現(xiàn)兩個版本的業(yè)務(wù)邏輯,分別對應(yīng)兩種存儲:Redis 和 MySQL。
Vert.x-Redis版本的待辦事項服務(wù)之前我們已經(jīng)實現(xiàn)過一遍Redis版本的服務(wù)了,因此你應(yīng)該對其非常熟悉了。這里我們僅僅解釋一個 update 方法,其它的實現(xiàn)都非常類似,代碼可以在GitHub上瀏覽。
Monadic Future回想一下我們之前寫的更新待辦事項的邏輯,我們會發(fā)現(xiàn)它其實是由兩個獨立的操作組成 - get 和 insert(對于Redis來說)。所以呢,我們可不可以復(fù)用 getCertain 和 insert 這兩個方法?當然了!因為Future是可組合的,因此我們可以將這兩個方法返回的Future組合到一起。是不是非常方便呢?我們來編寫此方法:
@Override public Futureupdate(String todoId, Todo newTodo) { return this.getCertain(todoId).compose(old -> { // (1) if (old.isPresent()) { Todo fnTodo = old.get().merge(newTodo); return this.insert(fnTodo) .map(r -> r ? fnTodo : null); // (2) } else { return Future.succeededFuture(); // (3) } }); }
首先我們調(diào)用了getCertain方法,此方法返回一個Future
Future 的本質(zhì)
在函數(shù)式編程中,Future 實際上是一種 Monad。有關(guān)Monad的理論較為復(fù)雜,這里就不進行闡述了。你可以簡單地把它看作是一個可以進行變換(map)和組合(compose)的包裝對象。我們把這種特性叫做 Monadic。
下面來實現(xiàn)MySQL版本的待辦事項服務(wù)。
Vert.x-JDBC版本的待辦事項服務(wù) JDBC ++ 異步我們使用Vert.x-JDBC和MySQL來實現(xiàn)JDBC版本的待辦事項服務(wù)。我們知道,數(shù)據(jù)庫操作都是阻塞操作,很可能會占用不少時間。而Vert.x-JDBC提供了一種異步操作數(shù)據(jù)庫的模式,很神奇吧?所以,在傳統(tǒng)JDBC代碼下我們要執(zhí)行SQL語句需要這樣:
String SQL = "SELECT * FROM todo"; // ... ResultSet rs = pstmt.executeQuery(SQL);
而在Vert.x JDBC中,我們可以利用回調(diào)獲取數(shù)據(jù):
connection.query(SQL, result -> { // do something with result... });
這種異步操作可以有效避免對數(shù)據(jù)的等待。當數(shù)據(jù)獲取成功時會自動調(diào)用回調(diào)函數(shù)來執(zhí)行處理數(shù)據(jù)的邏輯。
添加依賴首先我們需要向build.gradle文件中添加依賴:
compile "io.vertx:vertx-jdbc-client:3.3.0" compile "mysql:mysql-connector-java:6.0.2"
其中第二個依賴是MySQL的驅(qū)動,如果你想使用其他的數(shù)據(jù)庫,你需要自行替換掉這個依賴。
初始化JDBCClient在Vert.x JDBC中,我們需要從一個JDBCClient對象中獲取數(shù)據(jù)庫連接,因此我們來看一下如何創(chuàng)建JDBCClient實例。在io.vertx.blueprint.todolist.service包下創(chuàng)建JdbcTodoService類:
package io.vertx.blueprint.todolist.service; import io.vertx.blueprint.todolist.entity.Todo; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.jdbc.JDBCClient; import io.vertx.ext.sql.SQLConnection; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; public class JdbcTodoService implements TodoService { private final Vertx vertx; private final JsonObject config; private final JDBCClient client; public JdbcTodoService(JsonObject config) { this(Vertx.vertx(), config); } public JdbcTodoService(Vertx vertx, JsonObject config) { this.vertx = vertx; this.config = config; this.client = JDBCClient.createShared(vertx, config); } // ... }
我們使用JDBCClient.createShared(vertx, config)方法來創(chuàng)建一個JDBCClient實例,其中我們傳入一個JsonObject對象作為配置。一般來說,我們需要配置以下的內(nèi)容:
url - JDBC URL,比如 jdbc:mysql://localhost/vertx_blueprint
driver_class - JDBC驅(qū)動名稱,比如 com.mysql.cj.jdbc.Driver
user - 數(shù)據(jù)庫用戶
password - 數(shù)據(jù)庫密碼
我們將會通過Vert.x Launcher從配置文件中讀取此JsonObject。
現(xiàn)在我們已經(jīng)創(chuàng)建了JDBCClient實例了,下面我們需要在MySQL中建這樣一個表:
CREATE TABLE `todo` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) DEFAULT NULL, `completed` TINYINT(1) DEFAULT NULL, `order` INT(11) DEFAULT NULL, `url` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`) )
我們把要用到的數(shù)據(jù)庫語句都存到服務(wù)類中(這里我們就不討論如何設(shè)計表以及寫SQL了):
private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS `todo` ( " + " `id` int(11) NOT NULL AUTO_INCREMENT, " + " `title` varchar(255) DEFAULT NULL, " + " `completed` tinyint(1) DEFAULT NULL, " + " `order` int(11) DEFAULT NULL, " + " `url` varchar(255) DEFAULT NULL, " + " PRIMARY KEY (`id`) )"; private static final String SQL_INSERT = "INSERT INTO `todo` " + "(`id`, `title`, `completed`, `order`, `url`) VALUES (?, ?, ?, ?, ?)"; private static final String SQL_QUERY = "SELECT * FROM todo WHERE id = ?"; private static final String SQL_QUERY_ALL = "SELECT * FROM todo"; private static final String SQL_UPDATE = "UPDATE `todo` " + "SET `id` = ?, " + "`title` = ?, " + "`completed` = ?, " + "`order` = ?, " + "`url` = ? " + "WHERE `id` = ?;"; private static final String SQL_DELETE = "DELETE FROM `todo` WHERE `id` = ?"; private static final String SQL_DELETE_ALL = "DELETE FROM `todo`";
OK!一切工作準備就緒,下面我們來實現(xiàn)我們的JDBC版本的服務(wù)~
實現(xiàn)JDBC版本的服務(wù)所有的獲取連接、獲取執(zhí)行數(shù)據(jù)的操作都要在Handler中完成。比如我們可以這樣獲取數(shù)據(jù)庫連接:
client.getConnection(conn -> { if (conn.succeeded()) { final SQLConnection connection = conn.result(); // do something... } else { // handle failure } });
由于每一個數(shù)據(jù)庫操作都需要獲取數(shù)據(jù)庫連接,因此我們來包裝一個返回Handler
private Handler> connHandler(Future future, Handler handler) { return conn -> { if (conn.succeeded()) { final SQLConnection connection = conn.result(); handler.handle(connection); } else { future.fail(conn.cause()); } }; }
獲取數(shù)據(jù)庫連接以后,我們就可以對數(shù)據(jù)庫進行各種操作了:
query : 執(zhí)行查詢(raw SQL)
queryWithParams : 執(zhí)行預(yù)編譯查詢(prepared statement)
updateWithParams : 執(zhí)行預(yù)編譯DDL語句(prepared statement)
execute: 執(zhí)行任意SQL語句
所有的方法都是異步的所以每個方法最后都接受一個Handler參數(shù),我們可以在此Handler中獲取結(jié)果并執(zhí)行相應(yīng)邏輯。
現(xiàn)在我們來編寫初始化數(shù)據(jù)庫表的initData方法:
@Override public FutureinitData() { Future result = Future.future(); client.getConnection(connHandler(result, connection -> connection.execute(SQL_CREATE, create -> { if (create.succeeded()) { result.complete(true); } else { result.fail(create.cause()); } connection.close(); }))); return result; }
此方法僅會在Verticle初始化時被調(diào)用,如果todo表不存在的話就創(chuàng)建一下。注意,最后一定要關(guān)閉數(shù)據(jù)庫連接。
下面我們來實現(xiàn)插入邏輯方法:
@Override public Futureinsert(Todo todo) { Future result = Future.future(); client.getConnection(connHandler(result, connection -> { connection.updateWithParams(SQL_INSERT, new JsonArray().add(todo.getId()) .add(todo.getTitle()) .add(todo.isCompleted()) .add(todo.getOrder()) .add(todo.getUrl()), r -> { if (r.failed()) { result.fail(r.cause()); } else { result.complete(true); } connection.close(); }); })); return result; }
我們使用updateWithParams方法執(zhí)行插入邏輯,并且傳遞了一個JsonArray變量作為預(yù)編譯參數(shù)。這一點很重要,使用預(yù)編譯語句可以有效防止SQL注入。
我們再來實現(xiàn)getCertain方法:
@Override public Future> getCertain(String todoID) { Future > result = Future.future(); client.getConnection(connHandler(result, connection -> { connection.queryWithParams(SQL_QUERY, new JsonArray().add(todoID), r -> { if (r.failed()) { result.fail(r.cause()); } else { List list = r.result().getRows(); if (list == null || list.isEmpty()) { result.complete(Optional.empty()); } else { result.complete(Optional.of(new Todo(list.get(0)))); } } connection.close(); }); })); return result; }
在這個方法里,當我們的查詢語句執(zhí)行以后,我們獲得到了ResultSet實例作為查詢的結(jié)果集。我們可以通過getColumnNames方法獲取字段名稱,通過getResults方法獲取結(jié)果。這里我們通過getRows方法來獲取結(jié)果集,結(jié)果集的類型為List
其余的幾個方法:getAll, update, delete 以及 deleteAll都遵循上面的模式,這里就不多說了。你可以在GitHub上瀏覽完整的源代碼。
重構(gòu)完畢,我們來寫待辦事項服務(wù)對應(yīng)的配置,然后再來運行!
再來運行!首先我們在項目的根目錄下創(chuàng)建一個 config 文件夾作為配置文件夾。我們在其中創(chuàng)建一個config_jdbc.json文件作為 jdbc 類型服務(wù)的配置:
{ "service.type": "jdbc", "url": "jdbc:mysql://localhost/vertx_blueprint?characterEncoding=UTF-8&useSSL=false", "driver_class": "com.mysql.cj.jdbc.Driver", "user": "vbpdb1", "password": "666666*", "max_pool_size": 30 }
你需要根據(jù)自己的情況替換掉上述配置文件中相應(yīng)的內(nèi)容(如 JDBC URL,JDBC 驅(qū)動 等)。
再建一個config.json文件作為redis類型服務(wù)的配置(其它的項就用默認配置好啦):
{ "service.type": "redis" }
我們的構(gòu)建文件也需要更新咯~這里直接給出最終的build.gradle文件:
plugins { id "java" } version "1.0" ext { vertxVersion = "3.3.0" } jar { // by default fat jar archiveName = "vertx-blueprint-todo-backend-fat.jar" from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } manifest { attributes "Main-Class": "io.vertx.core.Launcher" attributes "Main-Verticle": "io.vertx.blueprint.todolist.verticles.TodoVerticle" } } repositories { jcenter() mavenCentral() mavenLocal() } task annotationProcessing(type: JavaCompile, group: "build") { source = sourceSets.main.java classpath = configurations.compile destinationDir = project.file("src/main/generated") options.compilerArgs = [ "-proc:only", "-processor", "io.vertx.codegen.CodeGenProcessor", "-AoutputDirectory=${destinationDir.absolutePath}" ] } sourceSets { main { java { srcDirs += "src/main/generated" } } } compileJava { targetCompatibility = 1.8 sourceCompatibility = 1.8 dependsOn annotationProcessing } dependencies { compile ("io.vertx:vertx-core:${vertxVersion}") compile ("io.vertx:vertx-web:${vertxVersion}") compile ("io.vertx:vertx-jdbc-client:${vertxVersion}") compile ("io.vertx:vertx-redis-client:${vertxVersion}") compile ("io.vertx:vertx-codegen:${vertxVersion}") compile "mysql:mysql-connector-java:6.0.2" testCompile ("io.vertx:vertx-unit:${vertxVersion}") testCompile group: "junit", name: "junit", version: "4.12" } task wrapper(type: Wrapper) { gradleVersion = "2.12" }
好啦好啦,迫不及待了吧?~打開終端,構(gòu)建我們的應(yīng)用:
gradle build
然后我們可以運行Redis版本的待辦事項服務(wù):
java -jar build/libs/vertx-blueprint-todo-backend-fat.jar -conf config/config.json
我們也可以運行JDBC版本的待辦事項服務(wù):
java -jar build/libs/vertx-blueprint-todo-backend-fat.jar -conf config/config_jdbc.json
同樣地,我們也可以使用todo-backend-js-spec來測試我們的API。由于我們的API設(shè)計沒有改變,因此測試結(jié)果應(yīng)該不會有變化。
我們也提供了待辦事項服務(wù)對應(yīng)的Docker Compose鏡像構(gòu)建文件,可以直接通過Docker來運行我們的待辦事項服務(wù)。你可以在倉庫的根目錄下看到相應(yīng)的配置文件,并通過 docker-compose up -- build 命令來構(gòu)建并運行。
哈哈,成功了!哈哈,恭喜你完成了整個待辦事項服務(wù),是不是很開心?~在整個教程中,你應(yīng)該學到了很多關(guān)于 Vert.x Web、 Vert.x Redis 和 Vert.x JDBC 的開發(fā)知識。當然,最重要的是,你會對Vert.x的 異步開發(fā)模式 有了更深的理解和領(lǐng)悟。
更多關(guān)于Vert.x的文章,請參考Blog on Vert.x Website。官網(wǎng)的資料是最全面的 :-)
來自其它框架?之前你可能用過其它的框架,比如Spring Boot。這一小節(jié),我將會用類比的方式來介紹Vert.x Web的使用。
來自Spring Boot/Spring MVC在Spring Boot中,我們通常在控制器(Controller)中來配置路由以及處理請求,比如:
@RestController @ComponentScan @EnableAutoConfiguration public class TodoController { @Autowired private TodoService service; @RequestMapping(method = RequestMethod.GET, value = "/todos/{id}") public Todo getCertain(@PathVariable("id") int id) { return service.fetch(id); } }
在Spring Boot中,我們使用 @RequestMapping 注解來配置路由,而在Vert.x Web中,我們是通過 Router 對象來配置路由的。并且因為Vert.x Web是異步的,我們會給每個路由綁定一個處理器(Handler)來處理對應(yīng)的請求。
另外,在Vert.x Web中,我們使用 end 方法來向客戶端發(fā)送HTTP response。相對地,在Spring Boot中我們直接在每個方法中返回結(jié)果作為response。
來自Play Framework 2如果之前用過Play Framework 2的話,你一定會非常熟悉異步開發(fā)模式。在Play Framework 2中,我們在 routes 文件中定義路由,類似于這樣:
GET /todos/:todoId controllers.TodoController.handleGetCertain(todoId: Int)
而在Vert.x Web中,我們通過Router對象來配置路由:
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/64828.html
摘要:上部分藍圖教程中我們一起探索了如何用開發(fā)一個基于消息的應(yīng)用。對部分來說,如果看過我們之前的藍圖待辦事項服務(wù)開發(fā)教程的話,你應(yīng)該對這一部分非常熟悉了,因此這里我們就不詳細解釋了。有關(guān)使用實現(xiàn)的教程可參考藍圖待辦事項服務(wù)開發(fā)教程。 上部分藍圖教程中我們一起探索了如何用Vert.x開發(fā)一個基于消息的應(yīng)用。在這部分教程中,我們將粗略地探索一下kue-http模塊的實現(xiàn)。 Vert.x Kue ...
摘要:本文章是藍圖系列的第二篇教程。這就是請求回應(yīng)模式。好多屬性我們一個一個地解釋一個序列,作為的地址任務(wù)的編號任務(wù)的類型任務(wù)攜帶的數(shù)據(jù),以類型表示任務(wù)優(yōu)先級,以枚舉類型表示。默認優(yōu)先級為正常任務(wù)的延遲時間,默認是任務(wù)狀態(tài),以枚舉類型表示。 本文章是 Vert.x 藍圖系列 的第二篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待辦事項服務(wù)開發(fā)教程 Vert.x B...
摘要:本教程是藍圖系列的第三篇教程,對應(yīng)的版本為。提供了一個服務(wù)發(fā)現(xiàn)模塊用于發(fā)布和獲取服務(wù)記錄。前端此微服務(wù)的前端部分,目前已整合至組件中。監(jiān)視儀表板用于監(jiān)視微服務(wù)系統(tǒng)的狀態(tài)以及日志統(tǒng)計數(shù)據(jù)的查看。而服務(wù)則負責發(fā)布其它服務(wù)如服務(wù)或消息源并且部署。 本文章是 Vert.x 藍圖系列 的第三篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待辦事項服務(wù)開發(fā)教程 Vert....
摘要:現(xiàn)在我們來給待辦事項增加一個緊急程度的字段,用來表示當前任務(wù)的優(yōu)先級。此處我們還給這個字段添加了默認值,表示當一個待辦事項被創(chuàng)建后,如果沒有指定緊急程度,將默認是待辦狀態(tài)。這篇教程中的代碼同樣會更新在我的倉庫中。 showImg(https://segmentfault.com/img/bVbfv3E?w=1330&h=912); 在這篇教程里我們將會了解到 Odoo 模型里的一些其他...
摘要:在這一篇教程中,將會涉及到外鍵字段,可以將兩個模型關(guān)聯(lián)起來,然后很方便地獲取到對應(yīng)的數(shù)據(jù)。關(guān)聯(lián)字段這一小節(jié)里,我們會給待辦事項加上分類,并且這個分類可以讓用戶自己創(chuàng)建維護。今天這篇教程的內(nèi)容就先到這里了,教程中的代碼會更新在我的倉庫中。 showImg(https://segmentfault.com/img/bVbfzvt?w=1280&h=795); 在這一篇教程中,將會涉及到外鍵...
閱讀 1734·2021-10-18 13:30
閱讀 2608·2021-10-09 10:02
閱讀 2965·2021-09-28 09:35
閱讀 2091·2019-08-26 13:39
閱讀 3522·2019-08-26 13:36
閱讀 1950·2019-08-26 11:46
閱讀 1135·2019-08-23 14:56
閱讀 1694·2019-08-23 10:38