成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

Vert.x Blueprint 系列教程(一) | 待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程

frank_fun / 3488人閱讀

摘要:本文章是藍(lán)圖系列的第一篇教程。是事件驅(qū)動(dòng)的,同時(shí)也是非阻塞的。是一組負(fù)責(zé)分發(fā)和處理事件的線程。注意,我們絕對(duì)不能去阻塞線程,否則事件的處理過(guò)程會(huì)被阻塞,我們的應(yīng)用就失去了響應(yīng)能力。每個(gè)負(fù)責(zé)處理請(qǐng)求并且寫(xiě)入回應(yīng)結(jié)果。

本文章是 Vert.x 藍(lán)圖系列 的第一篇教程。全系列:

Vert.x Blueprint 系列教程(一) | 待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程

Vert.x Blueprint 系列教程(二) | 開(kāi)發(fā)基于消息的應(yīng)用 - Vert.x Kue 教程

Vert.x Blueprint 系列教程(三) | Micro-Shop 微服務(wù)應(yīng)用實(shí)戰(zhàn)

本系列已發(fā)布至Vert.x官網(wǎng):Vert.x Blueprint Tutorials

前言

在本教程中,我們會(huì)使用Vert.x來(lái)一步一步地開(kāi)發(fā)一個(gè)REST風(fēng)格的Web服務(wù) - Todo Backend,你可以把它看作是一個(gè)簡(jiǎn)單的待辦事項(xiàng)服務(wù),我們可以自由添加或者取消各種待辦事項(xiàng)。

通過(guò)本教程,你將會(huì)學(xué)習(xí)到以下的內(nèi)容:

Vert.x 是什么,以及其基本設(shè)計(jì)思想

Verticle是什么,以及如何使用Verticle

如何用 Vert.x Web 來(lái)開(kāi)發(fā)REST風(fēng)格的Web服務(wù)

異步編程風(fēng)格 的應(yīng)用

如何通過(guò) Vert.x 的各種組件來(lái)進(jìn)行數(shù)據(jù)的存儲(chǔ)操作(如 RedisMySQL

本教程是Vert.x 藍(lán)圖系列的第一篇教程,對(duì)應(yīng)的Vert.x版本為3.3.0。本教程中的完整代碼已托管至GitHub。

踏入Vert.x之門(mén)

朋友,歡迎來(lái)到Vert.x的世界!初次聽(tīng)說(shuō)Vert.x,你一定會(huì)非常好奇:這是啥?讓我們來(lái)看一下Vert.x的官方解釋?zhuān)?/p>

Vert.x is a tool-kit for building reactive applications on the JVM.

(⊙o⊙)哦哦。。。翻譯一下,Vert.x是一個(gè)在JVM上構(gòu)建 響應(yīng)式 應(yīng)用的 工具集 。這個(gè)定義比較模糊,我們來(lái)簡(jiǎn)單解釋一下:工具集 意味著Vert.x非常輕量,可以嵌入到你當(dāng)前的應(yīng)用中而不需要改變現(xiàn)有的結(jié)構(gòu);另一個(gè)重要的描述是 響應(yīng)式 —— Vert.x就是為構(gòu)建響應(yīng)式應(yīng)用(系統(tǒng))而設(shè)計(jì)的。響應(yīng)式系統(tǒng)這個(gè)概念在 Reactive Manifesto 中有詳細(xì)的定義。我們?cè)谶@里總結(jié)4個(gè)要點(diǎn):

響應(yīng)式的(Responsive):一個(gè)響應(yīng)式系統(tǒng)需要在 合理 的時(shí)間內(nèi)處理請(qǐng)求。

彈性的(Resilient):一個(gè)響應(yīng)式系統(tǒng)必須在遇到 異常 (崩潰,超時(shí), 500 錯(cuò)誤等等)的時(shí)候保持響應(yīng)的能力,所以它必須要為 異常處理 而設(shè)計(jì)。

可伸縮的(Elastic):一個(gè)響應(yīng)式系統(tǒng)必須在不同的負(fù)載情況下都要保持響應(yīng)能力,所以它必須能伸能縮,并且可以利用最少的資源來(lái)處理負(fù)載。

消息驅(qū)動(dòng):一個(gè)響應(yīng)式系統(tǒng)的各個(gè)組件之間通過(guò) 異步消息傳遞 來(lái)進(jìn)行交互。

Vert.x是事件驅(qū)動(dòng)的,同時(shí)也是非阻塞的。首先,我們來(lái)介紹 Event Loop 的概念。Event Loop是一組負(fù)責(zé)分發(fā)和處理事件的線程。注意,我們絕對(duì)不能去阻塞Event Loop線程,否則事件的處理過(guò)程會(huì)被阻塞,我們的應(yīng)用就失去了響應(yīng)能力。因此當(dāng)我們?cè)趯?xiě)Vert.x應(yīng)用的時(shí)候,我們要時(shí)刻謹(jǐn)記 異步非阻塞開(kāi)發(fā)模式 而不是傳統(tǒng)的阻塞開(kāi)發(fā)模式。我們將會(huì)在下面詳細(xì)講解異步非阻塞開(kāi)發(fā)模式。

我們的應(yīng)用 - 待辦事項(xiàng)服務(wù)

我們的應(yīng)用是一個(gè)REST風(fēng)格的待辦事項(xiàng)服務(wù),它非常簡(jiǎn)單,整個(gè)API其實(shí)就圍繞著 增刪改查 四種操作。所以我們可以設(shè)計(jì)以下的路由:

添加待辦事項(xiàng): POST /todos

獲取某一待辦事項(xiàng): GET /todos/:todoId

獲取所有待辦事項(xiàng): GET /todos

更新待辦事項(xiàng): PATCH /todos/:todoId

刪除某一待辦事項(xiàng): DELETE /todos/:todoId

刪除所有待辦事項(xiàng): DELETE /todos

注意我們這里不討論REST風(fēng)格API的設(shè)計(jì)規(guī)范(仁者見(jiàn)仁,智者見(jiàn)智),因此你也可以用你喜歡的方式去定義路由。

下面我們開(kāi)始開(kāi)發(fā)我們的項(xiàng)目!High起來(lái)~~~

說(shuō)干就干!

Vert.x Core提供了一些較為底層的處理HTTP請(qǐng)求的功能,這對(duì)于Web開(kāi)發(fā)來(lái)說(shuō)不是很方便,因?yàn)槲覀兺ǔ2恍枰@么底層的功能,因此Vert.x Web應(yīng)運(yùn)而生。Vert.x Web基于Vert.x Core,并且提供一組更易于創(chuàng)建Web應(yīng)用的上層功能(如路由)。

