Vert.x Blueprint 系列教程(一) | 待辦事項服務開發教程

本文章是 Vert.x 藍圖系列 的第一篇教程。全系列:html

本系列已發佈至Vert.x官網:Vert.x Blueprint Tutorialsreact


前言

在本教程中,咱們會使用Vert.x來一步一步地開發一個REST風格的Web服務 - Todo Backend,你能夠把它看做是一個簡單的待辦事項服務,咱們能夠自由添加或者取消各類待辦事項。git

經過本教程,你將會學習到如下的內容:github

  • Vert.x 是什麼,以及其基本設計思想web

  • Verticle是什麼,以及如何使用Verticleredis

  • 如何用 Vert.x Web 來開發REST風格的Web服務sql

  • 異步編程風格 的應用

  • 如何經過 Vert.x 的各類組件來進行數據的存儲操做(如 RedisMySQL

本教程是Vert.x 藍圖系列的第一篇教程,對應的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上構建 響應式 應用的 工具集 。這個定義比較模糊,咱們來簡單解釋一下:工具集 意味着Vert.x很是輕量,能夠嵌入到你當前的應用中而不須要改變現有的結構;另外一個重要的描述是 響應式 —— Vert.x就是爲構建響應式應用(系統)而設計的。響應式系統這個概念在 Reactive Manifesto 中有詳細的定義。咱們在這裏總結4個要點:

  • 響應式的(Responsive):一個響應式系統須要在 合理 的時間內處理請求。

  • 彈性的(Resilient):一個響應式系統必須在遇到 異常 (崩潰,超時, 500 錯誤等等)的時候保持響應的能力,因此它必需要爲 異常處理 而設計。

  • 可伸縮的(Elastic):一個響應式系統必須在不一樣的負載狀況下都要保持響應能力,因此它必須能伸能縮,而且能夠利用最少的資源來處理負載。

  • 消息驅動:一個響應式系統的各個組件之間經過 異步消息傳遞 來進行交互。

Vert.x是事件驅動的,同時也是非阻塞的。首先,咱們來介紹 Event Loop 的概念。Event Loop是一組負責分發和處理事件的線程。注意,咱們絕對不能去阻塞Event Loop線程,不然事件的處理過程會被阻塞,咱們的應用就失去了響應能力。所以當咱們在寫Vert.x應用的時候,咱們要時刻謹記 異步非阻塞開發模式 而不是傳統的阻塞開發模式。咱們將會在下面詳細講解異步非阻塞開發模式。

咱們的應用 - 待辦事項服務

咱們的應用是一個REST風格的待辦事項服務,它很是簡單,整個API其實就圍繞着 增刪改查 四種操做。因此咱們能夠設計如下的路由:

  • 添加待辦事項: POST /todos

  • 獲取某一待辦事項: GET /todos/:todoId

  • 獲取全部待辦事項: GET /todos

  • 更新待辦事項: PATCH /todos/:todoId

  • 刪除某一待辦事項: DELETE /todos/:todoId

  • 刪除全部待辦事項: DELETE /todos

注意咱們這裏不討論REST風格API的設計規範(仁者見仁,智者見智),所以你也能夠用你喜歡的方式去定義路由。

下面咱們開始開發咱們的項目!High起來~~~

說幹就幹!

Vert.x Core提供了一些較爲底層的處理HTTP請求的功能,這對於Web開發來講不是很方便,由於咱們一般不須要這麼底層的功能,所以Vert.x Web應運而生。Vert.x Web基於Vert.x Core,而且提供一組更易於建立Web應用的上層功能(如路由)。

Gradle配置文件

首先咱們先來建立咱們的項目。在本教程中咱們使用Gradle做爲構建工具,固然你也可使用其它諸如Maven之類的構建工具。咱們的項目目錄裏須要有:

  1. src/main/java 文件夾(源碼目錄)

  2. src/test/java 文件夾(測試目錄)

  3. build.gradle 文件(Gradle配置文件)

.
├── build.gradle
├── settings.gradle
├── src
│   ├── main
│   │   └── java
│   └── test
│       └── java

咱們首先來建立 build.gradle 文件,這是Gradle對應的配置文件:

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,這沒關係。咱們來解釋一下:

  • 咱們將 targetCompatibilitysourceCompatibility 這兩個值都設爲1.8,表明目標Java版本是Java 8。這很是重要,由於Vert.x就是基於Java 8構建的。

  • dependencies中,咱們聲明瞭咱們須要的依賴。vertx-corevert-web 用於開發REST API。

注: 若國內用戶出現用Gradle解析依賴很是緩慢的狀況,能夠嘗試使用開源中國Maven鏡像代替默認的鏡像(有的時候速度比較快)。只要在build.gradle中配置便可:

repositories {
    maven {
            url 'http://maven.oschina.net/content/groups/public/'
        }
    mavenLocal()
}

搞定build.gradle之後,咱們開始寫代碼!

待辦事項對象

首先咱們須要建立咱們的數據實體對象 - Todo 實體。在io.vertx.blueprint.todolist.entity包下建立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 + '\'' +
      '}';
  }

  private <T> T 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格式的數據,咱們在後邊會大量使用JSON(事實上,在Vert.x中JSON很是廣泛)。同時注意到咱們給Todo類加上了一個註解:@DataObject,這是用於生成JSON轉換類的註解。

