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

相关文章
相关标签/搜索