Gradle配置文件

首先我們先來(lái)創(chuàng)建我們的項(xiàng)目。在本教程中我們使用Gradle作為構(gòu)建工具,當(dāng)然你也可以使用其它諸如Maven之類(lèi)的構(gòu)建工具。我們的項(xiàng)目目錄里需要有:

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

src/test/java 文件夾(測(cè)試目錄)

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

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

我們首先來(lái)創(chuàng)建 build.gradle 文件,這是Gradle對(duì)應(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,這不要緊。我們來(lái)解釋一下:

我們將 targetCompatibilitysourceCompatibility 這兩個(gè)值都設(shè)為1.8,代表目標(biāo)Java版本是Java 8。這非常重要,因?yàn)閂ert.x就是基于Java 8構(gòu)建的。

dependencies中,我們聲明了我們需要的依賴(lài)。vertx-corevert-web 用于開(kāi)發(fā)REST API。

注: 若國(guó)內(nèi)用戶出現(xiàn)用Gradle解析依賴(lài)非常緩慢的情況,可以嘗試使用開(kāi)源中國(guó)Maven鏡像代替默認(rèn)的鏡像(有的時(shí)候速度比較快)。只要在build.gradle中配置即可:

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

搞定build.gradle以后,我們開(kāi)始寫(xiě)代碼!

待辦事項(xiàng)對(duì)象

首先我們需要?jiǎng)?chuàng)建我們的數(shù)據(jù)實(shí)體對(duì)象 - Todo 實(shí)體。在io.vertx.blueprint.todolist.entity包下創(chuàng)建Todo類(lèi),并且編寫(xiě)以下代碼:

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的時(shí)候需要先注釋掉,生成過(guò)后再取消注釋
  }

  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 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 實(shí)體對(duì)象由序號(hào)id、標(biāo)題title、次序order、地址url以及代表待辦事項(xiàng)是否完成的一個(gè)標(biāo)識(shí)complete組成。我們可以把它看作是一個(gè)簡(jiǎn)單的Java Bean。它可以被編碼成JSON格式的數(shù)據(jù),我們?cè)诤筮厱?huì)大量使用JSON(事實(shí)上,在Vert.x中JSON非常普遍)。同時(shí)注意到我們給Todo類(lèi)加上了一個(gè)注解:@DataObject,這是用于生成JSON轉(zhuǎn)換類(lèi)的注解。

DataObject 注解
@DataObject 注解的實(shí)體類(lèi)需要滿足以下條件:擁有一個(gè)拷貝構(gòu)造函數(shù)以及一個(gè)接受一個(gè) JsonObject 對(duì)象的構(gòu)造函數(shù)。

我們利用Vert.x Codegen來(lái)自動(dòng)生成JSON轉(zhuǎn)換類(lèi)。我們需要在build.gradle中添加依賴(lài):

compile "io.vertx:vertx-codegen:3.3.0"

同時(shí),我們需要在io.vertx.blueprint.todolist.entity包中添加package-info.java文件來(lái)指引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ì)上是一個(gè)注解處理器(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
}

這樣,每次我們?cè)诰幾g項(xiàng)目的時(shí)候,Vert.x Codegen都會(huì)自動(dòng)檢測(cè)含有 @DataObject 注解的類(lèi)并且根據(jù)配置生成JSON轉(zhuǎn)換類(lèi)。在本例中,我們應(yīng)該會(huì)得到一個(gè) TodoConverter 類(lèi),然后我們可以在Todo類(lèi)中使用它。

Verticle

下面我們來(lái)寫(xiě)我們的應(yīng)用組件。在io.vertx.blueprint.todolist.verticles包中創(chuàng)建SingleApplicationVerticle類(lèi),并編寫(xiě)以下代碼:

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 future) throws Exception {
      // TODO with start...
  }
}

我們的SingleApplicationVerticle類(lèi)繼承了AbstractVerticle抽象類(lèi)。那么什么是 Verticle 呢?在Vert.x中,一個(gè)Verticle代表應(yīng)用的某一組件。我們可以通過(guò)部署Verticle來(lái)運(yùn)行這些組件。如果你了解 Actor 模型的話,你會(huì)發(fā)現(xiàn)它和Actor非常類(lèi)似。

當(dāng)Verticle被部署的時(shí)候,其start方法會(huì)被調(diào)用。我們注意到這里的start方法接受一個(gè)類(lèi)型為Future的參數(shù),這代表了這是一個(gè)異步的初始化方法。這里的Future代表著Verticle的初始化過(guò)程是否完成。你可以通過(guò)調(diào)用Future的complete方法來(lái)代表初始化過(guò)程完成,或者fail方法代表初始化過(guò)程失敗。

現(xiàn)在我們Verticle的輪廓已經(jīng)搞好了,那么下一步也就很明了了 - 創(chuàng)建HTTP Client并且配置路由,處理HTTP請(qǐng)求。

Vert.x Web與REST API 創(chuàng)建HTTP服務(wù)端并配置路由

我們來(lái)給start方法加點(diǎn)東西:

@Override
public void start(Future future) 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⊙)…一長(zhǎng)串代碼誒。。是不是看著很暈?zāi)??我們?lái)詳細(xì)解釋一下。

首先我們創(chuàng)建了一個(gè) Router 實(shí)例 (1)。這里的Router代表路由器,相信做過(guò)Web開(kāi)發(fā)的開(kāi)發(fā)者們一定不會(huì)陌生。路由器負(fù)責(zé)將對(duì)應(yīng)的HTTP請(qǐng)求分發(fā)至對(duì)應(yīng)的處理邏輯(Handler)中。每個(gè)Handler負(fù)責(zé)處理請(qǐng)求并且寫(xiě)入回應(yīng)結(jié)果。當(dāng)HTTP請(qǐng)求到達(dá)時(shí),對(duì)應(yīng)的Handler會(huì)被調(diào)用。

然后我們創(chuàng)建了兩個(gè)SetallowHeadersallowMethods,并且我們向里面添加了一些HTTP Header以及HTTP Method,然后我們給路由器綁定了一個(gè)CorsHandler (2)。route()方法(無(wú)參數(shù))代表此路由匹配所有請(qǐng)求。這兩個(gè)Set的作用是支持 CORS,因?yàn)槲覀兊腁PI需要開(kāi)啟CORS以便配合前端正常工作。有關(guān)CORS的詳細(xì)內(nèi)容我們就不在這里細(xì)說(shuō)了,詳情可以參考這里。我們這里只需要知道如何開(kāi)啟CORS支持即可。