DataObject 註解
@DataObject 註解的實體類須要知足如下條件:擁有一個拷貝構造函數以及一個接受一個 JsonObject 對象的構造函數。

咱們利用Vert.x Codegen來自動生成JSON轉換類。咱們須要在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本質上是一個註解處理器(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 註解的類而且根據配置生成JSON轉換類。在本例中,咱們應該會獲得一個 TodoConverter 類,而後咱們能夠在Todo類中使用它。

Verticle

下面咱們來寫咱們的應用組件。在io.vertx.blueprint.todolist.verticles包中建立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(Future<Void> future) throws Exception {
      // TODO with start...
  }
}

咱們的SingleApplicationVerticle類繼承了AbstractVerticle抽象類。那麼什麼是 Verticle 呢?在Vert.x中,一個Verticle表明應用的某一組件。咱們能夠經過部署Verticle來運行這些組件。若是你瞭解 Actor 模型的話,你會發現它和Actor很是相似。

Verticle被部署的時候,其start方法會被調用。咱們注意到這裏的start方法接受一個類型爲Future<Void>的參數,這表明了這是一個異步的初始化方法。這裏的Future表明着Verticle的初始化過程是否完成。你能夠經過調用Future的complete方法來表明初始化過程完成,或者fail方法表明初始化過程失敗。

如今咱們Verticle的輪廓已經搞好了,那麼下一步也就很明瞭了 - 建立HTTP Client而且配置路由,處理HTTP請求。

Vert.x Web與REST API

建立HTTP服務端並配置路由

咱們來給start方法加點東西:

@Override
public void start(Future<Void> future) throws Exception {
  initData();
  Router router = Router.router(vertx); // <1>
  // CORS support
  Set<String> 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<HttpMethod> 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⊙)…一長串代碼誒。。是否是看着很暈呢?咱們來詳細解釋一下。

首先咱們建立了一個 Router 實例 (1)。這裏的Router表明路由器,相信作過Web開發的開發者們必定不會陌生。路由器負責將對應的HTTP請求分發至對應的處理邏輯(Handler)中。每一個Handler負責處理請求而且寫入迴應結果。當HTTP請求到達時,對應的Handler會被調用。

而後咱們建立了兩個SetallowHeadersallowMethods,而且咱們向裏面添加了一些HTTP Header以及HTTP Method,而後咱們給路由器綁定了一個CorsHandler (2)。route()方法(無參數)表明此路由匹配全部請求。這兩個Set的做用是支持 CORS,由於咱們的API須要開啓CORS以便配合前端正常工做。有關CORS的詳細內容咱們就不在這裏細說了,詳情能夠參考這裏。咱們這裏只須要知道如何開啓CORS支持便可。

接下來咱們給路由器綁定了一個全局的BodyHandler (3),它的做用是處理HTTP請求正文並獲取其中的數據。好比,在實現添加待辦事項邏輯的時候,咱們須要讀取請求正文中的JSON數據,這時候咱們就能夠用BodyHandler

最後,咱們經過vertx.createHttpServer()方法來建立一個HTTP服務端 (4)。注意這個功能是Vert.x Core提供的底層功能之一。而後咱們將咱們的路由處理器綁定到服務端上,這也是Vert.x Web的核心。你可能不熟悉router::accept這樣的表示,這是Java 8中的 方法引用,它至關於一個分發路由的Handler。當有請求到達時,Vert.x會調用accept方法。而後咱們經過listen方法監聽8082端口。由於建立服務端的過程可能失敗,所以咱們還須要給listen方法傳遞一個Handler來檢查服務端是否建立成功。正如咱們前面所提到的,咱們可使用future.complete來表示過程成功,或者用future.fail來表示過程失敗。

