美文网首页我爱编程
Vert.x 导论之三:用Vert.x 开发Rest接口

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

作者: luciensun | 来源:发表于2018-05-27 23:33 被阅读0次

    ’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人员”。此外,有很多可以实现同样效果的更好方式以及应该尝试的各种框架,但这些不是关键。这里将尽量让一切显得简单并只依赖于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<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格式。 Jacksonbean 类的序列化和反序列化自动化,让我们的代码能更简单。

    让我们来创建几瓶威士忌。在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(处理器)一样,我们的方法接收一个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接口

        本文链接:https://www.haomeiwen.com/subject/yrqejftx.html