接下來(lái)我們給路由器綁定了一個(gè)全局的BodyHandler (3),它的作用是處理HTTP請(qǐng)求正文并獲取其中的數(shù)據(jù)。比如,在實(shí)現(xiàn)添加待辦事項(xiàng)邏輯的時(shí)候,我們需要讀取請(qǐng)求正文中的JSON數(shù)據(jù),這時(shí)候我們就可以用BodyHandler。

最后,我們通過(guò)vertx.createHttpServer()方法來(lái)創(chuàng)建一個(gè)HTTP服務(wù)端 (4)。注意這個(gè)功能是Vert.x Core提供的底層功能之一。然后我們將我們的路由處理器綁定到服務(wù)端上,這也是Vert.x Web的核心。你可能不熟悉router::accept這樣的表示,這是Java 8中的 方法引用,它相當(dāng)于一個(gè)分發(fā)路由的Handler。當(dāng)有請(qǐng)求到達(dá)時(shí),Vert.x會(huì)調(diào)用accept方法。然后我們通過(guò)listen方法監(jiān)聽(tīng)8082端口。因?yàn)閯?chuàng)建服務(wù)端的過(guò)程可能失敗,因此我們還需要給listen方法傳遞一個(gè)Handler來(lái)檢查服務(wù)端是否創(chuàng)建成功。正如我們前面所提到的,我們可以使用future.complete來(lái)表示過(guò)程成功,或者用future.fail來(lái)表示過(guò)程失敗。

到現(xiàn)在為止,我們已經(jīng)創(chuàng)建好HTTP服務(wù)端了,但我們還沒(méi)有見(jiàn)到任何的路由呢!不要著急,是時(shí)候去聲明路由了!

配置路由

下面我們來(lái)聲明路由。正如我們之前提到的,我們的路由可以設(shè)計(jì)成這樣:

添加待辦事項(xiàng): POST /todos

獲取某一待辦事項(xiàng): GET /todos/:todoId

獲取所有待辦事項(xiàng): GET /todos

更新待辦事項(xiàng): PATCH /todos/:todoId

刪除某一待辦事項(xiàng): DELETE /todos/:todoId

刪除所有待辦事項(xiàng): DELETE /todos

路徑參數(shù)

在URL中,我們可以通過(guò):name的形式定義路徑參數(shù)。當(dāng)處理請(qǐng)求的時(shí)候,Vert.x會(huì)自動(dòng)獲取這些路徑參數(shù)并允許我們?cè)L問(wèn)它們。拿我們的路由舉個(gè)例子,/todos/19todoId 映射為 19。

首先我們先在 io.vertx.blueprint.todolist 包下創(chuàng)建一個(gè)Constants類(lèi)用于存儲(chǔ)各種全局常量(當(dāng)然也可以放到其對(duì)應(yīng)的類(lèi)中):

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標(biāo)識(shí)處替換為以下的內(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);

代碼很直觀、明了。我們用對(duì)應(yīng)的方法(如get,post,patch等等)將路由路徑與路由器綁定,并且我們調(diào)用handler方法給每個(gè)路由綁定上對(duì)應(yīng)的Handler,接受的Handler類(lèi)型為Handler。這里我們分別綁定了六個(gè)方法引用,它們的形式都類(lèi)似于這樣:

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

我們將在稍后實(shí)現(xiàn)這六個(gè)方法,這也是我們待辦事項(xiàng)服務(wù)邏輯的核心。

異步方法模式

我們之前提到過(guò),Vert.x是 異步、非阻塞的 。每一個(gè)異步的方法總會(huì)接受一個(gè) Handler 參數(shù)作為回調(diào)函數(shù),當(dāng)對(duì)應(yīng)的操作完成時(shí)會(huì)調(diào)用接受的Handler,這是異步方法的一種實(shí)現(xiàn)。還有一種等價(jià)的實(shí)現(xiàn)是返回Future對(duì)象:

void doAsync(A a, B b, Handler handler);
// 這兩種實(shí)現(xiàn)等價(jià)
Future doAsync(A a, B b);

其中,Future 對(duì)象代表著一個(gè)操作的結(jié)果,這個(gè)操作可能還沒(méi)有進(jìn)行,可能正在進(jìn)行,可能成功也可能失敗。當(dāng)操作完成時(shí),Future對(duì)象會(huì)得到對(duì)應(yīng)的結(jié)果。我們也可以通過(guò)setHandler方法給Future綁定一個(gè)Handler,當(dāng)Future被賦予結(jié)果的時(shí)候,此Handler會(huì)被調(diào)用。

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

Vert.x中大多數(shù)異步方法都是基于Handler的。而在本教程中,這兩種異步模式我們都會(huì)接觸到。

待辦事項(xiàng)邏輯實(shí)現(xiàn)

現(xiàn)在是時(shí)候來(lái)實(shí)現(xiàn)我們的待辦事項(xiàng)業(yè)務(wù)邏輯了!這里我們使用 Redis 作為數(shù)據(jù)持久化存儲(chǔ)。有關(guān)Redis的詳細(xì)介紹請(qǐng)參照Redis 官方網(wǎng)站。Vert.x給我們提供了一個(gè)組件—— Vert.x-redis,允許我們以異步的形式操作Redis數(shù)據(jù)。

如何安裝Redis? | 請(qǐng)參照Redis官方網(wǎng)站上詳細(xì)的安裝指南。

Vert.x Redis

Vert.x Redis允許我們以異步的形式操作Redis數(shù)據(jù)。我們首先需要在build.gradle中添加以下依賴(lài):

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

我們通過(guò)RedisClient對(duì)象來(lái)操作Redis中的數(shù)據(jù),因此我們定義了一個(gè)類(lèi)成員redis。在使用RedisClient之前,我們首先需要與Redis建立連接,并且需要配置(以RedisOptions的形式),后邊我們?cè)僦v需要配置哪些東西。

我們來(lái)實(shí)現(xiàn) initData 方法用于初始化 RedisClient 并且測(cè)試連接:

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

}

當(dāng)我們?cè)诩虞dVerticle的時(shí)候,我們會(huì)首先調(diào)用initData方法,這樣可以保證RedisClient可以被正常創(chuàng)建。

存儲(chǔ)格式

