Vert.x Web 模块(一)

时间:2021-04-14 18:02:19


Vert.x-Web是用Vert.x创建Web应用程序的一组构建模块的集合。

可以将Vert.x想象成构建现代,伸缩we应用的瑞士军刀。Vert.x核心模块提供相当低层功能用于处理HTTP,和应用充分使用的功能。Vert.x-Web构建于Vert.x 核心模块之上,用于提供更丰富的构建真实web应用程序的功能集,并更加简单。

 

对于使用Vert.x 2.xYoke是成功的并且从Node.js世界中的Express框架和Ruby世界中的Sinatra获得灵感。

Vert.x-Web强在设计,灵活的并可以完全嵌入。使用你需要的的,其他都不需要。Vertx-Web不是容器。

可以使用 Vert.x-web创建经典的服务器侧Web应用程序,RESTfulweb应用程序,实时(服务器推送)Web应用’,或者是你关心过的某些种类的web应用。Vert.x-Web不关心。应用的类型根据你的喜好决定,而不是由Vert.x-web决定。

Vert.x-Web非常适合编写RESTful HTTP微服务,但是Vert.x不强迫你像那样编写应用程序。

Vert.x-Web包含的一关键特性有:

  • 路由(基于方法【get,post,路径,等)

  • 路径的正则表达式匹配

  • 从路径中抽取参数

  • 内容协商

  • 请求体处理

  • 请求和响应体大小限制

  • Cookie解析与处理

  • 多部分表单(指有表单域和上传的表单)

  • 多部分文件上传

  • 子路由器

  • 会话支持——包括本地(粘滞会话)和集群(非粘滞)

  • CORS(跨区域资源共享)支持

  • 错误页处理

  • Basic认证

  • 基于认证的重定向

  • JWt (Java Web Toolkit) 基本认证

  • 用户/角色/权限认证

  • favicon(指浏览器页面上的图标)处理

  • 模板支持服务端渲染,支持下列模板引擎

    • Handlebars

    • Jade

    • MVEL

    • Thymeleaf

  • 响应时间处理器

  • 静态文件提供,包括缓存逻辑和目录列表

  • 请求超时处理

  • SockJS支持

  • 事件总线桥

  • CSRF虚拟跨站请求

  • 虚拟主机

Vert.x-web的大多数特性被实现成处理器,所以要以自己编写。随着时间推移,会在vert.x-web中添加更多内容。我们将在此手册中讨认所有这些特性。

 

使用Vert.xWeb

为了使用vert.x web模块,添加下列依赖项到构建文件的依赖管理节。

  • Maven(在项目的pom.xml文件中)

<dependency>

 <groupId>io.vertx</groupId>

 <artifactId>vertx-web</artifactId>

 <version>3.2.1</version>

</dependency>

  • Gradle(在项目的build.gradle文件中)

dependencies {

 compile 'io.vertx:vertx-web:3.2.1'

}

 

Vert.x内核HTT服务器回顾

Vert.x-Web使用Vert.x内核模块API,同时也暴露了一些内核模块的API。所以撑握Vert.x内核中的编写HTTP服务器的基本概念是有价值的,如是你还不知道。

Vert.x内核HTTP文档包含很多详细信息。

这是一个用Vert.x内核模块编写的helloworld web服务器。这里不涉及Vert.x-Web:

HttpServer server =vertx.createHttpServer();

 

server.requestHandler(request -> {

 

  //This handler gets called for each request that arrives on the server

 HttpServerResponse response = request.response();

 response.putHeader("content-type", "text/plain");

 

  //Write to the response and end it

 response.end("Hello World!");

});

 

server.listen(8080);

我们创建了一个HTTP服务器实例,然后设置了一个请求处理器。无论什么时候,只要请求到达服务器,就会调用请求处理器。在请前到达到后,我们设置了Content-Typetext/plain,然后写入”Hello World!”并结束Response

最后们们告诉服务器在8080端口上侦听请求(默认的主机是localhost)

运行这段代码,让浏览器访问http://localhost:8080验正是否如我们期望的一样。

 

基本的Vert.x-Web概念

这是最基本的概念:

Router(路由器)Vert.x-Web核心概念之一。它是一个维护零到多个路由的对象。

某个路由器将一个HTTP请求匹配到第一个发现的路由并将请求传给此路由。

route路由与一些处理器关联,这此处理器接收请求。然后在处理器中对请求进行处理或者结束它,或者将其传递给下一个匹配的处理器。

这是一个简单路由器的例子:

HttpServer server =vertx.createHttpServer();

 

Router router = Router.router(vertx);

 