到如今爲止,咱們已經建立好HTTP服務端了,但咱們尚未見到任何的路由呢!不要着急,是時候去聲明路由了!

配置路由

下面咱們來聲明路由。正如咱們以前提到的,咱們的路由能夠設計成這樣:

  • 添加待辦事項: POST /todos

  • 獲取某一待辦事項: GET /todos/:todoId

  • 獲取全部待辦事項: GET /todos

  • 更新待辦事項: PATCH /todos/:todoId

  • 刪除某一待辦事項: DELETE /todos/:todoId

  • 刪除全部待辦事項: DELETE /todos

路徑參數

在URL中,咱們能夠經過:name的形式定義路徑參數。當處理請求的時候,Vert.x會自動獲取這些路徑參數並容許咱們訪問它們。拿咱們的路由舉個例子,/todos/19todoId 映射爲 19

首先咱們先在 io.vertx.blueprint.todolist 包下建立一個Constants類用於存儲各類全局常量(固然也能夠放到其對應的類中):

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標識處替換爲如下的內容:

// 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);

代碼很直觀、明瞭。咱們用對應的方法(如get,post,patch等等)將路由路徑與路由器綁定,而且咱們調用handler方法給每一個路由綁定上對應的Handler,接受的Handler類型爲Handler<RoutingContext>。這裏咱們分別綁定了六個方法引用,它們的形式都相似於這樣:

private void handleRequest(RoutingContext context) {
    // ...
}

咱們將在稍後實現這六個方法,這也是咱們待辦事項服務邏輯的核心。

異步方法模式

咱們以前提到過,Vert.x是 異步、非阻塞的 。每個異步的方法總會接受一個 Handler 參數做爲回調函數,當對應的操做完成時會調用接受的Handler,這是異步方法的一種實現。還有一種等價的實現是返回Future對象:

void doAsync(A a, B b, Handler<R> handler);
// 這兩種實現等價
Future<R> doAsync(A a, B b);

其中,Future 對象表明着一個操做的結果,這個操做可能尚未進行,可能正在進行,可能成功也可能失敗。當操做完成時,Future對象會獲得對應的結果。咱們也能夠經過setHandler方法給Future綁定一個Handler,當Future被賦予結果的時候,此Handler會被調用。

Future<R> future = doAsync(A a, B b);
future.setHandler(r -> {
    if (r.failed()) {
        // 處理失敗
    } else {
        // 操做結果
    }
});

Vert.x中大多數異步方法都是基於Handler的。而在本教程中,這兩種異步模式咱們都會接觸到。

待辦事項邏輯實現

如今是時候來實現咱們的待辦事項業務邏輯了!這裏咱們使用 Redis 做爲數據持久化存儲。有關Redis的詳細介紹請參照Redis 官方網站。Vert.x給咱們提供了一個組件—— Vert.x-redis,容許咱們以異步的形式操做Redis數據。

如何安裝Redis? | 請參照Redis官方網站上詳細的安裝指南

Vert.x Redis

Vert.x Redis容許咱們以異步的形式操做Redis數據。咱們首先須要在build.gradle中添加如下依賴:

compile 'io.vertx:vertx-redis-client:3.3.0'

咱們經過RedisClient對象來操做Redis中的數據,所以咱們定義了一個類成員redis。在使用RedisClient以前,咱們首先須要與Redis創建鏈接,而且須要配置(以RedisOptions的形式),後邊咱們再講須要配置哪些東西。

咱們來實現 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的時候,咱們會首先調用initData方法,這樣能夠保證RedisClient能夠被正常建立。

存儲格式

咱們知道,Redis支持各類格式的數據,而且支持多種方式存儲(如listhash map等)。這裏咱們將咱們的待辦事項存儲在 哈希表(map) 中。咱們使用待辦事項的id做爲key,JSON格式的待辦事項數據做爲value。同時,咱們的哈希表自己也要有個key,咱們把它命名爲 VERT_TODO,而且存儲到Constants類中:

public static final String REDIS_TODO_KEY = "VERT_TODO";

正如咱們以前提到的,咱們利用了生成的JSON數據轉換類來實現Todo實體與JSON數據之間的轉換(經過幾個構造函數),在後面實現待辦事項服務的時候能夠普遍利用。

獲取/獲取全部待辦事項