我們知道,Redis支持各種格式的數(shù)據(jù),并且支持多種方式存儲(chǔ)(如list、hash map等)。這里我們將我們的待辦事項(xiàng)存儲(chǔ)在 哈希表(map) 中。我們使用待辦事項(xiàng)的id作為key,JSON格式的待辦事項(xiàng)數(shù)據(jù)作為value。同時(shí),我們的哈希表本身也要有個(gè)key,我們把它命名為 VERT_TODO,并且存儲(chǔ)到Constants類(lèi)中:

public static final String REDIS_TODO_KEY = "VERT_TODO";

正如我們之前提到的,我們利用了生成的JSON數(shù)據(jù)轉(zhuǎn)換類(lèi)來(lái)實(shí)現(xiàn)Todo實(shí)體與JSON數(shù)據(jù)之間的轉(zhuǎn)換(通過(guò)幾個(gè)構(gòu)造函數(shù)),在后面實(shí)現(xiàn)待辦事項(xiàng)服務(wù)的時(shí)候可以廣泛利用。

獲取/獲取所有待辦事項(xiàng)

我們首先來(lái)實(shí)現(xiàn)獲取待辦事項(xiàng)的邏輯。正如我們之前所提到的,我們的處理邏輯方法需要接受一個(gè)RoutingContext類(lèi)型的參數(shù)。我們看一下獲取某一待辦事項(xiàng)的邏輯方法(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());
    });
  }
}

首先我們先通過(guò)getParam方法獲取路徑參數(shù)todoId (1)。我們需要檢測(cè)路徑參數(shù)獲取是否成功,如果不成功就返回 400 Bad Request 錯(cuò)誤 (2)。這里我們寫(xiě)一個(gè)函數(shù)封裝返回錯(cuò)誤response的邏輯:

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

這里面,end方法是非常重要的。只有我們調(diào)用end方法時(shí),對(duì)應(yīng)的HTTP Response才能被發(fā)送回客戶端。

再回到handleGetTodo方法中。如果我們成功獲取到了todoId,我們可以通過(guò)hget操作從Redis中獲取對(duì)應(yīng)的待辦事項(xiàng) (3)。hget代表通過(guò)key從對(duì)應(yīng)的哈希表中獲取對(duì)應(yīng)的value,我們來(lái)看一下hget函數(shù)的定義:

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

第一個(gè)參數(shù)key對(duì)應(yīng)哈希表的key,第二個(gè)參數(shù)field代表待辦事項(xiàng)的key,第三個(gè)參數(shù)代表當(dāng)獲取操作成功時(shí)對(duì)應(yīng)的回調(diào)。在Handler中,我們首先檢查操作是否成功,如果不成功就返回503錯(cuò)誤。如果成功了,我們就可以獲取操作的結(jié)果了。結(jié)果是null的話,說(shuō)明Redis中沒(méi)有對(duì)應(yīng)的待辦事項(xiàng),因此我們返回404 Not Found代表不存在。如果結(jié)果存在,那么我們就可以通過(guò)end方法將其寫(xiě)入response中 (4)。注意到我們所有的RESTful API都返回JSON格式的數(shù)據(jù),所以我們將content-type頭設(shè)為JSON。

獲取所有待辦事項(xiàng)的邏輯handleGetAllhandleGetTodo大體上類(lèi)似,但實(shí)現(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());
  });
}

這里我們通過(guò)hvals操作 (1) 來(lái)獲取某個(gè)哈希表中的所有數(shù)據(jù)(以JSON數(shù)組的形式返回,即JsonArray對(duì)象)。在Handler中我們還是像之前那樣先檢查操作是否成功。如果成功的話我們就可以將結(jié)果寫(xiě)入response了。注意這里我們不能直接將返回的JsonArray寫(xiě)入response。想象一下返回的JsonArray包括著待辦事項(xiàng)的key以及對(duì)應(yīng)的JSON數(shù)據(jù)(字符串形式),因此此時(shí)每個(gè)待辦事項(xiàng)對(duì)應(yīng)的JSON數(shù)據(jù)都被轉(zhuǎn)義了,所以我們需要先把這些轉(zhuǎn)義過(guò)的JSON數(shù)據(jù)轉(zhuǎn)換成實(shí)體對(duì)象,再重新編碼。

我們這里采用了一種響應(yīng)式編程思想的方法。首先我們了解到JsonArray類(lèi)繼承了Iterable接口(是不是感覺(jué)它很像List呢?),因此我們可以通過(guò)stream方法將其轉(zhuǎn)化為Stream對(duì)象。注意這里的Stream可不是傳統(tǒng)意義上講的輸入輸出流(I/O stream),而是數(shù)據(jù)流(data flow)。我們需要對(duì)數(shù)據(jù)流進(jìn)行一系列的變換處理操作,這就是響應(yīng)式編程的思想(也有點(diǎn)函數(shù)式編程的思想)。我們將數(shù)據(jù)流中的每個(gè)字符串?dāng)?shù)據(jù)轉(zhuǎn)換為Todo實(shí)體對(duì)象,這個(gè)過(guò)程是通過(guò)map算子實(shí)現(xiàn)的。我們這里就不深入討論map算子了,但它在函數(shù)式編程中非常重要。在map過(guò)后,我們通過(guò)collect方法將數(shù)據(jù)流“歸約”成List?,F(xiàn)在我們就可以通過(guò)Json.encodePrettily方法對(duì)得到的list進(jìn)行編碼了,轉(zhuǎn)換成JSON格式的數(shù)據(jù)。最后我們將轉(zhuǎn)換后的結(jié)果寫(xiě)入到response中 (3)。

創(chuàng)建待辦事項(xiàng)

經(jīng)過(guò)了上面兩個(gè)業(yè)務(wù)邏輯實(shí)現(xiàn)的過(guò)程,你應(yīng)該開(kāi)始熟悉Vert.x了~現(xiàn)在我們來(lái)實(shí)現(xiàn)創(chuàng)建待辦事項(xià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());
  }
}

首先我們通過(guò)context.getBodyAsString()方法來(lái)從請(qǐng)求正文中獲取JSON數(shù)據(jù)并轉(zhuǎn)換成Todo實(shí)體對(duì)象 (1)。這里我們包裝了一個(gè)處理Todo實(shí)例的方法,用于給其添加必要的信息(如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;
}

對(duì)于沒(méi)有ID(或者為默認(rèn)ID)的待辦事項(xiàng),我們會(huì)給它分配一個(gè)ID。這里我們采用了自增ID的策略,通過(guò)AtomicInteger來(lái)實(shí)現(xiàn)。