router.route().handler(routingContext ->{

 

  //This handler will be called for every request

 HttpServerResponse response = routingContext.response();

 response.putHeader("content-type", "text/plain");

 

  //Write to the response and end it

 response.end("Hello World from Vert.x-Web!");

});

 

server.requestHandler(router::accept).listen(8080);

这与前一节Vert.x内核模块的Helloworld的例子所做的事一样。但此时用的是Vert.x-Web

如前面章节一样,我们创建了一个HTT服务器,然后我们创建了一个路由器。无参route方法创建了一个没有匹配条件的简单路由,所以此路由将匹配到达服务器的所有请求。然后我们为路由指定了一个处理器,所有的请求到达服务器时,者将调用此处理器。传入处理器的是一个RoutingContex对象,此对象包含了标准的Vert.x HttpServerRequestHttpServerResponse对象,当然也包含一些使Vert.x-Webg易于使用其他对象。

 

对于被路由的每个请求,都有一个唯一的路由一下文实例,此实例被传给匹配此请求的所有处理器。一旦我们设置了处理器,Http服务器的所有请求处理器将把所有到来的请求传给accept方法。

这就是基本概念,现在我们来研究更多信息:

 

处理请求和调用下一个处理器

Vert.x-Web决定路由一个请求到匹配的路由时,Vert.x-Web将传入一个RoutingContext实例,并调用路由处理器。

如果在此路由处理器中不结束响应,Vert.x-Web将会调用另一个能处理请求的匹配路由。在当前处理器执行完成之前,不必调用next方法调用下一个匹配地处理器。

如果我想那样做,可以在一段时间之后:

Route route1 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

  //enable chunked responses because we will be adding data as

  //we execute over other handlers. This is only required once and

  //only if several handlers do output.

 response.setChunked(true);

 

 response.write("route1\n");

 

  //Call the next matching route after a 5 second delay

 routingContext.vertx().setTimer(5000, tid -> routingContext.next());

});

 

Route route2 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route2\n");

 

  //Call the next matching route after a 5 second delay

 routingContext.vertx().setTimer(5000, tid ->  routingContext.next());

});

 

Route route3 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route3");

 

  //Now end the response

 routingContext.response().end();

});

在上面例子中,route1被写入到响应,5秒后,route2被写到响应,然后5秒后,route3被写入响应,最后响应结束。

注意:做这些都不会产生线程阻塞。

 

使用阻塞处理器

有时,可能在处理器中不得不做一个阻塞事件循环的事,例如调用遗留阻塞API或者做高密度的运算。在正常的处理器不能这样做,所以Vert.x-Web提供了在路由上设置阻塞处理器的的能力。一个阻塞处理器和正常处理器一样,但是由Vert.x从工作线程池中获取的线程调用,而不是用事件循环调用。用blockingHandler方法将阻塞处理器设置到路由上,这里有一个例子:

router.route().blockingHandler(routingContext-> {

 

  //Do something that might take some time synchronously

 service.doSomethingThatBlocks();

 

  //Now call the next handler

 routingContext.next();

 

});

默认的,一些阻塞处理器在同一个上下文中顺序执行(如,相同的verticle实例),这意味着上一个没有执行完成,下一个不会执行。如果你不用关心顺序也不在意处理器的并发执行,可以在使用blockingHandler时设置orderedfalse

 

根据确切路径路由

一个设置好的路由用来匹配请求RUI中的路径。这样,这样路由可以匹配与设置路径一样的任何请求。在下面的例子中,处理器将由路径为/some/path/的请求调用。路么由会忽视斜线,因为/some/path/some/path//都会调用处理器:
Route route = router.route().path("/some/path/");

 

route.handler(routingContext -> {

  //This handler will be called for the following request paths:

 

  //`/some/path`

  //`/some/path/`

  //`/some/path//`

  //

  //but not:

  //`/some/path/subdir`

});

 

路由匹配前缀的路径

通常需要路由所由以某个路径为前缀的所有请求。可以用正则表达式完成这样任务,一个简单的方法是在声明路由路径时,在路径最后使用*号占位符。在下面的例子里,任何以/some/path/开关的RUI路径都会调用例子中的处理器。例如/some/path/foo.html/some/path/otherdir/blah.css都能匹配。

Route route =router.route().path("/some/path/*");

 

route.handler(routingContext -> {

  //This handler will be called for any path that starts with

  //`/some/path/`, e.g.

 

  //`/some/path`

  //`/some/path/`

  //`/some/path/subdir`

  //`/some/path/subdir/blah.html`

  //

  //but not:

  //`/some/bath`

});

在创建路由的时候也可以指定任意路径:

