资讯专栏INFORMATION COLUMN

Vert.x Blueprint 系列教程(一) | 待办事项服务开发教程

frank_fun / 3569人阅读

摘要:本文章是蓝图系列的第一篇教程。是事件驱动的,同时也是非阻塞的。是一组负责分发和处理事件的线程。注意,我们绝对不能去阻塞线程,否则事件的处理过程会被阻塞,我们的应用就失去了响应能力。每个负责处理请求并且写入回应结果。

本文章是 Vert.x 蓝图系列 的第一篇教程。全系列:

Vert.x Blueprint 系列教程(一) | 待办事项服务开发教程

Vert.x Blueprint 系列教程(二) | 开发基于消息的应用 - Vert.x Kue 教程

Vert.x Blueprint 系列教程(三) | Micro-Shop 微服务应用实战

本系列已发布至Vert.x官网:Vert.x Blueprint Tutorials

前言

在本教程中,我们会使用Vert.x来一步一步地开发一个REST风格的Web服务 - Todo Backend,你可以把它看作是一个简单的待办事项服务,我们可以自由添加或者取消各种待办事项。

通过本教程,你将会学习到以下的内容:

Vert.x 是什么,以及其基本设计思想

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

如何用 Vert.x Web 来开发REST风格的Web服务

异步编程风格 的应用