咱們首先來實現獲取待辦事項的邏輯。正如咱們以前所提到的,咱們的處理邏輯方法須要接受一個RoutingContext類型的參數。咱們看一下獲取某一待辦事項的邏輯方法(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方法獲取路徑參數todoId (1)。咱們須要檢測路徑參數獲取是否成功,若是不成功就返回 400 Bad Request 錯誤 (2)。這裏咱們寫一個函數封裝返回錯誤response的邏輯:

private void sendError(int statusCode, HttpServerResponse response) {
  response.setStatusCode(statusCode).end();
}

這裏面,end方法是很是重要的。只有咱們調用end方法時,對應的HTTP Response才能被髮送回客戶端。

再回到handleGetTodo方法中。若是咱們成功獲取到了todoId,咱們能夠經過hget操做從Redis中獲取對應的待辦事項 (3)。hget表明經過key從對應的哈希表中獲取對應的value,咱們來看一下hget函數的定義:

RedisClient hget(String key, String field, Handler<AsyncResult<String>> handler);

第一個參數key對應哈希表的key,第二個參數field表明待辦事項的key,第三個參數表明當獲取操做成功時對應的回調。在Handler中,咱們首先檢查操做是否成功,若是不成功就返回503錯誤。若是成功了,咱們就能夠獲取操做的結果了。結果是null的話,說明Redis中沒有對應的待辦事項,所以咱們返回404 Not Found表明不存在。若是結果存在,那麼咱們就能夠經過end方法將其寫入response中 (4)。注意到咱們全部的RESTful API都返回JSON格式的數據,因此咱們將content-type頭設爲JSON

獲取全部待辦事項的邏輯handleGetAllhandleGetTodo大致上相似,但實現上有些許不一樣:

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) 來獲取某個哈希表中的全部數據(以JSON數組的形式返回,即JsonArray對象)。在Handler中咱們仍是像以前那樣先檢查操做是否成功。若是成功的話咱們就能夠將結果寫入response了。注意這裏咱們不能直接將返回的JsonArray寫入response。想象一下返回的JsonArray包括着待辦事項的key以及對應的JSON數據(字符串形式),所以此時每一個待辦事項對應的JSON數據都被轉義了,因此咱們須要先把這些轉義過的JSON數據轉換成實體對象,再從新編碼。

咱們這裏採用了一種響應式編程思想的方法。首先咱們瞭解到JsonArray類繼承了Iterable<Object>接口(是否是感受它很像List呢?),所以咱們能夠經過stream方法將其轉化爲Stream對象。注意這裏的Stream可不是傳統意義上講的輸入輸出流(I/O stream),而是數據流(data flow)。咱們須要對數據流進行一系列的變換處理操做,這就是響應式編程的思想(也有點函數式編程的思想)。咱們將數據流中的每一個字符串數據轉換爲Todo實體對象,這個過程是經過map算子實現的。咱們這裏就不深刻討論map算子了,但它在函數式編程中很是重要。在map事後,咱們經過collect方法將數據流「歸約」成List<Todo>。如今咱們就能夠經過Json.encodePrettily方法對獲得的list進行編碼了,轉換成JSON格式的數據。最後咱們將轉換後的結果寫入到response中 (3)。

建立待辦事項

通過了上面兩個業務邏輯實現的過程,你應該開始熟悉Vert.x了~如今咱們來實現建立待辦事項的邏輯:

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數據並轉換成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來實現。

而後咱們經過Json.encodePrettily方法將咱們的Todo實例再次編碼成JSON格式的數據 (2)。接下來咱們利用hset函數將待辦事項實例插入到對應的哈希表中 (3)。若是插入成功,返回 201 狀態碼 (4)。

201 狀態碼?

| 正如你所看到的那樣,咱們將狀態碼設爲201,這表明CREATED(已建立)。另外,若是不指定狀態碼的話,Vert.x Web默認將狀態碼設爲 200 OK

同時,咱們接收到的HTTP請求首部可能格式不正確,所以咱們須要在方法中捕獲DecodeException異常。這樣一旦捕獲到DecodeException異常,咱們就返回400 Bad Request狀態碼。

更新待辦事項

若是你想改變你的計劃,你就須要更新你的待辦事項。咱們來實現更新待辦事項的邏輯,它有點小複雜(或者說是,繁瑣?):

// 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 中獲取路徑參數 todoId (1),這是咱們想要更改待辦事項對應的id。而後咱們從請求正文中獲取新的待辦事項數據 (2)。這一步也有可能拋出 DecodeException 異常所以咱們也須要去捕獲它。要更新待辦事項,咱們須要先經過hget函數獲取以前的待辦事項 (3),檢查其是否存在。獲取舊的待辦事項以後,咱們調用以前在Todo類中實現的merge方法將舊待辦事項與新待辦事項整合到一塊兒 (5),而後編碼成JSON格式的數據。而後咱們經過hset函數更新對應的待辦事項 (6)(hset表示若是不存在就插入,存在就更新)。操做成功的話,返回 200 OK 狀態。