Route route =router.route("/some/path/*");

 

route.handler(routingContext -> {

  //This handler will be called same as previous example

});

 

获取路径参数

使用参数占位符匹配路径是可能的,这些参数可以通过请求的params方法获取。这是一个例子:

Route route = router.route(HttpMethod.POST,"/catalogue/products/:productype/:productid/");

 

route.handler(routingContext -> {

 

 String productType = routingContext.request().getParam("producttype");

 String productID =routingContext.request().getParam("productid");

 

  //Do something with them...

});

占位符由跟在冒号后面的参数名组成。参数名称由一些字母,数字和下划线组成。在上面的例子中,一个POST请求发向路径:/catalogue/products/tools/drill123/,路由将会匹配到productType并得到值tools,productID将会收到值drill123

 

使用正则表达式路由

在路由中正则表达也可以用于匹配URI路径。

Route route =router.route().pathRegex(".*foo");

 

route.handler(routingContext -> {

 

  //This handler will be called for:

 

  ///some/path/foo

  ///foo

  ///foo/bar/wibble/foo

  ///foo/bar

 

  //But not:

  ///bar/wibble

});

另一种是在创建路由时指定正则表达式:
Route route = router.routeWithRegex(".*foo");

 

route.handler(routingContext -> {

 

  //This handler will be called same as previous example

 

});

 

用正则表达式获取路径参数

在使用正则表达式时,可以获取路径参数,这是一个例子:
Route route = router.routeWithRegex(".*foo");

 

// This regular expression matches pathsthat start with something like:

// "/foo/bar" - where the"foo" is captured into param0 and the "bar" is capturedinto

// param1

route.pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(routingContext-> {

 

 String productType =routingContext.request().getParam("param0");

 String productID =routingContext.request().getParam("param1");

 

  //Do something with them...

});

在上面的例子中,如何请求路径为:/tools/drill123/,然后路由将会匹配上,并且productType将接到值为toolsproductID将会接到值drill123。之所以能获取,因为在表达式中存在获取组(此例是小括号括起来的部分)。

 

根据HTTP方法(POST,GET,PUT,DELETE…)路由

一个路由默认是匹配所有HTTP方法。如果想让一个路由到指定的HTTP方法,可以有method方法。

Route route =router.route().method(HttpMethod.POST);

 

route.handler(routingContext -> {

 

  //This handler will be called for any POST request

 

});

也可以在创建路由时与path一起设置:

Route route = router.route(HttpMethod.POST,"/some/path/");

 

route.handler(routingContext -> {

 

  //This handler will be called for any POST request to a URI path starting with/some/path/

 

});

也可以用路由器上的get,post,put方法将请求路由到指定的HTTP方法,例如:

router.get().handler(routingContext -> {

 

  //Will be called for any GET request

 

});

 

router.get("/some/path/").handler(routingContext-> {

  //Will be called for any GET request to a path

  //starting with /some/path

});

 

router.getWithRegex(".*foo").handler(routingContext-> {

  //Will be called for any GET request to a path

  //ending with `foo`

});

如果想用多个HTTP方法指定一个路由,可以多次调用method方法:

Route route = router.route().method(HttpMethod.POST).method(HttpMethod.PUT);

route.handler(routingContext -> {

  //This handler will be called for any POST or PUT request

});

 

路由顺序

默认,路由匹配根据加入路由器的顺序执行。在请求到达路由器时,路由器将逐一通过每个路由,并检查是否匹配。如果匹配此路由的处理器将会被调用。如此处理器随后调用next方法,下一个匹配的处理器将会被调用,如此类推。这个例子对此进行了阐述:

Route route1 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

  //enable chunked responses because we will be adding data as

  //we execute over other handlers. This is only required once and

  //only if several handlers do output.

 response.setChunked(true);

 

 response.write("route1\n");

 

  //Now call the next matching route

 routingContext.next();

});

 

Route route2 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route2\n");

 

  //Now call the next matching route

 routingContext.next();

});

 

Route route3 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route3");

 

  //Now end the response

 routingContext.response().end();

});

从面例子的响应包括:

route1

route2

route3

因为路由已经按顺序匹配/some/path为前缀的路径的请求,并调用相应的处理器。如果相覆盖默认的路由顺序,可以用order方法指定一个整型值。在创建路由时,给期指定一个顺序值决定哪一个被加到路由器中。每个路由的数值是0,第二个路由的数值是1,以此类推。通过为路由指定顺序值,可以覆盖默认的顺序。顺序值可以为负值,例如想确保一个路由在顺序值为0的路由之前解析。

让我们改变路由route2顺序,让它在路由一(route1)之前执行:

