’Vert.x导论‘回顾
这篇帖子是Vert.x导论系列的一部分,现在让我们快速回顾一下之前帖子的内容。在第一篇帖子中,我们开发了一个非常简单的Vert.x 3应用,并且学习了这个应用如何被测试,打包及执行。在上一篇帖子中,我们学习了这个应用如何可配置,并在测试中采用了随机端口。
这次,我们打算走的更远些,开发一个CRUD(增删改查)的应用。一个暴露出一个HTML页面,采用REST API和后端交互的应用。API的REST风格不是这篇帖子的重点,基于这话题见仁见智,还是每个人自行判断。
换言之,我们将看到:
- Vert.x Web - 一个方便你用Vert.x创建Web应用的框架
- 如何暴露静态资源
- 如何开发一个REST API
在这篇帖子中开发的代码提供在 Github 项目的post-3分支。
现在开工。
Vert.x Web
如何你在之前的帖子中所观察到的,只是使用Vert.x核心来处理复杂HTTP应用有些麻烦。这是Vert.x Web成立的主要原因。这个模块让开发基于Vert.x的Web应用变得很方便,也没有改变Vert.x的开发哲学。
为了使用Vert.x Web,你需要更新'pom.xml'文件来添加如下依赖:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>3.5.1</version>
</dependency>
这是你使用Vert.x Web所需要的唯一事物,很方便,不是么?
在之前的帖子中,当我们请求http://localhost:8080,我们返回一个 Hello World 消息。我们现在用Vert.x Web做同样的事,打开io.vertx.blog.first.MyFirstVerticle
并将`start'方法改为:
@Override
public void start(Future<Void> fut) {
// 创建一个router对象。
Router router = Router.router(vertx);
// 将"/"绑定到我们的hello消息 - 从而保持兼容性
router.route("/").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response
.putHeader("Content-Type", "text/html")
.end("<h1>Hello from my first Vert.x 3 application</h1>");
});
// 创建HTTP服务器并将"accept"方法传递给请求处理器。
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(
// 从配置中获取端口,默认是8080端口。
config().getInteger("http.port", 8080),
result -> {
if (result.succeeded()) {
fut.complete();
} else {
fut.fail(result.cause());
}
}
);
}
你可能被这段代码的长度吓到了(相比之前的代码)。但是后面你将看到,这回让我们的应用升级为加强版。
我们首先创建了一个Router
对象。router是Vert.x Web的奠基石。该对象负责将HTTP请求分发到对应的处理器。在Vert.x Web中有两个概念很重要:
- Routes - 让你定义如何分发请求。
- Handlers - 真正处理请求的对象并输出结果。Handlers可以链化调用。
如果你理解了这三个概念,你就理解了Vert.x Web的所有概念。
我们先聚焦这段代码:
router.route("/").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response
.putHeader("Content-Type", "text/html")
.end("<h1>Hello from my first Vert.x 3 application</h1>");
});
该代码将抵达"/"的请求routes到指定的处理器。Handlers(处理器)接收一个RoutingContext
对象。这个处理器和我们之前的代码很类似,也很普通正如它处理同样类型的对象:HttpServerResponse
。
现在我们来看一下其余代码:
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(
// 从配置中获取端口,默认是8080端口。
config().getInteger("http.port", 8080),
result -> {
if (result.succeeded()) {
fut.complete();
} else {
fut.fail(result.cause());
}
}
);
}
基本如之前的代码,除了我们改了请求处理器。我们将router::accept
传递给了处理器。你可能对这个记号不熟。这是对一个方法的引用(此处是router
对象的accept
方法)。换言之,这段代码表示当Vert.x收到一个请求时调用router
对象的accept
方法。
现在看看是否如我们预期一样的运行:
mvn clean package
java -jar target/my-first-vertx-app-0.0.1-SNAPSHOT-fat.jar
在浏览器中打开http://localhost:8080
,你应该能看到Hello消息。由于我们没有改变我们应用的行为,我们的单元测试还是能通过。
暴露静态资源
现在我们有了使用vert.x web的第一个应用。现在开始服务静态资源,比如一个index.html
页面。当下,首先需要声明:“我们这里将看到的HTML页面很难看:因为作者不是UI人员”。此外,有很多可以实现同样效果的更好方式以及应该尝试的各种框架,但这些不是关键。这里将尽量让一切显得简单并只依赖于JQuery
和Twitter Bootstrap
框架,所以如果你比较懂JavaScript你将理解并编辑这个页面。
首先创建将成为我们应用入口的HTML页面。在src/main/resources/assets
中穿件一个index.html
页面,其中内容可见这里。由于这只是有不少JavaScript的HTML页面,我们这里不会对文件内容展开细节描述。
这个页面是一个比较简单的CRUD界面来管理我的尚未完成的威士忌库存。这里采用了很通用的方法,所以你可以转换为你自己的收藏。在主表格中展现了产品列表。你可以创建一个新产品,编辑一个产品或者删除一个产品。这些操作都依赖于通过AJAX
方式调用REST API
(我们将会实现的)。
一旦这个页面被创建好,编辑io.vertx.blog.first.MyFirstVerticle
类并将start
方法改为:
@Override
public void start(Future<Void> fut) {
Router router = Router.router(vertx);
router.route("/").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response
.putHeader("content-type", "text/html")
.end("<h1>Hello from my first Vert.x 3 application</h1>");
});
// 从/assets目录服务静态资源
router.route("/assets/*").handler(StaticHandler.create("assets"));
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(
// Retrieve the port from the configuration,
// default to 8080.
config().getInteger("http.port", 8080),
result -> {
if (result.succeeded()) {
fut.complete();
} else {
fut.fail(result.cause());
}
}
);
}
比起之前的代码唯一的区别就是 router.route("/assets/*").handler(StaticHandler.create("assets"));
这行代码意味着什么?很简单,将对"/assets/*"发起的请求路由到存放在“assets”目录下的资源。所以我们的index.html
页面将使用 http://localhost:8080/assets/index.html
对外服务。
在测试验证这之前,我们花些时间来看看处理器创建。在Vert.x web中所有的处理动作都采用handler(处理器)实现。你经常调用create方法来创建一个处理器。
你应该等不及要看一下我们漂亮的HTML页面,现在创建并运行应用:
mvn clean package
java -jar target/my-first-vertx-app-0.0.1-SNAPSHOT-fat.jar
现在,打开你的浏览器,访问http://localhost:8080/assets/index.html
,就是这里,不太好看,之前说过了。
你可能也注意到了,表格是空的,这是因为我们还没有实现REST API。现在开始做吧。
用Vert.x Web实现REST API
Vert.x Web让REST API的实现变得异常简单,基于它只是将你的URL(Uniform Resoure Locator)路由到对应的处理器。本组API很简单,将如下构建:
Vert.x Web makes the implementation of REST API really easy, as it basically routes your URL to the right handler. The API is very simple, and will be structured as follows:
- GET /api/whiskies => 获取所有威士忌 (getAll)
- GET /api/whiskies/:id => 获取对应id的威士忌 (getOne)
- POST /api/whiskies => 新增一瓶威士忌 (addOne)
- PUT /api/whiskies/:id => 更新一瓶威士忌 (updateOne)
- DELETE /api/whiskies/id => 删除一瓶威士忌 (deleteOne)
我们需要一些基础数据…
在我们开始进一步行动前,先创建一些基础数据。创建含如下内容的src/main/java/io/vertx/blog/first/Whisky.java
文件:
package io.vertx.blog.first;
import java.util.concurrent.atomic.AtomicInteger;
public class Whisky {
private static final AtomicInteger COUNTER = new AtomicInteger();
private final int id;
private String name;
private String origin;
public Whisky(String name, String origin) {
this.id = COUNTER.getAndIncrement();
this.name = name;
this.origin = origin;
}
public Whisky() {
this.id = COUNTER.getAndIncrement();
}
public String getName() {
return name;
}
public String getOrigin() {
return origin;
}
public int getId() {
return id;
}
public void setName(String name) {
this.name = name;
}
public void setOrigin(String origin) {
this.origin = origin;
}
}
这是个很简单的 bean 类(所以含有getters和setters方法). 我们选择这种格式因为Vert.x依赖 Jackson 来处理JSON
格式。 Jackson
将 bean 类的序列化和反序列化自动化,让我们的代码能更简单。
让我们来创建几瓶威士忌。在MyFirstVerticle
类中,添加如下代码:
// 存放我们的产品
private Map<Integer, Whisky> products = new LinkedHashMap<>();
// 创建一些产品
private void createSomeProducts() {
Whisky bowmore = new Whisky("Bowmore 15 Years Laimrig", "Scotland, Islay");
products.put(bowmore.getId(), bowmore);
Whisky talisker = new Whisky("Talisker 57° North", "Scotland, Island");
products.put(talisker.getId(), talisker);
}
在start
方法中,调用createSomeProducts
方法:
@Override
public void start(Future<Void> fut) {
createSomeProducts();
// 创建一个router对象。
Router router = Router.router(vertx);
// 方法的其余部分
}
你可能已经注意到了,到目前为止,我们实践上没有一个后端,只是一个内存中存在的map对象。在其他帖子中将涵盖如何添加一个backend
(后端,数据库存储层)。
获取我们的产品
现在开始实现REST API,从GET /api/whiskies
开始。该API用JSON数组的格式返回威士忌库存列表。
在start
方法中,在静态处理器代码行下添加如下内容:
router.get("/api/whiskies").handler(this::getAll);
这行代码指示router
(路由器)通过调用getAll
方法来处理对"/api/whiskies"的GET
请求。我们可以采用内联的方法来实现处理器代码,但为了清晰起见,我们创建另一个方法:
private void getAll(RoutingContext routingContext) {
routingContext.response()
.putHeader("Content-Type", "application/json; charset=utf-8")
.end(Json.encodePrettily(products.values()));
}
和其他handler(处理器)一样,我们的方法接收一个RoutingContext
。RoutingContext
通过设置Content-Type
和实际内容来填充response
(响应) 。因为我们的内容可能包含特殊字符,我们声明使用UTF-8字符编码。我们不需要自己计算JSON字符串来创建实际内容。Vert.x允许我们使用Json
API。所以 Json.encodePrettily(products.values())
返回代表威士忌库存的JSON字符串。
我们可以使用Json.encodePrettily(products)
,但为了让JavaScript代码更简单些,我们只是返回威士忌集合而不是含有ID=>Bottle
记录的对象。
一旦就绪,我们应该可以从HTML页面获取威士忌库存信息。让我们来试试:
mvn clean package
java -jar target/my-first-vertx-app-0.0.1-SNAPSHOT-fat.jar
然后打开你浏览器中的HTML页面(http://localhost:8080/assets/index.html
),你应该能看到:
我知道你很好奇并想知道REST API实际返回结果,打开浏览器访问
http://localhost:8080/api/whiskies
,你应该能看到:
[ {
"id" : 0,
"name" : "Bowmore 15 Years Laimrig",
"origin" : "Scotland, Islay"
}, {
"id" : 1,
"name" : "Talisker 57° North",
"origin" : "Scotland, Island"
} ]
创建一个产品
现在让我们来创建一瓶新的威士忌。不像之前的REST API 端点,当前的API需要读取请求体。基于性能考虑,这需要显式启用。
在start
方法中的getAll
代码行下添加如下代码:
router.route("/api/whiskies*").handler(BodyHandler.create());
router.post("/api/whiskies").handler(this::addOne);
第一行代码允许在"/api/whiskies"下的所有路由读取请求体。我们可以用router.route().handler(BodyHandler.create())
来允许全局读取请求体。
第二行代码将对/api/whiskies
的POST
请求映射到了addOne
方法。我们来创建这个方法:
private void addOne(RoutingContext routingContext) {
final Whisky whisky = Json.decodeValue(routingContext.getBodyAsString(),
Whisky.class);
products.put(whisky.getId(), whisky);
routingContext.response()
.setStatusCode(201)
.putHeader("content-type", "application/json; charset=utf-8")
.end(Json.encodePrettily(whisky));
}
以上方法首先从请求体中获取Whisky
对象。只是将请求体读取成一个字符串并传递给Json.decodeValue
方法。一旦Whisky对象创建成功,就将它加入内存map中,并以JSON格式返回被创建的威士忌信息。
现在来确认下,用如下命令重构并重启应用:
mvn clean package
java -jar target/my-first-vertx-app-0.0.1-SNAPSHOT-fat.jar
现在,刷新HTML页面并点击Add a new bottle
按钮。输入诸如:“Jameson”作为商品名称,“Ireland”作为来源。新增的威士忌信息应该会被添加到表格中。
HTTP 201状态码
如你所见,我们将响应状态设置为`201`.这意味着创建成功,在创建一个实体的REST API中经常被使用。
默认情况下,vert.x web会将响应状态设置为`200`意味成功。
喝完一瓶威士忌
威士忌并不会永流传,所以我们需要能删除某个威士忌的信息。在start方法中,添加如下一行代码:
router.delete("/api/whiskies/:id").handler(this::deleteOne);
在上面的URL中,我们定义一个路径参数:id
。所以,当处理一个匹配的请求时,Vert.x提取对应于参数的路径段并允许我们在handler方法中访问它。比如说,/api/whiskies/0
对应id
为0
。
来学习下这个参数如何在handler方法中使用。创建如下的deleteOne方法:
private void deleteOne(RoutingContext routingContext) {
String id = routingContext.request().getParam("id");
if (id == null) {
routingContext.response().setStatusCode(400).end();
} else {
Integer idAsInteger = Integer.valueOf(id);
products.remove(idAsInteger);
}
routingContext.response().setStatusCode(204).end();
}
路径参数通过 routingContext.request().getParam("id")
来获取。上述代码检查id是否为空(没有设置),这种情况下返回一个失败请求响应(状态码400)。否则,就从内存map中删除id对应的威士忌信息。
HTTP 204状态码
如你所见,我们将响应状态码设置为`204 - NO CONTENT`(没有内容)。
对应于HTTP动词`delete`的响应通常没有内容。
其他方法
我们在这里不会提及getOne
和updateOne
的细节,考虑到实现都很明确且相似。他们的实现在GitHub。
庆祝下!
是时候给这个帖子下个结论了。我们了解了Vert.x Web如何让你轻松实现REST API接口并服务静态资源。比起之前更有趣,但还是很简单。
下一篇帖子 我们将改进我们的测试来覆盖REST API。
敬请期待!