這就是更新待辦事項的邏輯~要有耐心喲,咱們立刻就要見到勝利的曙光了~下面咱們來實現刪除待辦事項的邏輯。

刪除/刪除所有待辦事項

刪除待辦事項的邏輯很是簡單。咱們利用hdel函數來刪除某一待辦事項,用del函數刪掉全部待辦事項(其實是直接把那個哈希表給刪了)。若是刪除操做成功,返回204 No Content 狀態。

這裏直接給出代碼:

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());
  });
}

啊哈!咱們實現待辦事項服務的Verticle已經完成咯~一顆賽艇!可是咱們該如何去運行咱們的Verticle呢?答案是,咱們須要 部署並運行 咱們的Verticle。還好Vert.x提供了一個運行Verticle的輔助工具:Vert.x Launcher,讓咱們來看看如何利用它。

將應用與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區塊中,咱們配置Gradle使其生成 fat-jar,並指定啓動類。fat-jar 是一個給Vert.x應用打包的簡便方法,它直接將咱們的應用連同全部的依賴都給打包到jar包中去了,這樣咱們能夠直接經過jar包運行咱們的應用而沒必要再指定依賴的 CLASSPATH

  • 咱們將Main-Class屬性設爲io.vertx.core.Launcher,這樣就能夠經過Vert.x Launcher來啓動對應的Verticle了。另外咱們須要將Main-Verticle屬性設爲咱們想要部署的Verticle的類名(全名)。

配置好了之後,咱們就能夠打包了:

gradle build

運行咱們的服務

萬事俱備,只欠東風。是時候運行咱們的待辦事項服務了!首先咱們先啓動Redis服務:

redis-server

而後運行服務:

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,查看測試結果:

測試結果

固然,咱們也能夠用其它工具,好比 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"
} ]

將服務與控制器分離

啊哈~咱們的待辦事項服務已經能夠正常運行了,可是回頭再來看看 SingleApplicationVerticle 類的代碼,你會發現它很是混亂,待辦事項業務邏輯與控制器混雜在一塊兒,讓這個類很是的龐大,而且這也不利於咱們服務的擴展。根據面向對象解耦的思想,咱們須要將控制器部分與業務邏輯部分分離。

用Future實現異步服務

下面咱們來設計咱們的業務邏輯層。就像咱們以前提到的那樣,咱們的服務須要是異步的,所以這些服務的方法要麼須要接受一個Handler參數做爲回調,要麼須要返回一個Future對象。可是想象一下不少個Handler混雜在一塊兒嵌套的狀況,你會陷入 回調地獄,這是很是糟糕的。所以,這裏咱們用Future實現咱們的待辦事項服務。

io.vertx.blueprint.todolist.service 包下建立 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 {

  Future<Boolean> initData(); // 初始化數據(或數據庫)

  Future<Boolean> insert(Todo todo);

  Future<List<Todo>> getAll();

  Future<Optional<Todo>> getCertain(String todoID);

  Future<Todo> update(String todoId, Todo newTodo);

  Future<Boolean> delete(String todoId);

  Future<Boolean> deleteAll();

}

注意到getCertain方法返回一個Future<Optional<Todo>>對象。那麼Optional是啥呢?它封裝了一個可能爲空的對象。由於數據庫裏面可能沒有與咱們給定的todoId相對應的待辦事項,查詢的結果可能爲空,所以咱們給它包裝上 OptionalOptional 能夠避免萬惡的 NullPointerException,而且它在函數式編程中用途特別普遍(在Haskell中對應 Maybe Monad)。

既然咱們已經設計好咱們的異步服務接口了,讓咱們來重構原先的Verticle吧!

開始重構!

咱們建立一個新的Verticle。在 io.vertx.blueprint.todolist.verticles 包中建立 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(Future<Void> future) throws Exception {
    Router router = Router.router(vertx);
    // CORS support
    Set<String> 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<HttpMethod> 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的結構與咱們以前的Verticle相相似,這裏就很少說了。下面咱們來利用咱們以前編寫的服務接口實現每個控制器方法。

首先先實現 initData 方法,此方法用於初始化存儲結構:

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();
      }
    });
}