Route route1 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route1\n");

 

  //Now call the next matching route

 routingContext.next();

});

 

Route route2 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

  //enable chunked responses because we will be adding data as

  //we execute over other handlers. This is only required once and

  //only if several handlers do output.

 response.setChunked(true);

 

 response.write("route2\n");

 

  //Now call the next matching route

 routingContext.next();

});

 

Route route3 =router.route("/some/path/").handler(routingContext -> {

 

 HttpServerResponse response = routingContext.response();

 response.write("route3");

 

  //Now end the response

 routingContext.response().end();

});

 

// Change the order of route2 so it runsbefore route1

route2.order(-1);

然后响应如下:
route2

route1

route3

如果两具路由有相同的顺序值,那么它们将按添加时的顺序被调用。可以用last方法指定最后处理的路由。

 

基于请求MIME类型路由

可以用consumes方法设置一个路由匹配请求的MIME类型。这样,请求将要包含一个Content-type的头,指定请求体的MIME类型。这将匹在consumes方法中指定的值。基本上,consumes方法描述那一个MIME类型可以被处理器消费。

可以进行精确的MIME类型匹配:

router.route().consumes("text/html").handler(routingContext-> {

   //This handler will be called for any request with

  //content-type header set to `text/html`

 });

也可以指定多个MIME类型的精确匹配:

router.route().consumes("text/html").consumes("text/plain").handler(routingContext-> {

   //This handler will be called for any request with

  //content-type header set to `text/html` or `text/plain`.

 });

用通配符匹配子类型也支持:
router.route().consumes("text/*").handler(routingContext -> {

 

  //This handler will be called for any request with top level type `text`

  //e.g. content-type header set to `text/html` or `text/plain` will both match

 });

也可以匹配MIME的顶层类型

router.route().consumes("*/json").handler(routingContext-> {

   //This handler will be called for any request with sub-type json

  //e.g. content-type header set to `text/json` or `application/json` will bothmatch

 });

如果在consumers没有指定“/”Vert.x-Web默认指定的是子类型。

 

基于客户可接收的MIME类型进行路由

accept HTTP头用于标识客户端接收的哪种MIME类型。一个accept头可以有多个以分隔的MIME类型。MIME类型也可以加一q值说明每个匹配MIME类型的响应的的权重。q值是01之间的一数值,如果忽略,默认值为1.0

例如,下面accept头说明客户端接收text/plainMIME类型的响应。

Accept: text/plain

下面的客户端将接收text/plain或者text/html的响应

Accept: text/plain, text/html

下面的客户端将接收text/plain或者text/html,但是更侧重于text/html,因为有较高的q值(默认值是q=1.0

Accept: text/plain; q=0.9, text/html

如果服务既可以提供text/plain,也可以提供text/html,服务器在此种设置下会提供text/html

使用produces方法,可以决定路由产生哪种MIME类型,例如下面的处理器产生一个application/jsonMIME类型响应。

router.route().produces("application/json").handler(routingContext-> {

 

 HttpServerResponse response = routingContext.response();

 response.putHeader("content-type","application/json");

 response.write(someJSON).end();

 

});

 

这样路由将匹配任何以application/json为值的accept的请求。这一些可以匹配accept头的例子:
 Accept: application/json

Accept: application/*

Accept: application/json, text/html

Accept: application/json;q=0.7, text/html;q=0.8,text/plain

也可以使路由生产多个MIME类型。如果这样,可以使用getAcceptableContentType方法找到实际可接收的MIME类型。

router.route().produces("application/json").produces("text/html").handler(routingContext-> {

 

 HttpServerResponse response = routingContext.response();

 

  // Get the actualMIME type acceptable

  StringacceptableContentType = routingContext.getAcceptableContentType();

 

 response.putHeader("content-type", acceptableContentType);

 response.write(whatever).end();

});

在上面的例子中,如果你发送一个下面的accept头的请求:
 Accept: application/json; q=0.7, text/html

路由将匹配,并且acceptableContentType将包含text/html,因两个都会接收,但是text/html有着较高的q值。

 

组合路由条件

可以用多种方法组合上面所有的路由条件,例如:

Route route = router.route(HttpMethod.PUT,"myapi/orders")

                   .consumes("application/json")

                   .produces("application/json");

 

route.handler(routingContext -> {

 

  // This would bematch for any PUT method to paths starting with "myapi/orders" with a

  // content-typeof "application/json"

  // and an acceptheader matching "application/json"

 

});

 

启动和禁用路由

disable方法禁用路由,一个禁用的路由在匹配时会被忽视,用enable方法重新启用路由。