然后我們通過(guò)Json.encodePrettily方法將我們的Todo實(shí)例再次編碼成JSON格式的數(shù)據(jù) (2)。接下來(lái)我們利用hset函數(shù)將待辦事項(xiàng)實(shí)例插入到對(duì)應(yīng)的哈希表中 (3)。如果插入成功,返回 201 狀態(tài)碼 (4)。

201 狀態(tài)碼?

| 正如你所看到的那樣,我們將狀態(tài)碼設(shè)為201,這代表CREATED(已創(chuàng)建)。另外,如果不指定狀態(tài)碼的話,Vert.x Web默認(rèn)將狀態(tài)碼設(shè)為 200 OK。

同時(shí),我們接收到的HTTP請(qǐng)求首部可能格式不正確,因此我們需要在方法中捕獲DecodeException異常。這樣一旦捕獲到DecodeException異常,我們就返回400 Bad Request狀態(tài)碼。

更新待辦事項(xiàng)

如果你想改變你的計(jì)劃,你就需要更新你的待辦事項(xiàng)。我們來(lái)實(shí)現(xiàn)更新待辦事項(xiàng)的邏輯,它有點(diǎn)小復(fù)雜(或者說(shuō)是,繁瑣?):

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

唔。。。一大長(zhǎng)串代碼誒。。。我們來(lái)看一下。首先我們從 RoutingContext 中獲取路徑參數(shù) todoId (1),這是我們想要更改待辦事項(xiàng)對(duì)應(yīng)的id。然后我們從請(qǐng)求正文中獲取新的待辦事項(xiàng)數(shù)據(jù) (2)。這一步也有可能拋出 DecodeException 異常因此我們也需要去捕獲它。要更新待辦事項(xiàng),我們需要先通過(guò)hget函數(shù)獲取之前的待辦事項(xiàng) (3),檢查其是否存在。獲取舊的待辦事項(xiàng)之后,我們調(diào)用之前在Todo類(lèi)中實(shí)現(xiàn)的merge方法將舊待辦事項(xiàng)與新待辦事項(xiàng)整合到一起 (5),然后編碼成JSON格式的數(shù)據(jù)。然后我們通過(guò)hset函數(shù)更新對(duì)應(yīng)的待辦事項(xiàng) (6)(hset表示如果不存在就插入,存在就更新)。操作成功的話,返回 200 OK 狀態(tài)。

這就是更新待辦事項(xiàng)的邏輯~要有耐心喲,我們馬上就要見(jiàn)到勝利的曙光了~下面我們來(lái)實(shí)現(xiàn)刪除待辦事項(xiàng)的邏輯。

刪除/刪除全部待辦事項(xiàng)

刪除待辦事項(xiàng)的邏輯非常簡(jiǎn)單。我們利用hdel函數(shù)來(lái)刪除某一待辦事項(xiàng),用del函數(shù)刪掉所有待辦事項(xiàng)(實(shí)際上是直接把那個(gè)哈希表給刪了)。如果刪除操作成功,返回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());
  });
}

啊哈!我們實(shí)現(xiàn)待辦事項(xiàng)服務(wù)的Verticle已經(jīng)完成咯~一顆賽艇!但是我們?cè)撊绾稳ミ\(yùn)行我們的Verticle呢?答案是,我們需要 部署并運(yùn)行 我們的Verticle。還好Vert.x提供了一個(gè)運(yùn)行Verticle的輔助工具:Vert.x Launcher,讓我們來(lái)看看如何利用它。

將應(yīng)用與Vert.x Launcher一起打包

要通過(guò)Vert.x Launcher來(lái)運(yùn)行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,并指定啟動(dòng)類(lèi)。fat-jar 是一個(gè)給Vert.x應(yīng)用打包的簡(jiǎn)便方法,它直接將我們的應(yīng)用連同所有的依賴(lài)都給打包到j(luò)ar包中去了,這樣我們可以直接通過(guò)jar包運(yùn)行我們的應(yīng)用而不必再指定依賴(lài)的 CLASSPATH

我們將Main-Class屬性設(shè)為io.vertx.core.Launcher,這樣就可以通過(guò)Vert.x Launcher來(lái)啟動(dòng)對(duì)應(yīng)的Verticle了。另外我們需要將Main-Verticle屬性設(shè)為我們想要部署的Verticle的類(lèi)名(全名)。

配置好了以后,我們就可以打包了:

gradle build
運(yùn)行我們的服務(wù)

萬(wàn)事俱備,只欠東風(fēng)。是時(shí)候運(yùn)行我們的待辦事項(xiàng)服務(wù)了!首先我們先啟動(dòng)Redis服務(wù):

redis-server

然后運(yùn)行服務(wù):

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

如果沒(méi)問(wèn)題的話,你將會(huì)在終端中看到 Succeeded in deploying verticle 的字樣。下面我們可以自由測(cè)試我們的API了,其中最簡(jiǎn)便的方法是借助 todo-backend-js-spec 來(lái)測(cè)試。

鍵入 http://127.0.0.1:8082/todos,查看測(cè)試結(jié)果:

當(dāng)然,我們也可以用其它工具,比如 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ù)與控制器分離

啊哈~我們的待辦事項(xiàng)服務(wù)已經(jīng)可以正常運(yùn)行了,但是回頭再來(lái)看看 SingleApplicationVerticle 類(lèi)的代碼,你會(huì)發(fā)現(xiàn)它非常混亂,待辦事項(xiàng)業(yè)務(wù)邏輯與控制器混雜在一起,讓這個(gè)類(lèi)非常的龐大,并且這也不利于我們服務(wù)的擴(kuò)展。根據(jù)面向?qū)ο蠼怦畹乃枷?,我們需要將控制器部分與業(yè)務(wù)邏輯部分分離。

用Future實(shí)現(xiàn)異步服務(wù)

下面我們來(lái)設(shè)計(jì)我們的業(yè)務(wù)邏輯層。就像我們之前提到的那樣,我們的服務(wù)需要是異步的,因此這些服務(wù)的方法要么需要接受一個(gè)Handler參數(shù)作為回調(diào),要么需要返回一個(gè)Future對(duì)象。但是想象一下很多個(gè)Handler混雜在一起嵌套的情況,你會(huì)陷入 回調(diào)地獄,這是非常糟糕的。因此,這里我們用Future實(shí)現(xiàn)我們的待辦事項(xiàng)服務(wù)。