首先咱們從配置中獲取服務的類型,這裏咱們有兩種類型的服務:redisjdbc,默認是redis。接着咱們會根據服務的類型以及對應的配置來建立服務。在這裏,咱們的配置都是從JSON格式的配置文件中讀取,並經過Vert.x Launcher的-conf項加載。後面咱們再講要配置哪些東西。

接着咱們給service.initData()方法返回的Future對象綁定了一個Handler,這個Handler將會在Future獲得結果的時候被調用。一旦初始化過程失敗,錯誤信息將會顯示到終端上。

其它的方法實現也相似,這裏就不詳細解釋了,直接放上代碼,很是簡潔明瞭:

/**
 * Wrap the result handler with failure handler (503 Service Unavailable)
 */
private <T> Handler<AsyncResult<T>> resultHandler(RoutingContext context, Consumer<T> 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<AsyncResult<Boolean>> 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生成器:resultHandlerdeleteResultHandler。這兩個生成器封裝了一些重複的代碼,能夠減小代碼量。

嗯。。。咱們的新Verticle寫好了,那麼是時候去實現具體的業務邏輯了。這裏咱們會實現兩個版本的業務邏輯,分別對應兩種存儲:RedisMySQL

Vert.x-Redis版本的待辦事項服務

以前咱們已經實現過一遍Redis版本的服務了,所以你應該對其很是熟悉了。這裏咱們僅僅解釋一個 update 方法,其它的實現都很是相似,代碼能夠在GitHub上瀏覽。

Monadic Future

回想一下咱們以前寫的更新待辦事項的邏輯,咱們會發現它實際上是由兩個獨立的操做組成 - getinsert(對於Redis來講)。因此呢,咱們可不能夠複用 getCertaininsert 這兩個方法?固然了!由於Future是可組合的,所以咱們能夠將這兩個方法返回的Future組合到一塊兒。是否是很是方便呢?咱們來編寫此方法:

@Override
public Future<Todo> update(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)
    }
  });
}

首先咱們調用了getCertain方法,此方法返回一個Future<Optional<Todo>>對象。同時咱們使用compose函數將此方法返回的Future與另外一個Future進行組合(1),其中compose函數接受一個T => Future<U>類型的lambda。而後咱們接着檢查舊的待辦事項是否存在,若是存在的話,咱們將新的待辦事項與舊的待辦事項相融合,而後更新待辦事項。注意到insert方法返回Future<Boolean>類型的Future,所以咱們還須要對此Future的結果作變換,這個變換的過程是經過map函數實現的(2)。map函數接受一個T => U類型的lambda。若是舊的待辦事項不存在,咱們返回一個包含null的Future(3)。最後咱們返回組合後的Future對象。

Future 的本質

在函數式編程中,Future 其實是一種 Monad。有關Monad的理論較爲複雜,這裏就不進行闡述了。你能夠簡單地把它看做是一個能夠進行變換(map)和組合(compose)的包裝對象。咱們把這種特性叫作 Monadic

下面來實現MySQL版本的待辦事項服務。

Vert.x-JDBC版本的待辦事項服務

JDBC ++ 異步

咱們使用Vert.x-JDBC和MySQL來實現JDBC版本的待辦事項服務。咱們知道,數據庫操做都是阻塞操做,極可能會佔用很多時間。而Vert.x-JDBC提供了一種異步操做數據庫的模式,很神奇吧?因此,在傳統JDBC代碼下咱們要執行SQL語句須要這樣:

String SQL = "SELECT * FROM todo";
// ...
ResultSet rs = pstmt.executeQuery(SQL);

而在Vert.x JDBC中,咱們能夠利用回調獲取數據:

connection.query(SQL, result -> {
    // do something with result...
});

這種異步操做能夠有效避免對數據的等待。當數據獲取成功時會自動調用回調函數來執行處理數據的邏輯。

添加依賴

首先咱們須要向build.gradle文件中添加依賴:

compile 'io.vertx:vertx-jdbc-client:3.3.0'
compile 'mysql:mysql-connector-java:6.0.2'

其中第二個依賴是MySQL的驅動,若是你想使用其餘的數據庫,你須要自行替換掉這個依賴。

初始化JDBCClient

在Vert.x JDBC中,咱們須要從一個JDBCClient對象中獲取數據庫鏈接,所以咱們來看一下如何建立JDBCClient實例。在io.vertx.blueprint.todolist.service包下建立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)方法來建立一個JDBCClient實例,其中咱們傳入一個JsonObject對象做爲配置。通常來講,咱們須要配置如下的內容:

  • url - JDBC URL,好比 jdbc:mysql://localhost/vertx_blueprint

  • driver_class - JDBC驅動名稱,好比 com.mysql.cj.jdbc.Driver

  • user - 數據庫用戶

  • password - 數據庫密碼

