Vert.x 导论之三:用Vert.x 开发Rest接口

’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'文件来添加如下依赖:


  io.vertx
  vertx-web
  3.5.1

这是你使用Vert.x Web所需要的唯一事物,很方便,不是么?

在之前的帖子中,当我们请求http://localhost:8080,我们返回一个 Hello World 消息。我们现在用Vert.x Web做同样的事,打开io.vertx.blog.first.MyFirstVerticle并将`start'方法改为:

@Override
public void start(Future fut) {
 // 创建一个router对象。
 Router router = Router.router(vertx);

 // 将"/"绑定到我们的hello消息 - 从而保持兼容性
 router.route("/").handler(routingContext -> {
   HttpServerResponse response = routingContext.response();
   response
       .putHeader("Content-Type", "text/html")
       .end("

Hello from my first Vert.x 3 application

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

Hello from my first Vert.x 3 application

"); });

该代码将抵达"/"的请求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人员”。此外,有很多可以实现同样效果的更好方式以及应该尝试的各种框架,但这些不是关键。这里将尽量让一切显得简单并只依赖于JQueryTwitter 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 fut) {
 Router router = Router.router(vertx);
 router.route("/").handler(routingContext -> {
   HttpServerResponse response = routingContext.response();
   response
       .putHeader("content-type", "text/html")
       .end("

Hello from my first Vert.x 3 application

"); }); // 从/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格式。 Jacksonbean 类的序列化和反序列化自动化,让我们的代码能更简单。

让我们来创建几瓶威士忌。在MyFirstVerticle类中,添加如下代码:

// 存放我们的产品
private Map 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 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(处理器)一样,我们的方法接收一个RoutingContextRoutingContext通过设置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),你应该能看到:

My Whiskies

我知道你很好奇并想知道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/whiskiesPOST请求映射到了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 对应id0

来学习下这个参数如何在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`的响应通常没有内容。

其他方法

我们在这里不会提及getOneupdateOne的细节,考虑到实现都很明确且相似。他们的实现在GitHub。

庆祝下!

是时候给这个帖子下个结论了。我们了解了Vert.x Web如何让你轻松实现REST API接口并服务静态资源。比起之前更有趣,但还是很简单。

下一篇帖子 我们将改进我们的测试来覆盖REST API。

敬请期待!

你可能感兴趣的:(Vert.x 导论之三:用Vert.x 开发Rest接口)