io.vertx.blueprint.todolist.service 包下創(chuàng)建 TodoService 接口并且編寫(xiě)以下代碼:

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 initData(); // 初始化數(shù)據(jù)(或數(shù)據(jù)庫(kù))

  Future insert(Todo todo);

  Future> getAll();

  Future> getCertain(String todoID);

  Future update(String todoId, Todo newTodo);

  Future delete(String todoId);

  Future deleteAll();

}

注意到getCertain方法返回一個(gè)Future>對(duì)象。那么Optional是啥呢?它封裝了一個(gè)可能為空的對(duì)象。因?yàn)閿?shù)據(jù)庫(kù)里面可能沒(méi)有與我們給定的todoId相對(duì)應(yīng)的待辦事項(xiàng),查詢的結(jié)果可能為空,因此我們給它包裝上 OptionalOptional 可以避免萬(wàn)惡的 NullPointerException,并且它在函數(shù)式編程中用途特別廣泛(在Haskell中對(duì)應(yīng) Maybe Monad)。

既然我們已經(jīng)設(shè)計(jì)好我們的異步服務(wù)接口了,讓我們來(lái)重構(gòu)原先的Verticle吧!

開(kāi)始重構(gòu)!

我們創(chuàng)建一個(gè)新的Verticle。在 io.vertx.blueprint.todolist.verticles 包中創(chuàng)建 TodoVerticle 類(lèi),并編寫(xiě)以下代碼:

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

很熟悉吧?這個(gè)Verticle的結(jié)構(gòu)與我們之前的Verticle相類(lèi)似,這里就不多說(shuō)了。下面我們來(lái)利用我們之前編寫(xiě)的服務(wù)接口實(shí)現(xiàn)每一個(gè)控制器方法。

首先先實(shí)現(xiàn) initData 方法,此方法用于初始化存儲(chǔ)結(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ù)的類(lèi)型,這里我們有兩種類(lèi)型的服務(wù):redisjdbc,默認(rèn)是redis。接著我們會(huì)根據(jù)服務(wù)的類(lèi)型以及對(duì)應(yīng)的配置來(lái)創(chuàng)建服務(wù)。在這里,我們的配置都是從JSON格式的配置文件中讀取,并通過(guò)Vert.x Launcher的-conf項(xiàng)加載。后面我們?cè)僦v要配置哪些東西。

接著我們給service.initData()方法返回的Future對(duì)象綁定了一個(gè)Handler,這個(gè)Handler將會(huì)在Future得到結(jié)果的時(shí)候被調(diào)用。一旦初始化過(guò)程失敗,錯(cuò)誤信息將會(huì)顯示到終端上。

其它的方法實(shí)現(xiàn)也類(lèi)似,這里就不詳細(xì)解釋了,直接放上代碼,非常簡(jiǎn)潔明了:

/**
 * Wrap the result handler with failure handler (503 Service Unavailable)
 */
private  Handler> 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很相似呢?這里我們還封裝了兩個(gè)Handler生成器:resultHandlerdeleteResultHandler。這兩個(gè)生成器封裝了一些重復(fù)的代碼,可以減少代碼量。

嗯。。。我們的新Verticle寫(xiě)好了,那么是時(shí)候去實(shí)現(xiàn)具體的業(yè)務(wù)邏輯了。這里我們會(huì)實(shí)現(xiàn)兩個(gè)版本的業(yè)務(wù)邏輯,分別對(duì)應(yīng)兩種存儲(chǔ):RedisMySQL

Vert.x-Redis版本的待辦事項(xiàng)服務(wù)

之前我們已經(jīng)實(shí)現(xiàn)過(guò)一遍Redis版本的服務(wù)了,因此你應(yīng)該對(duì)其非常熟悉了。這里我們僅僅解釋一個(gè) update 方法,其它的實(shí)現(xiàn)都非常類(lèi)似,代碼可以在GitHub上瀏覽。

Monadic Future

回想一下我們之前寫(xiě)的更新待辦事項(xiàng)的邏輯,我們會(huì)發(fā)現(xiàn)它其實(shí)是由兩個(gè)獨(dú)立的操作組成 - getinsert(對(duì)于Redis來(lái)說(shuō))。所以呢,我們可不可以復(fù)用 getCertaininsert 這兩個(gè)方法?當(dāng)然了!因?yàn)?b>Future是可組合的,因此我們可以將這兩個(gè)方法返回的Future組合到一起。是不是非常方便呢?我們來(lái)編寫(xiě)此方法:

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

首先我們調(diào)用了getCertain方法,此方法返回一個(gè)Future>對(duì)象。同時(shí)我們使用compose函數(shù)將此方法返回的Future與另一個(gè)Future進(jìn)行組合(1),其中compose函數(shù)接受一個(gè)T => Future類(lèi)型的lambda。然后我們接著檢查舊的待辦事項(xiàng)是否存在,如果存在的話,我們將新的待辦事項(xiàng)與舊的待辦事項(xiàng)相融合,然后更新待辦事項(xiàng)。注意到insert方法返回Future類(lèi)型的Future,因此我們還需要對(duì)此Future的結(jié)果做變換,這個(gè)變換的過(guò)程是通過(guò)map函數(shù)實(shí)現(xiàn)的(2)。map函數(shù)接受一個(gè)T => U類(lèi)型的lambda。如果舊的待辦事項(xiàng)不存在,我們返回一個(gè)包含null的Future(3)。最后我們返回組合后的Future對(duì)象。

Future 的本質(zhì)

在函數(shù)式編程中,Future 實(shí)際上是一種 Monad。有關(guān)Monad的理論較為復(fù)雜,這里就不進(jìn)行闡述了。你可以簡(jiǎn)單地把它看作是一個(gè)可以進(jìn)行變換(map)和組合(compose)的包裝對(duì)象。我們把這種特性叫做 Monadic。

下面來(lái)實(shí)現(xiàn)MySQL版本的待辦事項(xiàng)服務(wù)。

Vert.x-JDBC版本的待辦事項(xiàng)服務(wù) JDBC ++ 異步

我們使用Vert.x-JDBC和MySQL來(lái)實(shí)現(xiàn)JDBC版本的待辦事項(xiàng)服務(wù)。我們知道,數(shù)據(jù)庫(kù)操作都是阻塞操作,很可能會(huì)占用不少時(shí)間。而Vert.x-JDBC提供了一種異步操作數(shù)據(jù)庫(kù)的模式,很神奇吧?所以,在傳統(tǒng)JDBC代碼下我們要執(zhí)行SQL語(yǔ)句需要這樣:

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