咱們將會經過Vert.x Launcher從配置文件中讀取此JsonObject

如今咱們已經建立了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`)
)

咱們把要用到的數據庫語句都存到服務類中(這裏咱們就不討論如何設計表以及寫SQL了):

private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS `todo` (\n" +
  "  `id` int(11) NOT NULL AUTO_INCREMENT,\n" +
  "  `title` varchar(255) DEFAULT NULL,\n" +
  "  `completed` tinyint(1) DEFAULT NULL,\n" +
  "  `order` int(11) DEFAULT NULL,\n" +
  "  `url` varchar(255) DEFAULT NULL,\n" +
  "  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`\n" +
  "SET `id` = ?,\n" +
  "`title` = ?,\n" +
  "`completed` = ?,\n" +
  "`order` = ?,\n" +
  "`url` = ?\n" +
  "WHERE `id` = ?;";
private static final String SQL_DELETE = "DELETE FROM `todo` WHERE `id` = ?";
private static final String SQL_DELETE_ALL = "DELETE FROM `todo`";

OK!一切工做準備就緒,下面咱們來實現咱們的JDBC版本的服務~

實現JDBC版本的服務

全部的獲取鏈接、獲取執行數據的操做都要在Handler中完成。好比咱們能夠這樣獲取數據庫鏈接:

client.getConnection(conn -> {
      if (conn.succeeded()) {
        final SQLConnection connection = conn.result();
        // do something...
      } else {
        // handle failure
      }
    });

因爲每個數據庫操做都須要獲取數據庫鏈接,所以咱們來包裝一個返回Handler<AsyncResult<SQLConnection>>的方法,在此回調中能夠直接使用數據庫鏈接,能夠減小一些代碼量:

private Handler<AsyncResult<SQLConnection>> connHandler(Future future, Handler<SQLConnection> handler) {
  return conn -> {
    if (conn.succeeded()) {
      final SQLConnection connection = conn.result();
      handler.handle(connection);
    } else {
      future.fail(conn.cause());
    }
  };
}

獲取數據庫鏈接之後,咱們就能夠對數據庫進行各類操做了:

  • query : 執行查詢(raw SQL)

  • queryWithParams : 執行預編譯查詢(prepared statement)

  • updateWithParams : 執行預編譯DDL語句(prepared statement)

  • execute: 執行任意SQL語句

全部的方法都是異步的因此每一個方法最後都接受一個Handler參數,咱們能夠在此Handler中獲取結果並執行相應邏輯。

如今咱們來編寫初始化數據庫表的initData方法:

@Override
public Future<Boolean> initData() {
  Future<Boolean> 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初始化時被調用,若是todo表不存在的話就建立一下。注意,最後必定要關閉數據庫鏈接

下面咱們來實現插入邏輯方法:

@Override
public Future<Boolean> insert(Todo todo) {
  Future<Boolean> 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方法執行插入邏輯,而且傳遞了一個JsonArray變量做爲預編譯參數。這一點很重要,使用預編譯語句能夠有效防止SQL注入。

咱們再來實現getCertain方法:

@Override
public Future<Optional<Todo>> getCertain(String todoID) {
  Future<Optional<Todo>> 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<JsonObject> 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;
}

在這個方法裏,當咱們的查詢語句執行之後,咱們得到到了ResultSet實例做爲查詢的結果集。咱們能夠經過getColumnNames方法獲取字段名稱,經過getResults方法獲取結果。這裏咱們經過getRows方法來獲取結果集,結果集的類型爲List<JsonObject>

其他的幾個方法:getAll, update, delete 以及 deleteAll都遵循上面的模式,這裏就很少說了。你能夠在GitHub上瀏覽完整的源代碼。

重構完畢,咱們來寫待辦事項服務對應的配置,而後再來運行!

再來運行!

首先咱們在項目的根目錄下建立一個 config 文件夾做爲配置文件夾。咱們在其中建立一個config_jdbc.json文件做爲 jdbc 類型服務的配置:

{
  "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
}

你須要根據本身的狀況替換掉上述配置文件中相應的內容(如 JDBC URLJDBC 驅動 等)。

再建一個config.json文件做爲redis類型服務的配置(其它的項就用默認配置好啦):

{
  "service.type": "redis"
}

咱們的構建文件也須要更新咯~這裏直接給出最終的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'
}

好啦好啦,火燒眉毛了吧?~打開終端,構建咱們的應用:

gradle build

而後咱們能夠運行Redis版本的待辦事項服務:

java -jar build/libs/vertx-blueprint-todo-backend-fat.jar -conf config/config.json

咱們也能夠運行JDBC版本的待辦事項服務:

java -jar build/libs/vertx-blueprint-todo-backend-fat.jar -conf config/config_jdbc.json

一樣地,咱們也可使用todo-backend-js-spec來測試咱們的API。因爲咱們的API設計沒有改變,所以測試結果應該不會有變化。

咱們也提供了待辦事項服務對應的Docker Compose鏡像構建文件,能夠直接經過Docker來運行咱們的待辦事項服務。你能夠在倉庫的根目錄下看到相應的配置文件,並經過 docker-compose up -- build 命令來構建並運行。

Docker Compose

哈哈,成功了!

哈哈,恭喜你完成了整個待辦事項服務,是否是很開心?~在整個教程中,你應該學到了不少關於 Vert.x WebVert.x RedisVert.x JDBC 的開發知識。固然,最重要的是,你會對Vert.x的 異步開發模式 有了更深的理解和領悟。

更多關於Vert.x的文章,請參考Blog on Vert.x Website。官網的資料是最全面的 :-)