如何通过 Vert.x 的各种组件来进行数据的存储操作(如 RedisMySQL

本教程是Vert.x 蓝图系列的第一篇教程,对应的Vert.x版本为3.3.0。本教程中的完整代码已托管至GitHub。

踏入Vert.x之门

朋友,欢迎来到Vert.x的世界!初次听说Vert.x,你一定会非常好奇:这是啥?让我们来看一下Vert.x的官方解释:

</>复制代码

  1. 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之类的构建工具。我们的项目目录里需要有:

src/main/java 文件夹(源码目录)

src/test/java 文件夹(测试目录)

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

</>复制代码

  1. .
  2. ├── build.gradle
  3. ├── settings.gradle
  4. ├── src
  5. │ ├── main
  6. │ │ └── java
  7. │ └── test
  8. │ └── java

我们首先来创建 build.gradle 文件,这是Gradle对应的配置文件:

</>复制代码

  1. apply plugin: "java"
  2. targetCompatibility = 1.8
  3. sourceCompatibility = 1.8
  4. repositories {
  5. mavenCentral()
  6. mavenLocal()
  7. }
  8. dependencies {
  9. compile "io.vertx:vertx-core:3.3.0"
  10. compile "io.vertx:vertx-web:3.3.0"
  11. testCompile "io.vertx:vertx-unit:3.3.0"
  12. testCompile group: "junit", name: "junit", version: "4.12"
  13. }

你可能不是很熟悉Gradle,这不要紧。我们来解释一下:

我们将 targetCompatibilitysourceCompatibility 这两个值都设为1.8,代表目标Java版本是Java 8。这非常重要,因为Vert.x就是基于Java 8构建的。

dependencies中,我们声明了我们需要的依赖。vertx-corevert-web 用于开发REST API。

注: 若国内用户出现用Gradle解析依赖非常缓慢的情况,可以尝试使用开源中国Maven镜像代替默认的镜像(有的时候速度比较快)。只要在build.gradle中配置即可:

</>复制代码

  1. repositories {
  2. maven {
  3. url "http://maven.oschina.net/content/groups/public/"
  4. }
  5. mavenLocal()
  6. }

搞定build.gradle以后,我们开始写代码!

待办事项对象

首先我们需要创建我们的数据实体对象 - Todo 实体。在io.vertx.blueprint.todolist.entity包下创建Todo类,并且编写以下代码:

</>复制代码

  1. package io.vertx.blueprint.todolist.entity;
  2. import io.vertx.codegen.annotations.DataObject;
  3. import io.vertx.core.json.JsonObject;
  4. import java.util.concurrent.atomic.AtomicInteger;
  5. @DataObject(generateConverter = true)
  6. public class Todo {
  7. private static final AtomicInteger acc = new AtomicInteger(0); // counter
  8. private int id;
  9. private String title;
  10. private Boolean completed;
  11. private Integer order;
  12. private String url;
  13. public Todo() {
  14. }
  15. public Todo(Todo other) {
  16. this.id = other.id;
  17. this.title = other.title;
  18. this.completed = other.completed;
  19. this.order = other.order;
  20. this.url = other.url;
  21. }
  22. public Todo(JsonObject obj) {
  23. TodoConverter.fromJson(obj, this); // 还未生成Converter的时候需要先注释掉,生成过后再取消注释
  24. }
  25. public Todo(String jsonStr) {
  26. TodoConverter.fromJson(new JsonObject(jsonStr), this);
  27. }
  28. public Todo(int id, String title, Boolean completed, Integer order, String url) {
  29. this.id = id;
  30. this.title = title;
  31. this.completed = completed;
  32. this.order = order;
  33. this.url = url;
  34. }
  35. public JsonObject toJson() {
  36. JsonObject json = new JsonObject();
  37. TodoConverter.toJson(this, json);
  38. return json;
  39. }
  40. public int getId() {
  41. return id;
  42. }
  43. public void setId(int id) {
  44. this.id = id;
  45. }
  46. public void setIncId() {
  47. this.id = acc.incrementAndGet();
  48. }
  49. public static int getIncId() {
  50. return acc.get();
  51. }
  52. public static void setIncIdWith(int n) {
  53. acc.set(n);
  54. }
  55. public String getTitle() {
  56. return title;
  57. }
  58. public void setTitle(String title) {
  59. this.title = title;
  60. }
  61. public Boolean isCompleted() {
  62. return getOrElse(completed, false);
  63. }
  64. public void setCompleted(Boolean completed) {
  65. this.completed = completed;
  66. }
  67. public Integer getOrder() {
  68. return getOrElse(order, 0);
  69. }
  70. public void setOrder(Integer order) {
  71. this.order = order;
  72. }
  73. public String getUrl() {
  74. return url;
  75. }
  76. public void setUrl(String url) {
  77. this.url = url;
  78. }
  79. @Override
  80. public boolean equals(Object o) {
  81. if (this == o) return true;
  82. if (o == null || getClass() != o.getClass()) return false;
  83. Todo todo = (Todo) o;
  84. if (id != todo.id) return false;
  85. if (!title.equals(todo.title)) return false;
  86. if (completed != null ? !completed.equals(todo.completed) : todo.completed != null) return false;
  87. return order != null ? order.equals(todo.order) : todo.order == null;
  88. }
  89. @Override
  90. public int hashCode() {
  91. int result = id;
  92. result = 31 * result + title.hashCode();
  93. result = 31 * result + (completed != null ? completed.hashCode() : 0);
  94. result = 31 * result + (order != null ? order.hashCode() : 0);
  95. return result;
  96. }
  97. @Override
  98. public String toString() {
  99. return "Todo -> {" +
  100. "id=" + id +
  101. ", title="" + title + """ +
  102. ", completed=" + completed +
  103. ", order=" + order +
  104. ", url="" + url + """ +
  105. "}";
  106. }
  107. private T getOrElse(T value, T defaultValue) {
  108. return value == null ? defaultValue : value;
  109. }
  110. public Todo merge(Todo todo) {
  111. return new Todo(id,
  112. getOrElse(todo.title, title),
  113. getOrElse(todo.completed, completed),
  114. getOrElse(todo.order, order),
  115. url);
  116. }
  117. }

我们的 Todo 实体对象由序号id、标题title、次序order、地址url以及代表待办事项是否完成的一个标识complete组成。我们可以把它看作是一个简单的Java Bean。它可以被编码成JSON格式的数据,我们在后边会大量使用JSON(事实上,在Vert.x中JSON非常普遍)。同时注意到我们给Todo类加上了一个注解:@DataObject,这是用于生成JSON转换类的注解。

</>复制代码

  1. DataObject 注解
    @DataObject 注解的实体类需要满足以下条件:拥有一个拷贝构造函数以及一个接受一个 JsonObject 对象的构造函数。

我们利用Vert.x Codegen来自动生成JSON转换类。我们需要在build.gradle中添加依赖:

</>复制代码

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

同时,我们需要在io.vertx.blueprint.todolist.entity包中添加package-info.java文件来指引Vert.x Codegen生成代码:

</>复制代码

  1. /**
  2. * Indicates that this module contains classes that need to be generated / processed.
  3. */
  4. @ModuleGen(name = "vertx-blueprint-todo-entity", groupPackage = "io.vertx.blueprint.todolist.entity")
  5. package io.vertx.blueprint.todolist.entity;
  6. import io.vertx.codegen.annotations.ModuleGen;

Vert.x Codegen本质上是一个注解处理器(annotation processing tool),因此我们还需要在build.gradle中配置apt。往里面添加以下代码:

</>复制代码

  1. task annotationProcessing(type: JavaCompile, group: "build") {
  2. source = sourceSets.main.java
  3. classpath = configurations.compile
  4. destinationDir = project.file("src/main/generated")
  5. options.compilerArgs = [
  6. "-proc:only",
  7. "-processor", "io.vertx.codegen.CodeGenProcessor",
  8. "-AoutputDirectory=${destinationDir.absolutePath}"
  9. ]
  10. }
  11. sourceSets {
  12. main {
  13. java {
  14. srcDirs += "src/main/generated"
  15. }
  16. }
  17. }
  18. compileJava {
  19. targetCompatibility = 1.8
  20. sourceCompatibility = 1.8
  21. dependsOn annotationProcessing
  22. }

这样,每次我们在编译项目的时候,Vert.x Codegen都会自动检测含有 @DataObject 注解的类并且根据配置生成JSON转换类。在本例中,我们应该会得到一个 TodoConverter 类,然后我们可以在Todo类中使用它。

Verticle

下面我们来写我们的应用组件。在io.vertx.blueprint.todolist.verticles包中创建SingleApplicationVerticle类,并编写以下代码:

</>复制代码

  1. package io.vertx.blueprint.todolist.verticles;
  2. import io.vertx.core.AbstractVerticle;
  3. import io.vertx.core.Future;
  4. import io.vertx.redis.RedisClient;
  5. import io.vertx.redis.RedisOptions;
  6. public class SingleApplicationVerticle extends AbstractVerticle {
  7. private static final String HTTP_HOST = "0.0.0.0";
  8. private static final String REDIS_HOST = "127.0.0.1";
  9. private static final int HTTP_PORT = 8082;
  10. private static final int REDIS_PORT = 6379;
  11. private RedisClient redis;
  12. @Override
  13. public void start(Future future) throws Exception {
  14. // TODO with start...
  15. }
  16. }

我们的SingleApplicationVerticle类继承了AbstractVerticle抽象类。那么什么是 Verticle 呢?在Vert.x中,一个Verticle代表应用的某一组件。我们可以通过部署Verticle来运行这些组件。如果你了解 Actor 模型的话,你会发现它和Actor非常类似。

Verticle被部署的时候,其start方法会被调用。我们注意到这里的start方法接受一个类型为Future的参数,这代表了这是一个异步的初始化方法。这里的Future代表着Verticle的初始化过程是否完成。你可以通过调用Future的complete方法来代表初始化过程完成,或者fail方法代表初始化过程失败。

现在我们Verticle的轮廓已经搞好了,那么下一步也就很明了了 - 创建HTTP Client并且配置路由,处理HTTP请求。

Vert.x Web与REST API 创建HTTP服务端并配置路由

我们来给start方法加点东西:

</>复制代码

  1. @Override
  2. public void start(Future future) throws Exception {
  3. initData();
  4. Router router = Router.router(vertx); // <1>
  5. // CORS support
  6. Set allowHeaders = new HashSet<>();
  7. allowHeaders.add("x-requested-with");
  8. allowHeaders.add("Access-Control-Allow-Origin");
  9. allowHeaders.add("origin");
  10. allowHeaders.add("Content-Type");
  11. allowHeaders.add("accept");
  12. Set allowMethods = new HashSet<>();
  13. allowMethods.add(HttpMethod.GET);
  14. allowMethods.add(HttpMethod.POST);
  15. allowMethods.add(HttpMethod.DELETE);
  16. allowMethods.add(HttpMethod.PATCH);
  17. router.route().handler(CorsHandler.create("*") // <2>
  18. .allowedHeaders(allowHeaders)
  19. .allowedMethods(allowMethods));
  20. router.route().handler(BodyHandler.create()); // <3>
  21. // TODO:routes
  22. vertx.createHttpServer() // <4>
  23. .requestHandler(router::accept)
  24. .listen(PORT, HOST, result -> {
  25. if (result.succeeded())
  26. future.complete();
  27. else
  28. future.fail(result.cause());
  29. });
  30. }

(⊙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

</>复制代码

  1. 路径参数

在URL中,我们可以通过:name的形式定义路径参数。当处理请求的时候,Vert.x会自动获取这些路径参数并允许我们访问它们。拿我们的路由举个例子,/todos/19todoId 映射为 19

首先我们先在 io.vertx.blueprint.todolist 包下创建一个Constants类用于存储各种全局常量(当然也可以放到其对应的类中):

</>复制代码

  1. package io.vertx.blueprint.todolist;
  2. public final class Constants {
  3. private Constants() {}
  4. /** API Route */
  5. public static final String API_GET = "/todos/:todoId";
  6. public static final String API_LIST_ALL = "/todos";
  7. public static final String API_CREATE = "/todos";
  8. public static final String API_UPDATE = "/todos/:todoId";
  9. public static final String API_DELETE = "/todos/:todoId";
  10. public static final String API_DELETE_ALL = "/todos";
  11. }

然后我们将start方法中的TODO标识处替换为以下的内容:

</>复制代码

  1. // routes
  2. router.get(Constants.API_GET).handler(this::handleGetTodo);
  3. router.get(Constants.API_LIST_ALL).handler(this::handleGetAll);
  4. router.post(Constants.API_CREATE).handler(this::handleCreateTodo);
  5. router.patch(Constants.API_UPDATE).handler(this::handleUpdateTodo);
  6. router.delete(Constants.API_DELETE).handler(this::handleDeleteOne);
  7. router.delete(Constants.API_DELETE_ALL).handler(this::handleDeleteAll);

代码很直观、明了。我们用对应的方法(如get,post,patch等等)将路由路径与路由器绑定,并且我们调用handler方法给每个路由绑定上对应的Handler,接受的Handler类型为Handler。这里我们分别绑定了六个方法引用,它们的形式都类似于这样:

</>复制代码

  1. private void handleRequest(RoutingContext context) {
  2. // ...
  3. }

我们将在稍后实现这六个方法,这也是我们待办事项服务逻辑的核心。

异步方法模式

我们之前提到过,Vert.x是 异步、非阻塞的 。每一个异步的方法总会接受一个 Handler 参数作为回调函数,当对应的操作完成时会调用接受的Handler,这是异步方法的一种实现。还有一种等价的实现是返回Future对象:

</>复制代码

  1. void doAsync(A a, B b, Handler handler);
  2. // 这两种实现等价
  3. Future doAsync(A a, B b);

其中,Future 对象代表着一个操作的结果,这个操作可能还没有进行,可能正在进行,可能成功也可能失败。当操作完成时,Future对象会得到对应的结果。我们也可以通过setHandler方法给Future绑定一个Handler,当Future被赋予结果的时候,此Handler会被调用。

</>复制代码

  1. Future future = doAsync(A a, B b);
  2. future.setHandler(r -> {
  3. if (r.failed()) {
  4. // 处理失败
  5. } else {
  6. // 操作结果
  7. }
  8. });

Vert.x中大多数异步方法都是基于Handler的。而在本教程中,这两种异步模式我们都会接触到。

待办事项逻辑实现

现在是时候来实现我们的待办事项业务逻辑了!这里我们使用 Redis 作为数据持久化存储。有关Redis的详细介绍请参照Redis 官方网站。Vert.x给我们提供了一个组件—— Vert.x-redis,允许我们以异步的形式操作Redis数据。

</>复制代码

  1. 如何安装Redis? | 请参照Redis官方网站上详细的安装指南。

Vert.x Redis

Vert.x Redis允许我们以异步的形式操作Redis数据。我们首先需要在build.gradle中添加以下依赖:

</>复制代码

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

我们通过RedisClient对象来操作Redis中的数据,因此我们定义了一个类成员redis。在使用RedisClient之前,我们首先需要与Redis建立连接,并且需要配置(以RedisOptions的形式),后边我们再讲需要配置哪些东西。

我们来实现 initData 方法用于初始化 RedisClient 并且测试连接:

</>复制代码

  1. private void initData() {
  2. RedisOptions config = new RedisOptions()
  3. .setHost(config().getString("redis.host", REDIS_HOST)) // redis host
  4. .setPort(config().getInteger("redis.port", REDIS_PORT)); // redis port
  5. this.redis = RedisClient.create(vertx, config); // create redis client
  6. redis.hset(Constants.REDIS_TODO_KEY, "24", Json.encodePrettily( // test connection
  7. new Todo(24, "Something to do...", false, 1, "todo/ex")), res -> {
  8. if (res.failed()) {
  9. System.err.println("[Error] Redis service is not running!");
  10. res.cause().printStackTrace();
  11. }
  12. });
  13. }

当我们在加载Verticle的时候,我们会首先调用initData方法,这样可以保证RedisClient可以被正常创建。

存储格式

我们知道,Redis支持各种格式的数据,并且支持多种方式存储(如listhash map等)。这里我们将我们的待办事项存储在 哈希表(map) 中。我们使用待办事项的id作为key,JSON格式的待办事项数据作为value。同时,我们的哈希表本身也要有个key,我们把它命名为 VERT_TODO,并且存储到Constants类中:

</>复制代码

  1. public static final String REDIS_TODO_KEY = "VERT_TODO";

正如我们之前提到的,我们利用了生成的JSON数据转换类来实现Todo实体与JSON数据之间的转换(通过几个构造函数),在后面实现待办事项服务的时候可以广泛利用。

获取/获取所有待办事项

我们首先来实现获取待办事项的逻辑。正如我们之前所提到的,我们的处理逻辑方法需要接受一个RoutingContext类型的参数。我们看一下获取某一待办事项的逻辑方法(handleGetTodo):

</>复制代码

  1. private void handleGetTodo(RoutingContext context) {
  2. String todoID = context.request().getParam("todoId"); // (1)
  3. if (todoID == null)
  4. sendError(400, context.response()); // (2)
  5. else {
  6. redis.hget(Constants.REDIS_TODO_KEY, todoID, x -> { // (3)
  7. if (x.succeeded()) {
  8. String result = x.result();
  9. if (result == null)
  10. sendError(404, context.response());
  11. else {
  12. context.response()
  13. .putHeader("content-type", "application/json")
  14. .end(result); // (4)
  15. }
  16. } else
  17. sendError(503, context.response());
  18. });
  19. }
  20. }

首先我们先通过getParam方法获取路径参数todoId (1)。我们需要检测路径参数获取是否成功,如果不成功就返回 400 Bad Request 错误 (2)。这里我们写一个函数封装返回错误response的逻辑:

</>复制代码

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

这里面,end方法是非常重要的。只有我们调用end方法时,对应的HTTP Response才能被发送回客户端。

再回到handleGetTodo方法中。如果我们成功获取到了todoId,我们可以通过hget操作从Redis中获取对应的待办事项 (3)。hget代表通过key从对应的哈希表中获取对应的value,我们来看一下hget函数的定义:

</>复制代码

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

第一个参数key对应哈希表的key,第二个参数field代表待办事项的key,第三个参数代表当获取操作成功时对应的回调。在Handler中,我们首先检查操作是否成功,如果不成功就返回503错误。如果成功了,我们就可以获取操作的结果了。结果是null的话,说明Redis中没有对应的待办事项,因此我们返回404 Not Found代表不存在。如果结果存在,那么我们就可以通过end方法将其写入response中 (4)。注意到我们所有的RESTful API都返回JSON格式的数据,所以我们将content-type头设为JSON

获取所有待办事项的逻辑handleGetAllhandleGetTodo大体上类似,但实现上有些许不同:

</>复制代码

  1. private void handleGetAll(RoutingContext context) {
  2. redis.hvals(Constants.REDIS_TODO_KEY, res -> { // (1)
  3. if (res.succeeded()) {
  4. String encoded = Json.encodePrettily(res.result().stream() // (2)
  5. .map(x -> new Todo((String) x))
  6. .collect(Collectors.toList()));
  7. context.response()
  8. .putHeader("content-type", "application/json")
  9. .end(encoded); // (3)
  10. } else
  11. sendError(503, context.response());
  12. });
  13. }

这里我们通过hvals操作 (1) 来获取某个哈希表中的所有数据(以JSON数组的形式返回,即JsonArray对象)。在Handler中我们还是像之前那样先检查操作是否成功。如果成功的话我们就可以将结果写入response了。注意这里我们不能直接将返回的JsonArray写入response。想象一下返回的JsonArray包括着待办事项的key以及对应的JSON数据(字符串形式),因此此时每个待办事项对应的JSON数据都被转义了,所以我们需要先把这些转义过的JSON数据转换成实体对象,再重新编码。

我们这里采用了一种响应式编程思想的方法。首先我们了解到JsonArray类继承了Iterable接口(是不是感觉它很像List呢?),因此我们可以通过stream方法将其转化为Stream对象。注意这里的Stream可不是传统意义上讲的输入输出流(I/O stream),而是数据流(data flow)。我们需要对数据流进行一系列的变换处理操作,这就是响应式编程的思想(也有点函数式编程的思想)。我们将数据流中的每个字符串数据转换为Todo实体对象,这个过程是通过map算子实现的。我们这里就不深入讨论map算子了,但它在函数式编程中非常重要。在map过后,我们通过collect方法将数据流“归约”成List。现在我们就可以通过Json.encodePrettily方法对得到的list进行编码了,转换成JSON格式的数据。最后我们将转换后的结果写入到response中 (3)。

创建待办事项

经过了上面两个业务逻辑实现的过程,你应该开始熟悉Vert.x了~现在我们来实现创建待办事项的逻辑:

</>复制代码

  1. private void handleCreateTodo(RoutingContext context) {
  2. try {
  3. final Todo todo = wrapObject(new Todo(context.getBodyAsString()), context);
  4. final String encoded = Json.encodePrettily(todo);
  5. redis.hset(Constants.REDIS_TODO_KEY, String.valueOf(todo.getId()),
  6. encoded, res -> {
  7. if (res.succeeded())
  8. context.response()
  9. .setStatusCode(201)
  10. .putHeader("content-type", "application/json")
  11. .end(encoded);
  12. else
  13. sendError(503, context.response());
  14. });
  15. } catch (DecodeException e) {
  16. sendError(400, context.response());
  17. }
  18. }

首先我们通过context.getBodyAsString()方法来从请求正文中获取JSON数据并转换成Todo实体对象 (1)。这里我们包装了一个处理Todo实例的方法,用于给其添加必要的信息(如URL):

</>复制代码

  1. private Todo wrapObject(Todo todo, RoutingContext context) {
  2. int id = todo.getId();
  3. if (id > Todo.getIncId()) {
  4. Todo.setIncIdWith(id);
  5. } else if (id == 0)
  6. todo.setIncId();
  7. todo.setUrl(context.request().absoluteURI() + "/" + todo.getId());
  8. return todo;
  9. }

对于没有ID(或者为默认ID)的待办事项,我们会给它分配一个ID。这里我们采用了自增ID的策略,通过AtomicInteger来实现。

然后我们通过Json.encodePrettily方法将我们的Todo实例再次编码成JSON格式的数据 (2)。接下来我们利用hset函数将待办事项实例插入到对应的哈希表中 (3)。如果插入成功,返回 201 状态码 (4)。

</>复制代码

  1. 201 状态码?

| 正如你所看到的那样,我们将状态码设为201,这代表CREATED(已创建)。另外,如果不指定状态码的话,Vert.x Web默认将状态码设为 200 OK

同时,我们接收到的HTTP请求首部可能格式不正确,因此我们需要在方法中捕获DecodeException异常。这样一旦捕获到DecodeException异常,我们就返回400 Bad Request状态码。

更新待办事项

如果你想改变你的计划,你就需要更新你的待办事项。我们来实现更新待办事项的逻辑,它有点小复杂(或者说是,繁琐?):

</>复制代码

  1. // PATCH /todos/:todoId
  2. private void handleUpdateTodo(RoutingContext context) {
  3. try {
  4. String todoID = context.request().getParam("todoId"); // (1)
  5. final Todo newTodo = new Todo(context.getBodyAsString()); // (2)
  6. // handle error
  7. if (todoID == null || newTodo == null) {
  8. sendError(400, context.response());
  9. return;
  10. }
  11. redis.hget(Constants.REDIS_TODO_KEY, todoID, x -> { // (3)
  12. if (x.succeeded()) {
  13. String result = x.result();
  14. if (result == null)
  15. sendError(404, context.response()); // (4)
  16. else {
  17. Todo oldTodo = new Todo(result);
  18. String response = Json.encodePrettily(oldTodo.merge(newTodo)); // (5)
  19. redis.hset(Constants.REDIS_TODO_KEY, todoID, response, res -> { // (6)
  20. if (res.succeeded()) {
  21. context.response()
  22. .putHeader("content-type", "application/json")
  23. .end(response); // (7)
  24. }
  25. });
  26. }
  27. } else
  28. sendError(503, context.response());
  29. });
  30. } catch (DecodeException e) {
  31. sendError(400, context.response());
  32. }
  33. }

唔。。。一大长串代码诶。。。我们来看一下。首先我们从 RoutingContext 中获取路径参数 todoId (1),这是我们想要更改待办事项对应的id。然后我们从请求正文中获取新的待办事项数据 (2)。这一步也有可能抛出 DecodeException 异常因此我们也需要去捕获它。要更新待办事项,我们需要先通过hget函数获取之前的待办事项 (3),检查其是否存在。获取旧的待办事项之后,我们调用之前在Todo类中实现的merge方法将旧待办事项与新待办事项整合到一起 (5),然后编码成JSON格式的数据。然后我们通过hset函数更新对应的待办事项 (6)(hset表示如果不存在就插入,存在就更新)。操作成功的话,返回 200 OK 状态。

这就是更新待办事项的逻辑~要有耐心哟,我们马上就要见到胜利的曙光了~下面我们来实现删除待办事项的逻辑。

删除/删除全部待办事项

删除待办事项的逻辑非常简单。我们利用hdel函数来删除某一待办事项,用del函数删掉所有待办事项(实际上是直接把那个哈希表给删了)。如果删除操作成功,返回204 No Content 状态。

这里直接给出代码:

</>复制代码

  1. private void handleDeleteOne(RoutingContext context) {
  2. String todoID = context.request().getParam("todoId");
  3. redis.hdel(Constants.REDIS_TODO_KEY, todoID, res -> {
  4. if (res.succeeded())
  5. context.response().setStatusCode(204).end();
  6. else
  7. sendError(503, context.response());
  8. });
  9. }
  10. private void handleDeleteAll(RoutingContext context) {
  11. redis.del(Constants.REDIS_TODO_KEY, res -> {
  12. if (res.succeeded())
  13. context.response().setStatusCode(204).end();
  14. else
  15. sendError(503, context.response());
  16. });
  17. }

啊哈!我们实现待办事项服务的Verticle已经完成咯~一颗赛艇!但是我们该如何去运行我们的Verticle呢?答案是,我们需要 部署并运行 我们的Verticle。还好Vert.x提供了一个运行Verticle的辅助工具:Vert.x Launcher,让我们来看看如何利用它。

将应用与Vert.x Launcher一起打包

要通过Vert.x Launcher来运行Verticle,我们需要在build.gradle中配置一下:

</>复制代码

  1. jar {
  2. // by default fat jar
  3. archiveName = "vertx-blueprint-todo-backend-fat.jar"
  4. from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
  5. manifest {
  6. attributes "Main-Class": "io.vertx.core.Launcher"
  7. attributes "Main-Verticle": "io.vertx.blueprint.todolist.verticles.SingleApplicationVerticle"
  8. }
  9. }

jar区块中,我们配置Gradle使其生成 fat-jar,并指定启动类。fat-jar 是一个给Vert.x应用打包的简便方法,它直接将我们的应用连同所有的依赖都给打包到jar包中去了,这样我们可以直接通过jar包运行我们的应用而不必再指定依赖的 CLASSPATH

我们将Main-Class属性设为io.vertx.core.Launcher,这样就可以通过Vert.x Launcher来启动对应的Verticle了。另外我们需要将Main-Verticle属性设为我们想要部署的Verticle的类名(全名)。

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

</>复制代码

  1. gradle build
运行我们的服务

万事俱备,只欠东风。是时候运行我们的待办事项服务了!首先我们先启动Redis服务:

</>复制代码

  1. redis-server

然后运行服务:

</>复制代码

  1. 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

</>复制代码

  1. sczyh30@sczyh30-workshop:~$ curl http://127.0.0.1:8082/todos
  2. [ {
  3. "id" : 20578623,
  4. "title" : "blah",
  5. "completed" : false,
  6. "order" : 95,
  7. "url" : "http://127.0.0.1:8082/todos/20578623"
  8. }, {
  9. "id" : 1744802607,
  10. "title" : "blah",
  11. "completed" : false,
  12. "order" : 523,
  13. "url" : "http://127.0.0.1:8082/todos/1744802607"
  14. }, {
  15. "id" : 981337975,
  16. "title" : "blah",
  17. "completed" : false,
  18. "order" : 95,
  19. "url" : "http://127.0.0.1:8082/todos/981337975"
  20. } ]
将服务与控制器分离

啊哈~我们的待办事项服务已经可以正常运行了,但是回头再来看看 SingleApplicationVerticle 类的代码,你会发现它非常混乱,待办事项业务逻辑与控制器混杂在一起,让这个类非常的庞大,并且这也不利于我们服务的扩展。根据面向对象解耦的思想,我们需要将控制器部分与业务逻辑部分分离。

用Future实现异步服务

下面我们来设计我们的业务逻辑层。就像我们之前提到的那样,我们的服务需要是异步的,因此这些服务的方法要么需要接受一个Handler参数作为回调,要么需要返回一个Future对象。但是想象一下很多个Handler混杂在一起嵌套的情况,你会陷入 回调地狱,这是非常糟糕的。因此,这里我们用Future实现我们的待办事项服务。

io.vertx.blueprint.todolist.service 包下创建 TodoService 接口并且编写以下代码:

</>复制代码

  1. package io.vertx.blueprint.todolist.service;
  2. import io.vertx.blueprint.todolist.entity.Todo;
  3. import io.vertx.core.Future;
  4. import java.util.List;
  5. import java.util.Optional;
  6. public interface TodoService {
  7. Future initData(); // 初始化数据(或数据库)
  8. Future insert(Todo todo);
  9. Future> getAll();
  10. Future> getCertain(String todoID);
  11. Future update(String todoId, Todo newTodo);
  12. Future delete(String todoId);
  13. Future deleteAll();
  14. }

注意到getCertain方法返回一个Future>对象。那么Optional是啥呢?它封装了一个可能为空的对象。因为数据库里面可能没有与我们给定的todoId相对应的待办事项,查询的结果可能为空,因此我们给它包装上 OptionalOptional 可以避免万恶的 NullPointerException,并且它在函数式编程中用途特别广泛(在Haskell中对应 Maybe Monad)。

既然我们已经设计好我们的异步服务接口了,让我们来重构原先的Verticle吧!

开始重构!

我们创建一个新的Verticle。在 io.vertx.blueprint.todolist.verticles 包中创建 TodoVerticle 类,并编写以下代码:

</>复制代码

  1. package io.vertx.blueprint.todolist.verticles;
  2. import io.vertx.blueprint.todolist.Constants;
  3. import io.vertx.blueprint.todolist.entity.Todo;
  4. import io.vertx.blueprint.todolist.service.TodoService;
  5. import io.vertx.core.AbstractVerticle;
  6. import io.vertx.core.AsyncResult;
  7. import io.vertx.core.Future;
  8. import io.vertx.core.Handler;
  9. import io.vertx.core.http.HttpMethod;
  10. import io.vertx.core.http.HttpServerResponse;
  11. import io.vertx.core.json.DecodeException;
  12. import io.vertx.core.json.Json;
  13. import io.vertx.ext.web.Router;
  14. import io.vertx.ext.web.RoutingContext;
  15. import io.vertx.ext.web.handler.BodyHandler;
  16. import io.vertx.ext.web.handler.CorsHandler;
  17. import java.util.HashSet;
  18. import java.util.Random;
  19. import java.util.Set;
  20. import java.util.function.Consumer;
  21. public class TodoVerticle extends AbstractVerticle {
  22. private static final String HOST = "0.0.0.0";
  23. private static final int PORT = 8082;
  24. private TodoService service;
  25. private void initData() {
  26. // TODO
  27. }
  28. @Override
  29. public void start(Future future) throws Exception {
  30. Router router = Router.router(vertx);
  31. // CORS support
  32. Set allowHeaders = new HashSet<>();
  33. allowHeaders.add("x-requested-with");
  34. allowHeaders.add("Access-Control-Allow-Origin");
  35. allowHeaders.add("origin");
  36. allowHeaders.add("Content-Type");
  37. allowHeaders.add("accept");
  38. Set allowMethods = new HashSet<>();
  39. allowMethods.add(HttpMethod.GET);
  40. allowMethods.add(HttpMethod.POST);
  41. allowMethods.add(HttpMethod.DELETE);
  42. allowMethods.add(HttpMethod.PATCH);
  43. router.route().handler(BodyHandler.create());
  44. router.route().handler(CorsHandler.create("*")
  45. .allowedHeaders(allowHeaders)
  46. .allowedMethods(allowMethods));
  47. // routes
  48. router.get(Constants.API_GET).handler(this::handleGetTodo);
  49. router.get(Constants.API_LIST_ALL).handler(this::handleGetAll);
  50. router.post(Constants.API_CREATE).handler(this::handleCreateTodo);
  51. router.patch(Constants.API_UPDATE).handler(this::handleUpdateTodo);
  52. router.delete(Constants.API_DELETE).handler(this::handleDeleteOne);
  53. router.delete(Constants.API_DELETE_ALL).handler(this::handleDeleteAll);
  54. vertx.createHttpServer()
  55. .requestHandler(router::accept)
  56. .listen(PORT, HOST, result -> {
  57. if (result.succeeded())
  58. future.complete();
  59. else
  60. future.fail(result.cause());
  61. });
  62. initData();
  63. }
  64. private void handleCreateTodo(RoutingContext context) {
  65. // TODO
  66. }
  67. private void handleGetTodo(RoutingContext context) {
  68. // TODO
  69. }
  70. private void handleGetAll(RoutingContext context) {
  71. // TODO
  72. }
  73. private void handleUpdateTodo(RoutingContext context) {
  74. // TODO
  75. }
  76. private void handleDeleteOne(RoutingContext context) {
  77. // TODO
  78. }
  79. private void handleDeleteAll(RoutingContext context) {
  80. // TODO
  81. }
  82. private void sendError(int statusCode, HttpServerResponse response) {
  83. response.setStatusCode(statusCode).end();
  84. }
  85. private void badRequest(RoutingContext context) {
  86. context.response().setStatusCode(400).end();
  87. }
  88. private void notFound(RoutingContext context) {
  89. context.response().setStatusCode(404).end();
  90. }
  91. private void serviceUnavailable(RoutingContext context) {
  92. context.response().setStatusCode(503).end();
  93. }
  94. private Todo wrapObject(Todo todo, RoutingContext context) {
  95. int id = todo.getId();
  96. if (id > Todo.getIncId()) {
  97. Todo.setIncIdWith(id);
  98. } else if (id == 0)
  99. todo.setIncId();
  100. todo.setUrl(context.request().absoluteURI() + "/" + todo.getId());
  101. return todo;
  102. }
  103. }

很熟悉吧?这个Verticle的结构与我们之前的Verticle相类似,这里就不多说了。下面我们来利用我们之前编写的服务接口实现每一个控制器方法。

首先先实现 initData 方法,此方法用于初始化存储结构:

</>复制代码

  1. private void initData() {
  2. final String serviceType = config().getString("service.type", "redis");
  3. switch (serviceType) {
  4. case "jdbc":
  5. service = new JdbcTodoService(vertx, config());
  6. break;
  7. case "redis":
  8. default:
  9. RedisOptions config = new RedisOptions()
  10. .setHost(config().getString("redis.host", "127.0.0.1"))
  11. .setPort(config().getInteger("redis.port", 6379));
  12. service = new RedisTodoService(vertx, config);
  13. }
  14. service.initData().setHandler(res -> {
  15. if (res.failed()) {
  16. System.err.println("[Error] Persistence service is not running!");
  17. res.cause().printStackTrace();
  18. }
  19. });
  20. }

首先我们从配置中获取服务的类型,这里我们有两种类型的服务:redisjdbc,默认是redis。接着我们会根据服务的类型以及对应的配置来创建服务。在这里,我们的配置都是从JSON格式的配置文件中读取,并通过Vert.x Launcher的-conf项加载。后面我们再讲要配置哪些东西。

接着我们给service.initData()方法返回的Future对象绑定了一个Handler,这个Handler将会在Future得到结果的时候被调用。一旦初始化过程失败,错误信息将会显示到终端上。

其它的方法实现也类似,这里就不详细解释了,直接放上代码,非常简洁明了:

</>复制代码

  1. /**
  2. * Wrap the result handler with failure handler (503 Service Unavailable)
  3. */
  4. private Handler> resultHandler(RoutingContext context, Consumer consumer) {
  5. return res -> {
  6. if (res.succeeded()) {
  7. consumer.accept(res.result());
  8. } else {
  9. serviceUnavailable(context);
  10. }
  11. };
  12. }
  13. private void handleCreateTodo(RoutingContext context) {
  14. try {
  15. final Todo todo = wrapObject(new Todo(context.getBodyAsString()), context);
  16. final String encoded = Json.encodePrettily(todo);
  17. service.insert(todo).setHandler(resultHandler(context, res -> {
  18. if (res) {
  19. context.response()
  20. .setStatusCode(201)
  21. .putHeader("content-type", "application/json")
  22. .end(encoded);
  23. } else {
  24. serviceUnavailable(context);
  25. }
  26. }));
  27. } catch (DecodeException e) {
  28. sendError(400, context.response());
  29. }
  30. }
  31. private void handleGetTodo(RoutingContext context) {
  32. String todoID = context.request().getParam("todoId");
  33. if (todoID == null) {
  34. sendError(400, context.response());
  35. return;
  36. }
  37. service.getCertain(todoID).setHandler(resultHandler(context, res -> {
  38. if (!res.isPresent())
  39. notFound(context);
  40. else {
  41. final String encoded = Json.encodePrettily(res.get());
  42. context.response()
  43. .putHeader("content-type", "application/json")
  44. .end(encoded);
  45. }
  46. }));
  47. }
  48. private void handleGetAll(RoutingContext context) {
  49. service.getAll().setHandler(resultHandler(context, res -> {
  50. if (res == null) {
  51. serviceUnavailable(context);
  52. } else {
  53. final String encoded = Json.encodePrettily(res);
  54. context.response()
  55. .putHeader("content-type", "application/json")
  56. .end(encoded);
  57. }
  58. }));
  59. }
  60. private void handleUpdateTodo(RoutingContext context) {
  61. try {
  62. String todoID = context.request().getParam("todoId");
  63. final Todo newTodo = new Todo(context.getBodyAsString());
  64. // handle error
  65. if (todoID == null) {
  66. sendError(400, context.response());
  67. return;
  68. }
  69. service.update(todoID, newTodo)
  70. .setHandler(resultHandler(context, res -> {
  71. if (res == null)
  72. notFound(context);
  73. else {
  74. final String encoded = Json.encodePrettily(res);
  75. context.response()
  76. .putHeader("content-type", "application/json")
  77. .end(encoded);
  78. }
  79. }));
  80. } catch (DecodeException e) {
  81. badRequest(context);
  82. }
  83. }
  84. private Handler> deleteResultHandler(RoutingContext context) {
  85. return res -> {
  86. if (res.succeeded()) {
  87. if (res.result()) {
  88. context.response().setStatusCode(204).end();
  89. } else {
  90. serviceUnavailable(context);
  91. }
  92. } else {
  93. serviceUnavailable(context);
  94. }
  95. };
  96. }
  97. private void handleDeleteOne(RoutingContext context) {
  98. String todoID = context.request().getParam("todoId");
  99. service.delete(todoID)
  100. .setHandler(deleteResultHandler(context));
  101. }
  102. private void handleDeleteAll(RoutingContext context) {
  103. service.deleteAll()
  104. .setHandler(deleteResultHandler(context));
  105. }

是不是和之前的Verticle很相似呢?这里我们还封装了两个Handler生成器:resultHandlerdeleteResultHandler。这两个生成器封装了一些重复的代码,可以减少代码量。

嗯。。。我们的新Verticle写好了,那么是时候去实现具体的业务逻辑了。这里我们会实现两个版本的业务逻辑,分别对应两种存储:RedisMySQL

Vert.x-Redis版本的待办事项服务

之前我们已经实现过一遍Redis版本的服务了,因此你应该对其非常熟悉了。这里我们仅仅解释一个 update 方法,其它的实现都非常类似,代码可以在GitHub上浏览。

Monadic Future

回想一下我们之前写的更新待办事项的逻辑,我们会发现它其实是由两个独立的操作组成 - getinsert(对于Redis来说)。所以呢,我们可不可以复用 getCertaininsert 这两个方法?当然了!因为Future是可组合的,因此我们可以将这两个方法返回的Future组合到一起。是不是非常方便呢?我们来编写此方法:

</>复制代码

  1. @Override
  2. public Future update(String todoId, Todo newTodo) {
  3. return this.getCertain(todoId).compose(old -> { // (1)
  4. if (old.isPresent()) {
  5. Todo fnTodo = old.get().merge(newTodo);
  6. return this.insert(fnTodo)
  7. .map(r -> r ? fnTodo : null); // (2)
  8. } else {
  9. return Future.succeededFuture(); // (3)
  10. }
  11. });
  12. }

首先我们调用了getCertain方法,此方法返回一个Future>对象。同时我们使用compose函数将此方法返回的Future与另一个Future进行组合(1),其中compose函数接受一个T => Future类型的lambda。然后我们接着检查旧的待办事项是否存在,如果存在的话,我们将新的待办事项与旧的待办事项相融合,然后更新待办事项。注意到insert方法返回Future类型的Future,因此我们还需要对此Future的结果做变换,这个变换的过程是通过map函数实现的(2)。map函数接受一个T => U类型的lambda。如果旧的待办事项不存在,我们返回一个包含null的Future(3)。最后我们返回组合后的Future对象。

</>复制代码

  1. Future 的本质

在函数式编程中,Future 实际上是一种 Monad。有关Monad的理论较为复杂,这里就不进行阐述了。你可以简单地把它看作是一个可以进行变换(map)和组合(compose)的包装对象。我们把这种特性叫做 Monadic

下面来实现MySQL版本的待办事项服务。

Vert.x-JDBC版本的待办事项服务 JDBC ++ 异步

我们使用Vert.x-JDBC和MySQL来实现JDBC版本的待办事项服务。我们知道,数据库操作都是阻塞操作,很可能会占用不少时间。而Vert.x-JDBC提供了一种异步操作数据库的模式,很神奇吧?所以,在传统JDBC代码下我们要执行SQL语句需要这样:

</>复制代码

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

而在Vert.x JDBC中,我们可以利用回调获取数据:

</>复制代码

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

这种异步操作可以有效避免对数据的等待。当数据获取成功时会自动调用回调函数来执行处理数据的逻辑。

添加依赖

首先我们需要向build.gradle文件中添加依赖:

</>复制代码

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

其中第二个依赖是MySQL的驱动,如果你想使用其他的数据库,你需要自行替换掉这个依赖。

初始化JDBCClient

在Vert.x JDBC中,我们需要从一个JDBCClient对象中获取数据库连接,因此我们来看一下如何创建JDBCClient实例。在io.vertx.blueprint.todolist.service包下创建JdbcTodoService类:

</>复制代码

  1. package io.vertx.blueprint.todolist.service;
  2. import io.vertx.blueprint.todolist.entity.Todo;
  3. import io.vertx.core.Future;
  4. import io.vertx.core.Vertx;
  5. import io.vertx.core.json.JsonArray;
  6. import io.vertx.core.json.JsonObject;
  7. import io.vertx.ext.jdbc.JDBCClient;
  8. import io.vertx.ext.sql.SQLConnection;
  9. import java.util.List;
  10. import java.util.Optional;
  11. import java.util.stream.Collectors;
  12. public class JdbcTodoService implements TodoService {
  13. private final Vertx vertx;
  14. private final JsonObject config;
  15. private final JDBCClient client;
  16. public JdbcTodoService(JsonObject config) {
  17. this(Vertx.vertx(), config);
  18. }
  19. public JdbcTodoService(Vertx vertx, JsonObject config) {
  20. this.vertx = vertx;
  21. this.config = config;
  22. this.client = JDBCClient.createShared(vertx, config);
  23. }
  24. // ...
  25. }

我们使用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中建这样一个表:

</>复制代码

  1. CREATE TABLE `todo` (
  2. `id` INT(11) NOT NULL AUTO_INCREMENT,
  3. `title` VARCHAR(255) DEFAULT NULL,
  4. `completed` TINYINT(1) DEFAULT NULL,
  5. `order` INT(11) DEFAULT NULL,
  6. `url` VARCHAR(255) DEFAULT NULL,
  7. PRIMARY KEY (`id`)
  8. )

我们把要用到的数据库语句都存到服务类中(这里我们就不讨论如何设计表以及写SQL了):

</>复制代码

  1. private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS `todo` (
  2. " +
  3. " `id` int(11) NOT NULL AUTO_INCREMENT,
  4. " +
  5. " `title` varchar(255) DEFAULT NULL,
  6. " +
  7. " `completed` tinyint(1) DEFAULT NULL,
  8. " +
  9. " `order` int(11) DEFAULT NULL,
  10. " +
  11. " `url` varchar(255) DEFAULT NULL,
  12. " +
  13. " PRIMARY KEY (`id`) )";
  14. private static final String SQL_INSERT = "INSERT INTO `todo` " +
  15. "(`id`, `title`, `completed`, `order`, `url`) VALUES (?, ?, ?, ?, ?)";
  16. private static final String SQL_QUERY = "SELECT * FROM todo WHERE id = ?";
  17. private static final String SQL_QUERY_ALL = "SELECT * FROM todo";
  18. private static final String SQL_UPDATE = "UPDATE `todo`
  19. " +
  20. "SET `id` = ?,
  21. " +
  22. "`title` = ?,
  23. " +
  24. "`completed` = ?,
  25. " +
  26. "`order` = ?,
  27. " +
  28. "`url` = ?
  29. " +
  30. "WHERE `id` = ?;";
  31. private static final String SQL_DELETE = "DELETE FROM `todo` WHERE `id` = ?";
  32. private static final String SQL_DELETE_ALL = "DELETE FROM `todo`";

OK!一切工作准备就绪,下面我们来实现我们的JDBC版本的服务~

实现JDBC版本的服务

所有的获取连接、获取执行数据的操作都要在Handler中完成。比如我们可以这样获取数据库连接:

</>复制代码

  1. client.getConnection(conn -> {
  2. if (conn.succeeded()) {
  3. final SQLConnection connection = conn.result();
  4. // do something...
  5. } else {
  6. // handle failure
  7. }
  8. });

由于每一个数据库操作都需要获取数据库连接,因此我们来包装一个返回Handler>的方法,在此回调中可以直接使用数据库连接,可以减少一些代码量:

</>复制代码

  1. private Handler> connHandler(Future future, Handler handler) {
  2. return conn -> {
  3. if (conn.succeeded()) {
  4. final SQLConnection connection = conn.result();
  5. handler.handle(connection);
  6. } else {
  7. future.fail(conn.cause());
  8. }
  9. };
  10. }

获取数据库连接以后,我们就可以对数据库进行各种操作了:

query : 执行查询(raw SQL)

queryWithParams : 执行预编译查询(prepared statement)

updateWithParams : 执行预编译DDL语句(prepared statement)

execute: 执行任意SQL语句

所有的方法都是异步的所以每个方法最后都接受一个Handler参数,我们可以在此Handler中获取结果并执行相应逻辑。

现在我们来编写初始化数据库表的initData方法:

</>复制代码

  1. @Override
  2. public Future initData() {
  3. Future result = Future.future();
  4. client.getConnection(connHandler(result, connection ->
  5. connection.execute(SQL_CREATE, create -> {
  6. if (create.succeeded()) {
  7. result.complete(true);
  8. } else {
  9. result.fail(create.cause());
  10. }
  11. connection.close();
  12. })));
  13. return result;
  14. }

此方法仅会在Verticle初始化时被调用,如果todo表不存在的话就创建一下。注意,最后一定要关闭数据库连接

下面我们来实现插入逻辑方法:

</>复制代码

  1. @Override
  2. public Future insert(Todo todo) {
  3. Future result = Future.future();
  4. client.getConnection(connHandler(result, connection -> {
  5. connection.updateWithParams(SQL_INSERT, new JsonArray().add(todo.getId())
  6. .add(todo.getTitle())
  7. .add(todo.isCompleted())
  8. .add(todo.getOrder())
  9. .add(todo.getUrl()), r -> {
  10. if (r.failed()) {
  11. result.fail(r.cause());
  12. } else {
  13. result.complete(true);
  14. }
  15. connection.close();
  16. });
  17. }));
  18. return result;
  19. }

我们使用updateWithParams方法执行插入逻辑,并且传递了一个JsonArray变量作为预编译参数。这一点很重要,使用预编译语句可以有效防止SQL注入。

我们再来实现getCertain方法:

</>复制代码

  1. @Override
  2. public Future> getCertain(String todoID) {
  3. Future> result = Future.future();
  4. client.getConnection(connHandler(result, connection -> {
  5. connection.queryWithParams(SQL_QUERY, new JsonArray().add(todoID), r -> {
  6. if (r.failed()) {
  7. result.fail(r.cause());
  8. } else {
  9. List list = r.result().getRows();
  10. if (list == null || list.isEmpty()) {
  11. result.complete(Optional.empty());
  12. } else {
  13. result.complete(Optional.of(new Todo(list.get(0))));
  14. }
  15. }
  16. connection.close();
  17. });
  18. }));
  19. return result;
  20. }

在这个方法里,当我们的查询语句执行以后,我们获得到了ResultSet实例作为查询的结果集。我们可以通过getColumnNames方法获取字段名称,通过getResults方法获取结果。这里我们通过getRows方法来获取结果集,结果集的类型为List

其余的几个方法:getAll, update, delete 以及 deleteAll都遵循上面的模式,这里就不多说了。你可以在GitHub上浏览完整的源代码。

重构完毕,我们来写待办事项服务对应的配置,然后再来运行!

再来运行!

首先我们在项目的根目录下创建一个 config 文件夹作为配置文件夹。我们在其中创建一个config_jdbc.json文件作为 jdbc 类型服务的配置:

</>复制代码

  1. {
  2. "service.type": "jdbc",
  3. "url": "jdbc:mysql://localhost/vertx_blueprint?characterEncoding=UTF-8&useSSL=false",
  4. "driver_class": "com.mysql.cj.jdbc.Driver",
  5. "user": "vbpdb1",
  6. "password": "666666*",
  7. "max_pool_size": 30
  8. }

你需要根据自己的情况替换掉上述配置文件中相应的内容(如 JDBC URLJDBC 驱动 等)。

再建一个config.json文件作为redis类型服务的配置(其它的项就用默认配置好啦):

</>复制代码

  1. {
  2. "service.type": "redis"
  3. }

我们的构建文件也需要更新咯~这里直接给出最终的build.gradle文件:

</>复制代码

  1. plugins {
  2. id "java"
  3. }
  4. version "1.0"
  5. ext {
  6. vertxVersion = "3.3.0"
  7. }
  8. jar {
  9. // by default fat jar
  10. archiveName = "vertx-blueprint-todo-backend-fat.jar"
  11. from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
  12. manifest {
  13. attributes "Main-Class": "io.vertx.core.Launcher"
  14. attributes "Main-Verticle": "io.vertx.blueprint.todolist.verticles.TodoVerticle"
  15. }
  16. }
  17. repositories {
  18. jcenter()
  19. mavenCentral()
  20. mavenLocal()
  21. }
  22. task annotationProcessing(type: JavaCompile, group: "build") {
  23. source = sourceSets.main.java
  24. classpath = configurations.compile
  25. destinationDir = project.file("src/main/generated")
  26. options.compilerArgs = [
  27. "-proc:only",
  28. "-processor", "io.vertx.codegen.CodeGenProcessor",
  29. "-AoutputDirectory=${destinationDir.absolutePath}"
  30. ]
  31. }
  32. sourceSets {
  33. main {
  34. java {
  35. srcDirs += "src/main/generated"
  36. }
  37. }
  38. }
  39. compileJava {
  40. targetCompatibility = 1.8
  41. sourceCompatibility = 1.8
  42. dependsOn annotationProcessing
  43. }
  44. dependencies {
  45. compile ("io.vertx:vertx-core:${vertxVersion}")
  46. compile ("io.vertx:vertx-web:${vertxVersion}")
  47. compile ("io.vertx:vertx-jdbc-client:${vertxVersion}")
  48. compile ("io.vertx:vertx-redis-client:${vertxVersion}")
  49. compile ("io.vertx:vertx-codegen:${vertxVersion}")
  50. compile "mysql:mysql-connector-java:6.0.2"
  51. testCompile ("io.vertx:vertx-unit:${vertxVersion}")
  52. testCompile group: "junit", name: "junit", version: "4.12"
  53. }
  54. task wrapper(type: Wrapper) {
  55. gradleVersion = "2.12"
  56. }

好啦好啦,迫不及待了吧?~打开终端,构建我们的应用:

</>复制代码

  1. gradle build

然后我们可以运行Redis版本的待办事项服务:

</>复制代码

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

我们也可以运行JDBC版本的待办事项服务:

</>复制代码

  1. 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 命令来构建并运行。

哈哈,成功了!

哈哈,恭喜你完成了整个待办事项服务,是不是很开心?~在整个教程中,你应该学到了很多关于 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)中来配置路由以及处理请求,比如:

</>复制代码

  1. @RestController
  2. @ComponentScan
  3. @EnableAutoConfiguration
  4. public class TodoController {
  5. @Autowired
  6. private TodoService service;
  7. @RequestMapping(method = RequestMethod.GET, value = "/todos/{id}")
  8. public Todo getCertain(@PathVariable("id") int id) {
  9. return service.fetch(id);
  10. }
  11. }

在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 文件中定义路由,类似于这样:

</>复制代码

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

而在Vert.x Web中,我们通过Router对象来配置路由:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64828.html

相关文章

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

    摘要:上部分蓝图教程中我们一起探索了如何用开发一个基于消息的应用。对部分来说,如果看过我们之前的蓝图待办事项服务开发教程的话,你应该对这一部分非常熟悉了,因此这里我们就不详细解释了。有关使用实现的教程可参考蓝图待办事项服务开发教程。 上部分蓝图教程中我们一起探索了如何用Vert.x开发一个基于消息的应用。在这部分教程中,我们将粗略地探索一下kue-http模块的实现。 Vert.x Kue ...

    Kerr1Gan 评论0 收藏0
  • Vert.x Blueprint 系列教程(二) | 开发基于消息的应用 - Vert.x Kue

    摘要:本文章是蓝图系列的第二篇教程。这就是请求回应模式。好多属性我们一个一个地解释一个序列,作为的地址任务的编号任务的类型任务携带的数据,以类型表示任务优先级,以枚举类型表示。默认优先级为正常任务的延迟时间,默认是任务状态,以枚举类型表示。 本文章是 Vert.x 蓝图系列 的第二篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待办事项服务开发教程 Vert.x B...

    elina 评论0 收藏0
  • Vert.x Blueprint 系列教程(三) | Micro-Shop 微服务应用实战

    摘要:本教程是蓝图系列的第三篇教程,对应的版本为。提供了一个服务发现模块用于发布和获取服务记录。前端此微服务的前端部分,目前已整合至组件中。监视仪表板用于监视微服务系统的状态以及日志统计数据的查看。而服务则负责发布其它服务如服务或消息源并且部署。 本文章是 Vert.x 蓝图系列 的第三篇教程。全系列: Vert.x Blueprint 系列教程(一) | 待办事项服务开发教程 Vert....

    QiShare 评论0 收藏0
  • 「Odoo 基础教程系列」第三篇——从 Todo 应用开始(2)

    摘要:现在我们来给待办事项增加一个紧急程度的字段,用来表示当前任务的优先级。此处我们还给这个字段添加了默认值,表示当一个待办事项被创建后,如果没有指定紧急程度,将默认是待办状态。这篇教程中的代码同样会更新在我的仓库中。 showImg(https://segmentfault.com/img/bVbfv3E?w=1330&h=912); 在这篇教程里我们将会了解到 Odoo 模型里的一些其他...

    iflove 评论0 收藏0
  • 「Odoo 基础教程系列」第四篇——从 Todo 应用开始(3)

    摘要:在这一篇教程中,将会涉及到外键字段,可以将两个模型关联起来,然后很方便地获取到对应的数据。关联字段这一小节里,我们会给待办事项加上分类,并且这个分类可以让用户自己创建维护。今天这篇教程的内容就先到这里了,教程中的代码会更新在我的仓库中。 showImg(https://segmentfault.com/img/bVbfzvt?w=1280&h=795); 在这一篇教程中,将会涉及到外键...

    HollisChuang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<