這種異步操作可以有效避免對(duì)數(shù)據(jù)的等待。當(dāng)數(shù)據(jù)獲取成功時(shí)會(huì)自動(dòng)調(diào)用回調(diào)函數(shù)來(lái)執(zhí)行處理數(shù)據(jù)的邏輯。

添加依賴(lài)

首先我們需要向build.gradle文件中添加依賴(lài):

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

其中第二個(gè)依賴(lài)是MySQL的驅(qū)動(dòng),如果你想使用其他的數(shù)據(jù)庫(kù),你需要自行替換掉這個(gè)依賴(lài)。

初始化JDBCClient

在Vert.x JDBC中,我們需要從一個(gè)JDBCClient對(duì)象中獲取數(shù)據(jù)庫(kù)連接,因此我們來(lái)看一下如何創(chuàng)建JDBCClient實(shí)例。在io.vertx.blueprint.todolist.service包下創(chuàng)建JdbcTodoService類(lèi):

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)方法來(lái)創(chuàng)建一個(gè)JDBCClient實(shí)例,其中我們傳入一個(gè)JsonObject對(duì)象作為配置。一般來(lái)說(shuō),我們需要配置以下的內(nèi)容:

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

driver_class - JDBC驅(qū)動(dòng)名稱(chēng),比如 com.mysql.cj.jdbc.Driver

user - 數(shù)據(jù)庫(kù)用戶

password - 數(shù)據(jù)庫(kù)密碼

我們將會(huì)通過(guò)Vert.x Launcher從配置文件中讀取此JsonObject。

現(xiàn)在我們已經(jīng)創(chuàng)建了JDBCClient實(shí)例了,下面我們需要在MySQL中建這樣一個(gè)表:

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ù)庫(kù)語(yǔ)句都存到服務(wù)類(lèi)中(這里我們就不討論如何設(shè)計(jì)表以及寫(xiě)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!一切工作準(zhǔn)備就緒,下面我們來(lái)實(shí)現(xiàn)我們的JDBC版本的服務(wù)~

實(shí)現(xiàn)JDBC版本的服務(wù)

所有的獲取連接、獲取執(zhí)行數(shù)據(jù)的操作都要在Handler中完成。比如我們可以這樣獲取數(shù)據(jù)庫(kù)連接:

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

由于每一個(gè)數(shù)據(jù)庫(kù)操作都需要獲取數(shù)據(jù)庫(kù)連接,因此我們來(lái)包裝一個(gè)返回Handler>的方法,在此回調(diào)中可以直接使用數(shù)據(jù)庫(kù)連接,可以減少一些代碼量:

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ù)庫(kù)連接以后,我們就可以對(duì)數(shù)據(jù)庫(kù)進(jìn)行各種操作了:

query : 執(zhí)行查詢(raw SQL)

queryWithParams : 執(zhí)行預(yù)編譯查詢(prepared statement)

updateWithParams : 執(zhí)行預(yù)編譯DDL語(yǔ)句(prepared statement)

execute: 執(zhí)行任意SQL語(yǔ)句

所有的方法都是異步的所以每個(gè)方法最后都接受一個(gè)Handler參數(shù),我們可以在此Handler中獲取結(jié)果并執(zhí)行相應(yīng)邏輯。

現(xiàn)在我們來(lái)編寫(xiě)初始化數(shù)據(jù)庫(kù)表的initData方法:

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

此方法僅會(huì)在Verticle初始化時(shí)被調(diào)用,如果todo表不存在的話就創(chuàng)建一下。注意,最后一定要關(guān)閉數(shù)據(jù)庫(kù)連接

下面我們來(lái)實(shí)現(xiàn)插入邏輯方法:

@Override
public Future insert(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í)行插入邏輯,并且傳遞了一個(gè)JsonArray變量作為預(yù)編譯參數(shù)。這一點(diǎn)很重要,使用預(yù)編譯語(yǔ)句可以有效防止SQL注入。

我們?cè)賮?lái)實(shí)現(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;
}

在這個(gè)方法里,當(dāng)我們的查詢語(yǔ)句執(zhí)行以后,我們獲得到了ResultSet實(shí)例作為查詢的結(jié)果集。我們可以通過(guò)getColumnNames方法獲取字段名稱(chēng),通過(guò)getResults方法獲取結(jié)果。這里我們通過(guò)getRows方法來(lái)獲取結(jié)果集,結(jié)果集的類(lèi)型為List。

其余的幾個(gè)方法:getAll, update, delete 以及 deleteAll都遵循上面的模式,這里就不多說(shuō)了。你可以在GitHub上瀏覽完整的源代碼。

重構(gòu)完畢,我們來(lái)寫(xiě)待辦事項(xiàng)服務(wù)對(duì)應(yīng)的配置,然后再來(lái)運(yùn)行!

再來(lái)運(yùn)行!

首先我們?cè)陧?xiàng)目的根目錄下創(chuàng)建一個(gè) config 文件夾作為配置文件夾。我們?cè)谄渲袆?chuàng)建一個(gè)config_jdbc.json文件作為 jdbc 類(lèi)型服務(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ū)動(dòng) 等)。

再建一個(gè)config.json文件作為redis類(lèi)型服務(wù)的配置(其它的項(xiàng)就用默認(rèn)配置好啦):

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

好啦好啦,迫不及待了吧?~打開(kāi)終端,構(gòu)建我們的應(yīng)用:

gradle build

然后我們可以運(yùn)行Redis版本的待辦事項(xiàng)服務(wù):

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

我們也可以運(yùn)行JDBC版本的待辦事項(xiàng)服務(wù):

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

同樣地,我們也可以使用todo-backend-js-spec來(lái)測(cè)試我們的API。由于我們的API設(shè)計(jì)沒(méi)有改變,因此測(cè)試結(jié)果應(yīng)該不會(huì)有變化。

我們也提供了待辦事項(xiàng)服務(wù)對(duì)應(yīng)的Docker Compose鏡像構(gòu)建文件,可以直接通過(guò)Docker來(lái)運(yùn)行我們的待辦事項(xiàng)服務(wù)。你可以在倉(cāng)庫(kù)的根目錄下看到相應(yīng)的配置文件,并通過(guò) docker-compose up -- build 命令來(lái)構(gòu)建并運(yùn)行。

哈哈,成功了!