來自其它框架?

以前你可能用過其它的框架,好比Spring Boot。這一小節,我將會用類比的方式來介紹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)來處理對應的請求。

另外,在Vert.x Web中,咱們使用 end 方法來向客戶端發送HTTP response。相對地,在Spring Boot中咱們直接在每一個方法中返回結果做爲response。

來自Play Framework 2

若是以前用過Play Framework 2的話,你必定會很是熟悉異步開發模式。在Play Framework 2中,咱們在 routes 文件中定義路由,相似於這樣:

GET     /todos/:todoId      controllers.TodoController.handleGetCertain(todoId: Int)

而在Vert.x Web中,咱們經過Router對象來配置路由:

router.get("/todos/:todoId").handler(this::handleGetCertain);

this::handleGetCertain是處理對應請求的方法引用(在Scala裏能夠把它看做是一個函數)。

Play Framework 2中的異步開發模式是基於Future的。每個路由處理函數都返回一個Action對象(實質上是一個類型爲Request[A] => Result的函數),咱們在Action.apply(或Action.async)閉包中編寫咱們的處理邏輯,相似於這樣:

def handleGetCertain(todoId: Int): Action[AnyContent] = Action.async {
    service.getCertain(todoId) map { // 服務返回的類型是 `Future[Option[Todo]]`
        case Some(res) =>
            Ok(Json.toJson(res))
        case None =>
            NotFound()
    }
}

而在Vert.x Web中,異步開發模式基本上都是基於回調的(固然也能夠用Vert.x RxJava)。咱們能夠這麼寫:

private void handleCreateTodo(RoutingContext context) {
    String todoId = context.request().getParam("todoId"); // 獲取Path Variable
    service.getCertain(todoId).setHandler(r -> { // 服務返回的類型是 `Future<Optional<Todo>>`
        if (r.succeeded) {
            Optional<Todo> res = r.result;
            if (res.isPresent()) {
                context.response()
                    .putHeader("content-type", "application/json")
                    .end(Json.encodePrettily(res));
            } else {
                sendError(404, context.response()); // NotFound(404)
            }
        } else {
            sendError(503, context.response());
        }
    });
}

想要使用其它持久化存儲框架?

你可能想在Vert.x中使用其它的持久化存儲框架或庫,好比MyBatis ORM或者Jedis,這固然能夠啦!Vert.x容許開發者整合任何其它的框架和庫,可是像MyBatis ORM這種框架都是阻塞型的,可能會阻塞Event Loop線程,所以咱們須要利用blockingHandler方法去執行阻塞的操做:

router.get("/todos/:todoId").blockingHandler(routingContext -> {
            String todoID = routingContext.request().getParam("todoId");
            Todo res = service.fetchBlocking(todoID); // 阻塞型

            // 作一些微小的工做

            routingContext.next();
        });

Vert.x會使用Worker線程去執行blockingHandler方法(或者Worker Verticles)中的操做,所以不會阻塞Event Loop線程。


My Blog: 「千載絃歌,芳華如夢」 - sczyh30's blog

若是您對Vert.x感興趣,歡迎加入Vert.x中國用戶組QQ羣,一塊兒探討。羣號:515203212

相關文章
相關標籤/搜索