上一篇请点击:Vert.x Blueprint-教你如何玩转待办事项服务一
Vert.x Web与REST API
创建HTTP服务端并配置路由
我们来给start方法加点东西:
(⊙o⊙)…一长串代码诶。。是不是看着很晕呢?我们来详细解释一下。
首先我们创建了一个 Router 实例 (1)。这里的Router代表路由器,相信做过Web开发的开发者们一定不会陌生。路由器负责将对应的HTTP请求分发至对应的处理逻辑(Handler)中。每个Handler负责处理请求并且写入回应结果。当HTTP请求到达时,对应的Handler会被调用。
然后我们创建了两个Set:allowHeaders和allowMethods,并且我们向里面添加了一些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
[NOTE 路径参数 | 在URL中,我们可以通过:name
的形式定义路径参数。当处理请求的时候,Vert.x会自动获取这些路径参数并允许我们访问它们。拿我们的路由举个例子,/todos/19
将 todoId
映射为 19
。]
首先我们先在 io.vertx.blueprint.todolist
包下创建一个Constants
类用于存储各种全局常量(当然也可以放到其对应的类中):
然后我们将start
方法中的TODO
标识处替换为以下的内容:
代码很直观、明了。我们用对应的方法(如get
,post
,patch
等等)将路由路径与路由器绑定,并且我们调用handler
方法给每个路由绑定上对应的Handler
,接受的Handler
类型为Handler<RoutingContext>
。这里我们分别绑定了六个方法引用,它们的形式都类似于这样:
我们将在稍后实现这六个方法,这也是我们待办事项服务逻辑的核心。
异步方法模式
我们之前提到过,Vert.x是 异步、非阻塞的 。每一个异步的方法总会接受一个 Handler
参数作为回调函数,当对应的操作完成时会调用接受的Handler
,这是异步方法的一种实现。还有一种等价的实现是返回Future
对象:
其中,Future
对象代表着一个操作的结果,这个操作可能还没有进行,可能正在进行,可能成功也可能失败。当操作完成时,Future
对象会得到对应的结果。我们也可以通过setHandler
方法给Future
绑定一个Handler
,当Future
被赋予结果的时候,此Handler
会被调用。
Vert.x中大多数异步方法都是基于Handler的。而在本教程中,这两种异步模式我们都会接触到。
待办事项逻辑实现
现在是时候来实现我们的待办事项业务逻辑了!这里我们使用 Redis 作为数据持久化存储。有关Redis的详细介绍请参照Redis 官方网站。Vert.x给我们提供了一个组件—— Vert.x-redis,允许我们以异步的形式操作Redis数据。
[NOTE 如何安装Redis? | 请参照Redis官方网站上详细的安装指南。]
Vert.x Redis
Vert.x Redis允许我们以异步的形式操作Redis数据。我们首先需要在build.gradle
中添加以下依赖:
我们通过RedisClient
对象来操作Redis中的数据,因此我们定义了一个类成员redis
。在使用RedisClient
之前,我们首先需要与Redis建立连接,并且需要配置(以RedisOptions
的形式),后边我们再讲需要配置哪些东西。
我们来实现 initData
方法用于初始化 RedisClient
并且测试连接:
当我们在加载Verticle的时候,我们会首先调用initData
方法,这样可以保证RedisClient
可以被正常创建。
存储格式
我们知道,Redis支持各种格式的数据,并且支持多种方式存储(如list
、hash map
等)。这里我们将我们的待办事项存储在哈希表(map) 中。我们使用待办事项的id
作为key,JSON格式的待办事项数据作为value。同时,我们的哈希表本身也要有个key,我们把它命名为 VERT_TODO,并且存储到Constants
类中:
正如我们之前提到的,我们利用了生成的JSON数据转换类来实现Todo
实体与JSON数据之间的转换(通过几个构造函数),在后面实现待办事项服务的时候可以广泛利用。
获取/获取所有待办事项
我们首先来实现获取待办事项的逻辑。正如我们之前所提到的,我们的处理逻辑方法需要接受一个RoutingContext
类型的参数。我们看一下获取某一待办事项的逻辑方法(handleGetTodo
):
首先我们先通过getParam
方法获取路径参数todoId
(1)。我们需要检测路径参数获取是否成功,如果不成功就返回 400 Bad Request
错误 (2)。这里我们写一个函数封装返回错误response的逻辑:
这里面,end
方法是非常重要的。只有我们调用end
方法时,对应的HTTP Response才能被发送回客户端。
再回到handleGetTodo
方法中。如果我们成功获取到了todoId
,我们可以通过hget
操作从Redis中获取对应的待办事项 (3)。hget
代表通过key从对应的哈希表中获取对应的value,我们来看一下hget
函数的定义:
第一个参数key
对应哈希表的key,第二个参数field
代表待办事项的key,第三个参数代表当获取操作成功时对应的回调。在Handler
中,我们首先检查操作是否成功,如果不成功就返回503
错误。如果成功了,我们就可以获取操作的结果了。结果是null
的话,说明Redis中没有对应的待办事项,因此我们返回404 Not Found
代表不存在。如果结果存在,那么我们就可以通过end
方法将其写入response中 (4)。注意到我们所有的RESTful API都返回JSON格式的数据,所以我们将content-type
头设为JSON
。
获取所有待办事项的逻辑handleGetAll
与handleGetTodo
大体上类似,但实现上有些许不同:
这里我们通过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了~现在我们来实现创建待办事项的逻辑:
首先我们通过context.getBodyAsString()
方法来从请求正文中获取JSON数据并转换成Todo
实体对象 (1)。这里我们包装了一个处理Todo
实例的方法,用于给其添加必要的信息(如URL):
对于没有ID(或者为默认ID)的待办事项,我们会给它分配一个ID。这里我们采用了自增ID的策略,通过AtomicInteger
来实现。
然后我们通过Json.encodePrettily
方法将我们的Todo
实例再次编码成JSON格式的数据 (2)。接下来我们利用hset
函数将待办事项实例插入到对应的哈希表中 (3)。如果插入成功,返回 201
状态码 (4)。
[NOTE 201 状态码? | 正如你所看到的那样,我们将状态码设为201
,这代表CREATED
(已创建)。另外,如果不指定状态码的话,Vert.x Web默认将状态码设为 200 OK
。]
同时,我们接收到的HTTP请求首部可能格式不正确,因此我们需要在方法中捕获DecodeException
异常。这样一旦捕获到DecodeException
异常,我们就返回400 Bad Request
状态码。
更新待办事项
如果你想改变你的计划,你就需要更新你的待办事项。我们来实现更新待办事项的逻辑,它有点小复杂(或者说是,繁琐?):
唔。。。一大长串代码诶。。。我们来看一下。首先我们从 RoutingContext
中获取路径参数 todoId
(1),这是我们想要更改待办事项对应的id。然后我们从请求正文中获取新的待办事项数据 (2)。这一步也有可能抛出 DecodeException
异常因此我们也需要去捕获它。要更新待办事项,我们需要先通过hget
函数获取之前的待办事项 (3),检查其是否存在。获取旧的待办事项之后,我们调用之前在Todo
类中实现的merge
方法将旧待办事项与新待办事项整合到一起 (5),然后编码成JSON格式的数据。然后我们通过hset
函数更新对应的待办事项 (6)(hset
表示如果不存在就插入,存在就更新)。操作成功的话,返回 200 OK
状态。
这就是更新待办事项的逻辑~要有耐心哟,我们马上就要见到胜利的曙光了~下面我们来实现删除待办事项的逻辑。
删除/删除全部待办事项
删除待办事项的逻辑非常简单。我们利用hdel
函数来删除某一待办事项,用del
函数删掉所有待办事项(实际上是直接把那个哈希表给删了)。如果删除操作成功,返回204 No Content
状态。
这里直接给出代码:
啊哈!我们实现待办事项服务的Verticle已经完成咯~一颗赛艇!但是我们该如何去运行我们的Verticle
呢?答案是,我们需要 部署并运行 我们的Verticle。还好Vert.x提供了一个运行Verticle的辅助工具:Vert.x Launcher,让我们来看看如何利用它。
将应用与Vert.x Launcher一起打包
要通过Vert.x Launcher来运行Verticle,我们需要在build.gradle
中配置一下:
在
jar
区块中,我们配置Gradle使其生成 fat-jar,并指定启动类。fat-jar 是一个给Vert.x应用打包的简便方法,它直接将我们的应用连同所有的依赖都给打包到jar包中去了,这样我们可以直接通过jar包运行我们的应用而不必再指定依赖的CLASSPATH
我们将
Main-Class
属性设为io.vertx.core.Launcher
,这样就可以通过Vert.x Launcher来启动对应的Verticle了。另外我们需要将Main-Verticle
属性设为我们想要部署的Verticle的类名(全名)。
配置好了以后,我们就可以打包了:
运行我们的服务
万事俱备,只欠东风。是时候运行我们的待办事项服务了!首先我们先启动Redis服务:
如果没问题的话,你将会在终端中看到 Succeeded in deploying verticle
的字样。下面我们可以自由测试我们的API了,其中最简便的方法是借助 todo-backend-js-spec 来测试。
键入 http://127.0.0.1:8082/todos
:
测试结果:
当然,我们也可以用其它工具,比如 curl
:
到此第二部分就结束了,下一部分我们会对上述的代码进行重构,将控制器部分与业务逻辑部分分离。敬请期待