哈哈,恭喜你完成了整個(gè)待辦事項(xiàng)服務(wù),是不是很開(kāi)心?~在整個(gè)教程中,你應(yīng)該學(xué)到了很多關(guān)于 Vert.x WebVert.x RedisVert.x JDBC 的開(kāi)發(fā)知識(shí)。當(dāng)然,最重要的是,你會(huì)對(duì)Vert.x的 異步開(kāi)發(fā)模式 有了更深的理解和領(lǐng)悟。

更多關(guān)于Vert.x的文章,請(qǐng)參考Blog on Vert.x Website。官網(wǎng)的資料是最全面的 :-)

來(lái)自其它框架?

之前你可能用過(guò)其它的框架,比如Spring Boot。這一小節(jié),我將會(huì)用類(lèi)比的方式來(lái)介紹Vert.x Web的使用。

來(lái)自Spring Boot/Spring MVC

在Spring Boot中,我們通常在控制器(Controller)中來(lái)配置路由以及處理請(qǐng)求,比如:

@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 注解來(lái)配置路由,而在Vert.x Web中,我們是通過(guò) Router 對(duì)象來(lái)配置路由的。并且因?yàn)閂ert.x Web是異步的,我們會(huì)給每個(gè)路由綁定一個(gè)處理器(Handler)來(lái)處理對(duì)應(yīng)的請(qǐng)求。

另外,在Vert.x Web中,我們使用 end 方法來(lái)向客戶端發(fā)送HTTP response。相對(duì)地,在Spring Boot中我們直接在每個(gè)方法中返回結(jié)果作為response。

來(lái)自Play Framework 2

如果之前用過(guò)Play Framework 2的話,你一定會(huì)非常熟悉異步開(kāi)發(fā)模式。在Play Framework 2中,我們?cè)?routes 文件中定義路由,類(lèi)似于這樣:

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

而在Vert.x Web中,我們通過(guò)Router對(duì)象來(lái)配置路由:

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/64828.html

相關(guān)文章

  • Vert.x Blueprint 系列教程(二) | Vert.x Kue 教程(Web部分)

    摘要:上部分藍(lán)圖教程中我們一起探索了如何用開(kāi)發(fā)一個(gè)基于消息的應(yīng)用。對(duì)部分來(lái)說(shuō),如果看過(guò)我們之前的藍(lán)圖待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程的話,你應(yīng)該對(duì)這一部分非常熟悉了,因此這里我們就不詳細(xì)解釋了。有關(guān)使用實(shí)現(xiàn)的教程可參考藍(lán)圖待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程。 上部分藍(lán)圖教程中我們一起探索了如何用Vert.x開(kāi)發(fā)一個(gè)基于消息的應(yīng)用。在這部分教程中,我們將粗略地探索一下kue-http模塊的實(shí)現(xiàn)。 Vert.x Kue ...

    Kerr1Gan 評(píng)論0 收藏0
  • Vert.x Blueprint 系列教程(二) | 開(kāi)發(fā)基于消息的應(yīng)用 - Vert.x Kue

    摘要:本文章是藍(lán)圖系列的第二篇教程。這就是請(qǐng)求回應(yīng)模式。好多屬性我們一個(gè)一個(gè)地解釋一個(gè)序列,作為的地址任務(wù)的編號(hào)任務(wù)的類(lèi)型任務(wù)攜帶的數(shù)據(jù),以類(lèi)型表示任務(wù)優(yōu)先級(jí),以枚舉類(lèi)型表示。默認(rèn)優(yōu)先級(jí)為正常任務(wù)的延遲時(shí)間,默認(rèn)是任務(wù)狀態(tài),以枚舉類(lèi)型表示。 本文章是 Vert.x 藍(lán)圖系列 的第二篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程 Vert.x B...

    elina 評(píng)論0 收藏0
  • Vert.x Blueprint 系列教程(三) | Micro-Shop 微服務(wù)應(yīng)用實(shí)戰(zhàn)

    摘要:本教程是藍(lán)圖系列的第三篇教程,對(duì)應(yīng)的版本為。提供了一個(gè)服務(wù)發(fā)現(xiàn)模塊用于發(fā)布和獲取服務(wù)記錄。前端此微服務(wù)的前端部分,目前已整合至組件中。監(jiān)視儀表板用于監(jiān)視微服務(wù)系統(tǒng)的狀態(tài)以及日志統(tǒng)計(jì)數(shù)據(jù)的查看。而服務(wù)則負(fù)責(zé)發(fā)布其它服務(wù)如服務(wù)或消息源并且部署。 本文章是 Vert.x 藍(lán)圖系列 的第三篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待辦事項(xiàng)服務(wù)開(kāi)發(fā)教程 Vert....

    QiShare 評(píng)論0 收藏0
  • 「Odoo 基礎(chǔ)教程系列」第三篇——從 Todo 應(yīng)用開(kāi)始(2)

    摘要:現(xiàn)在我們來(lái)給待辦事項(xiàng)增加一個(gè)緊急程度的字段,用來(lái)表示當(dāng)前任務(wù)的優(yōu)先級(jí)。此處我們還給這個(gè)字段添加了默認(rèn)值,表示當(dāng)一個(gè)待辦事項(xiàng)被創(chuàng)建后,如果沒(méi)有指定緊急程度,將默認(rèn)是待辦狀態(tài)。這篇教程中的代碼同樣會(huì)更新在我的倉(cāng)庫(kù)中。 showImg(https://segmentfault.com/img/bVbfv3E?w=1330&h=912); 在這篇教程里我們將會(huì)了解到 Odoo 模型里的一些其他...

    iflove 評(píng)論0 收藏0
  • 「Odoo 基礎(chǔ)教程系列」第四篇——從 Todo 應(yīng)用開(kāi)始(3)

    摘要:在這一篇教程中,將會(huì)涉及到外鍵字段,可以將兩個(gè)模型關(guān)聯(lián)起來(lái),然后很方便地獲取到對(duì)應(yīng)的數(shù)據(jù)。關(guān)聯(lián)字段這一小節(jié)里,我們會(huì)給待辦事項(xiàng)加上分類(lèi),并且這個(gè)分類(lèi)可以讓用戶自己創(chuàng)建維護(hù)。今天這篇教程的內(nèi)容就先到這里了,教程中的代碼會(huì)更新在我的倉(cāng)庫(kù)中。 showImg(https://segmentfault.com/img/bVbfzvt?w=1280&h=795); 在這一篇教程中,將會(huì)涉及到外鍵...

    HollisChuang 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<