Vert.x-Web是用Vert.x创建Web应用程序的一组构建模块的集合。
可以将Vert.x想象成构建现代,伸缩we应用的瑞士军刀。Vert.x核心模块提供相当低层功能用于处理HTTP,和应用充分使用的功能。Vert.x-Web构建于Vert.x 核心模块之上,用于提供更丰富的构建真实web应用程序的功能集,并更加简单。
对于使用Vert.x 2.x的Yoke是成功的并且从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-Type为text/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 HttpServerRequest和HttpServerResponse对象,当然也包含一些使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时设置ordered为false。
根据确切路径路由
一个设置好的路由用来匹配请求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将接到值为tools,productID将会接到值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值是0至1之间的一数值,如果忽略,默认值为1.0。
例如,下面accept头说明客户端接收text/plain的MIME类型的响应。
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/json的MIME类型响应。
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方法重新启用路由。