Vert.x Web 是一系列用于基于 Vert.x 构建 Web 应用的构建模块。css
能够把它想象成一把构建现代的、可伸缩的 Web 应用的瑞士军刀。html
Vert.x Core 提供了一系列底层的功能用于操做 HTTP,对于一部分应用来是足够的。java
Vert.x Web 基于 Vert.x Core,提供了一系列更丰富的功能以便更容易地开发实际的 Web 应用。git
它继承了 Vert.x 2.x 里的 Yoke 的特色,灵感来自于 Node.js 的框架 Express 和 Ruby 的框架 Sinatra 等等。github
Vert.x Web 的设计是强大的,非侵入式的,而且是彻底可插拔的。Vert.x Web 不是一个容器,您能够只使用您须要的部分。web
您可使用 Vert.x Web 来构建经典的服务端 Web 应用、RESTful 应用、实时的(服务端推送)Web 应用,或任何类型的您所能想到的 Web 应用。应用类型的选择取决于您,而不是 Vert.x Web。正则表达式
Vert.x Web 很是适合编写 RESTful HTTP 微服务,但咱们不强制您必须把应用实现成这样。算法
Vert.x Web 的一部分关键特性有:数据库
Vert.x Web 的大多数特性被实现为了处理器(Handler),所以您随时能够实现您本身的处理器。咱们预计随着时间的推移会有更多的处理器被实现。express
咱们会在本手册里讨论全部上述的特性。
在使用 Vert.x Web 以前,须要为您的构建工具在描述文件中添加依赖项:
pom.xml
文件中):<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> <version>3.4.2</version> </dependency>
build.gradle
文件中):dependencies { compile 'io.vertx:vertx-web:3.4.2' }
Vert.x Web 使用了 Vert.x Core 暴露的 API,因此熟悉基于 Vert.x Core 编写 HTTP 服务端的基本概念是颇有价值的。
Vert.x Core 的 HTTP 文档 有不少关于这方面的细节。
下面是一个使用 Vert.x Core 编写的 Hello World Web 服务器,暂不涉及 Vert.x Web:
HttpServer server = vertx.createHttpServer(); server.requestHandler(request -> { // 全部的请求都会调用这个处理器处理 HttpServerResponse response = request.response(); response.putHeader("content-type", "text/plain"); // 写入响应并结束处理 response.end("Hello World!"); }); server.listen(8080);
咱们建立了一个 HTTP 服务端,并设置了一个请求处理器。全部的请求都会调用这个处理器处理。
当请求到达时,咱们设置了响应的 Content Type 为 text/plain
并写入了 Hello World!
而后结束了处理。
以后,咱们告诉服务器监听 8080
端口(默认的主机名是 localhost
)。
您能够执行这段代码,并打开浏览器访问 http://localhost:8080 来验证它是否如预期的同样工做。
Router
是 Vert.x Web 的核心概念之一。它是一个维护了零或多个 Route
的对象。
Router 接收 HTTP 请求,并查找首个匹配该请求的 Route
,而后将请求传递给这个 Route
。
Route
能够持有一个与之关联的处理器用于接收请求。您能够经过这个处理器对请求作一些事情,而后结束响应或者把请求传递给下一个匹配的处理器。
如下是一个简单的路由示例:
HttpServer server = vertx.createHttpServer(); Router router = Router.router(vertx); router.route().handler(routingContext -> { // 全部的请求都会调用这个处理器处理 HttpServerResponse response = routingContext.response(); response.putHeader("content-type", "text/plain"); // 写入响应并结束处理 response.end("Hello World from Vert.x-Web!"); }); server.requestHandler(router::accept).listen(8080);
HttpServer server = vertx.createHttpServer(); Router router = Router.router(vertx); router.route().handler(routingContext -> { // 全部的请求都会调用这个处理器处理 HttpServerResponse response = routingContext.response(); response.putHeader("content-type", "text/plain"); // 写入响应并结束处理 response.end("Hello World from Vert.x-Web!"); }); server.requestHandler(router::accept).listen(8080);
它作了和上文使用 Vert.x Core 实现的 HTTP 服务器基本相同的事情,只是这一次换成了 Vert.x Web。
和上文同样,咱们建立了一个 HTTP 服务器,而后建立了一个 Router
。在这以后,咱们建立了一个没有匹配条件的 Route
,这个 route 会匹配全部到达这个服务器的请求。
以后,咱们为这个 route
指定了一个处理器,全部的请求都会调用这个处理器处理。
调用处理器的参数是一个 RoutingContext
对象。它不只包含了 Vert.x 中标准的 HttpServerRequest
和HttpServerResponse
,还包含了各类用于简化 Vert.x Web 使用的东西。
每个被路由的请求对应一个惟一的 RoutingContext
,这个实例会被传递到全部处理这个请求的处理器上。
当咱们建立了处理器以后,咱们设置了 HTTP 服务器的请求处理器,使全部的请求都经过 accept
(3)处理。
这些是最基本的,下面咱们来看一下更多的细节:
当 Vert.x Web 决定路由一个请求到匹配的 route
上,它会使用一个 RoutingContext
调用对应处理器。
若是您不在处理器里结束这个响应,您须要调用 next
方法让其余匹配的 Route
来处理请求(若是有)。
您不须要在处理器执行完毕时调用 next
方法。您能够在以后您须要的时间点调用它:
Route route1 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); // 因为咱们会在不一样的处理器里写入响应,所以须要启用分块传输 // 仅当须要经过多个处理器输出响应时才须要 response.setChunked(true); response.write("route1\n"); // 5 秒后调用下一个处理器 routingContext.vertx().setTimer(5000, tid -> routingContext.next()); }); Route route2 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route2\n"); // 5 秒后调用下一个处理器 routingContext.vertx().setTimer(5000, tid -> routingContext.next()); }); Route route3 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route3"); // 结束响应 routingContext.response().end(); });
在上述的例子中,route1
向响应里写入了数据,5秒以后 route2
向响应里写入了数据,再5秒以后 route3
向响应里写入了数据并结束了响应。
注意,全部发生的这些没有线程阻塞。
某些时候您可能须要在处理器里执行一些须要阻塞 Event Loop 的操做,好比调用某个传统的阻塞式 API 或者执行密集计算。
您不能在普通的处理器里执行这些操做,因此咱们提供了向 Route
设置阻塞式处理器的能力。
阻塞式处理器和普通处理器的区别是 Vert.x 会使用 Worker Pool 中的线程而不是 Event Loop 线程来处理请求。
您可使用 blockingHandler
方法来设置阻塞式处理器。下面是一个例子:
router.route().blockingHandler(routingContext -> { // 执行某些同步的耗时操做 service.doSomethingThatBlocks(); // 调用下一个处理器 routingContext.next(); });
默认状况下在一个 Context(Vert.x Core 的 Context
,例如同一个 Verticle 实例) 上执行的全部阻塞式处理器的执行是顺序的,也就意味着只有一个处理器执行完了才会继续执行下一个。 若是您不关心执行的顺序,而且不介意阻塞式处理器以并行的方式执行,您能够在调用 blockingHandler
方法时将 ordered
设置为 false
。
注意,若是您须要在一个阻塞处理器中处理一个 multipart 类型的表单数据,您须要首先使用一个非阻塞的处理器来调用 setExpectMultipart(true)
。 下面是一个例子:
router.post("/some/endpoint").handler(ctx -> { ctx.request().setExpectMultipart(true); ctx.next(); }).blockingHandler(ctx -> { // 执行某些阻塞操做 });
能够将 Route
设置为只匹配指定的 URI。在这种状况下它只会匹配路径和该路径一致的请求。
在下面这个例子中会被路径为 /some/path/
的请求调用。咱们会忽略结尾的 /
,因此路径 /some/path
或者 /some/path//
的请求也是匹配的:
Route route = router.route().path("/some/path/"); route.handler(routingContext -> { // 全部如下路径的请求都会调用这个处理器: // `/some/path` // `/some/path/` // `/some/path//` // // 但不包括: // `/some/path/subdir` });
您常常须要为全部以某些路径开始的请求设置 Route
。您可使用正则表达式来实现,但更简单的方式是在声明 Route
的路径时使用一个 *
做为结尾。
在下面的例子中处理器会匹配全部 URI 以 /some/path
开头的请求。
例如 /some/path/foo.html
和 /some/path/otherdir/blah.css
都会匹配。
Route route = router.route().path("/some/path/*"); route.handler(routingContext -> { // 全部路径以 `/some/path/` 开头的请求都会调用这个处理器处理,例如: // `/some/path` // `/some/path/` // `/some/path/subdir` // `/some/path/subdir/blah.html` // // 但不包括: // `/some/bath` });
也能够在建立 Route
的时候指定任意的路径:
Route route = router.route("/some/path/*"); route.handler(routingContext -> { // 这个路由器的调用规则和上面的例子同样 });
能够经过占位符声明路径参数并在处理请求时经过 params
方法获取:
如下是一个例子:
Route route = router.route(HttpMethod.POST, "/catalogue/products/:producttype/:productid/"); route.handler(routingContext -> { String productType = routingContext.request().getParam("producttype"); String productID = routingContext.request().getParam("productid"); // 执行某些操做... });
占位符由 :
和参数名构成。参数名由字母、数字和下划线构成。
在上述的例子中,若是一个 POST 请求的路径为 /catalogue/products/tools/drill123/
,那么会匹配这个 Route
,而且会接收到参数 productType
的值为 tools
,参数 productID
的值为 drill123
。
正则表达式一样也可用于在路由时匹配 URI 路径。
Route route = router.route().pathRegex(".*foo"); route.handler(routingContext -> { // 如下路径的请求都会调用这个处理器: // /some/path/foo // /foo // /foo/bar/wibble/foo // /bar/foo // 但不包括: // /bar/wibble });
或者在建立 Route
时指定正则表达式:
Route route = router.routeWithRegex(".*foo"); route.handler(routingContext -> { // 这个路由器的调用规则和上面的例子同样 });
您也能够捕捉经过正则表达式声明的路径参数,下面是一个例子:
Route route = router.routeWithRegex(".*foo"); // 这个正则表达式能够匹配路径相似于 `/foo/bar` 的请求 // `foo` 能够经过参数 param0 获取,`bar` 能够经过参数 param1 获取 route.pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(routingContext -> { String productType = routingContext.request().getParam("param0"); String productID = routingContext.request().getParam("param1"); // 执行某些操做 });
在上面的例子中,若是一个请求的路径为 /tools/drill123/
,那么会匹配这个 route,而且会接收到参数 productType
的值为 tools
,参数 productID
的值为 drill123
。
默认的,Route
会匹配全部 HTTP Method。
若是您须要 Route
只匹配指定的 HTTP Method,您可使用 method
方法。
Route route = router.route().method(HttpMethod.POST); route.handler(routingContext -> { // 全部的 POST 请求都会调用这个处理器 });
或者能够在建立这个 Route
时和路径一块儿指定:
Route route = router.route(HttpMethod.POST, "/some/path/"); route.handler(routingContext -> { // 全部路径为 `/some/path/` 的 POST 请求都会调用这个处理器 });
若是您想让 Route
指定的 HTTP Method ,您也可使用对应的 get
、post
、put
等方法。下面是一个例子:
router.get().handler(routingContext -> { // 全部 GET 请求都会调用这个处理器 }); router.get("/some/path/").handler(routingContext -> { // 全部路径为 `/some/path/` 的 GET 请求都会调用这个处理器 }); router.getWithRegex(".*foo").handler(routingContext -> { // 全部路径以 `foo` 结尾的 GET 请求都会调用这个处理器 });
若是您想要让一个路由匹配不止一个 HTTP Method,您能够调用 method 方法屡次:
Route route = router.route().method(HttpMethod.POST).method(HttpMethod.PUT); route.handler(routingContext -> { // 全部 GET 或 POST 请求都会调用这个处理器 });
默认的路由的匹配顺序与添加到 Router
的顺序一致。
当一个请求到达时,Router
会一步一步检查每个 Route
是否匹配,若是匹配则对应的处理器会被调用。
若是处理器随后调用了 next
,则下一个匹配的 Route
对应的处理器(若是有)会被调用,以此类推。
下面的例子展现了这个过程:
Route route1 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); // 因为咱们会在不一样的处理器里写入响应,所以须要启用分块传输 // 仅当须要经过多个处理器输出响应时才须要 response.setChunked(true); response.write("route1\n"); // 调用下一个匹配的 route routingContext.next(); }); Route route2 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route2\n"); // 调用下一个匹配的 route routingContext.next(); }); Route route3 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route3"); // 结束响应 routingContext.response().end(); });
在上面的例子里,响应中会包含:
route1
route2
route3
对于任意以 /some/path
开头的请求,Route
会被依次调用。
若是您想覆盖路由默认的顺序,您能够经过 order
方法为每个路由指定一个 integer 值。
当 Route
被建立时 order
会被赋值为其被添加到 Router
时的序号,例如第一个 Route
是 0,第二个是 1,以此类推。
您可使用特定的顺序值覆盖默认的顺序。若是您须要确保一个 Route
在顺序 0 的 Route
以前执行,能够将其指定为负值。
让咱们改变 route2
的值使其能在 route1
以前执行:
Route route1 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route1\n"); // 调用下一个匹配的 route routingContext.next(); }); Route route2 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); // 因为咱们会在不一样的处理器里写入响应,所以须要启用分块传输 // 仅当须要经过多个处理器输出响应时才须要 response.setChunked(true); response.write("route2\n"); // 调用下一个匹配的 route routingContext.next(); }); Route route3 = router.route("/some/path/").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route3"); // 结束响应 routingContext.response().end(); }); // 更改 route2 的顺序使其能够在 route1 以前执行 route2.order(-1);
此时响应内容会是:
route2
route1
route3
若是两个匹配的 Route
有相同的顺序值,则会按照添加它们的顺序来调用。
您也能够经过 last
方法来指定 Route
最后执行。
您可使用 consumes
方法指定 Route
匹配对应 MIME 类型的请求。
在这种状况下,若是请求中包含了消息头 content-type
声明了消息体的 MIME 类型。则它会与经过 consumes
方法声明的值进行比较。
通常来讲,consumes
描述了处理器可以处理的 MIME 类型。
MIME Type 的匹配过程是精确的:
router.route().consumes("text/html").handler(routingContext -> { // 全部 `content-type` 消息头的值为 `text/html` 的请求会调用这个处理器 });
也能够匹配多个精确的值(MIME 类型):
router.route().consumes("text/html").consumes("text/plain").handler(routingContext -> { // 全部 `content-type` 消息头的值为 `text/html` 或 `text/plain` 的请求会调用这个处理器 });
基于通配符的子类型匹配也是支持的:
router.route().consumes("text/*").handler(routingContext -> { // 全部 `content-type` 消息头的顶级类型为 `text` 的请求会调用这个处理器 // 例如 `content-type` 消息头设置为 `text/html` 或 `text/plain` 都会匹配 });
您也能够用通配符匹配顶级的类型(top level type):
router.route().consumes("*/json").handler(routingContext -> { // 全部 `content-type` 消息头的子类型为 `json` 的请求会调用这个处理器 // 例如 `content-type` 消息头设置为 `text/json` 或 `application/json` 都会匹配 });
若是您没有在 consumers 中包含 /
,则意味着是一个子类型(sub-type)。
HTTP 的 accept
消息头用于表示哪些 MIME 类型的响应是客户端可接受的。
一个 accept
消息头能够包含多个用 ,
分隔的 MIME 类型。
若是在 accept
消息头中匹配了不止一个 MIME 类型,则能够为每个 MIME 类型追加一个 q
值来表示权重。q 的取值范围由 0 到 1.0。缺省值为 1.0。
例如,下面的 accept
消息头表示客户端只接受 text/plain
类型的响应。
Accept: text/plain
如下 accept
表示客户端会无偏好地接受 text/plain
或 text/html
。
Accept: text/plain, text/html
如下 accept
表示客户端会接受 text/plain
或 text/html
,但会更倾向于 text/html
,由于其具备更高的 q
值(默认值为 1.0)。
Accept: text/plain; q=0.9, text/html
在这种状况下,若是服务器能够同时提供 text/plain
和 text/html
,它须要提供 text/html
。
您可使用 produces
来定义 Route
能够提供哪些 MIME 类型。例如如下处理器能够提供 MIME 类型为 application/json
的响应。
router.route().produces("application/json").handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.putHeader("content-type", "application/json"); response.write(someJSON).end(); });
在这种状况下这个 Route
会匹配任何 accept
消息头匹配 application/json
的请求。例如:
Accept: application/json Accept: application/* Accept: application/json, text/html Accept: application/json;q=0.7, text/html;q=0.8, text/plain
您也能够标记您的 Route
提供不止一种 MIME 类型。在这种状况下,您可使用 getAcceptableContentType
方法来找出真正被接受的 MIME 类型。
router.route().produces("application/json").produces("text/html").handler(routingContext -> { HttpServerResponse response = routingContext.response(); // 获取最终匹配到的 MIME type String acceptableContentType = routingContext.getAcceptableContentType(); response.putHeader("content-type", acceptableContentType); response.write(whatever).end(); });
在上述例子中,若是您发送一个包含以下 accept
消息头的请求:
Accept: application/json; q=0.7, text/html
那么会匹配上面的 Route
,而且 acceptableContentType
的值会是 text/html
由于其具备更高的 q
值。
您能够用不一样的方式来组合上述的路由规则,例如:
Route route = router.route(HttpMethod.PUT, "myapi/orders") .consumes("application/json") .produces("application/json"); route.handler(routingContext -> { // 这会匹配全部路径以 `/myapi/orders` 开头,`content-type` 值为 `application/json` 而且 `accept` 值为 `application/json` 的 PUT 请求 });
您能够经过 disable
方法来停用一个 Route
。停用的 Route
在匹配时会被忽略。
您能够用 enable
方法来从新启用它。
在请求的生命周期中,您能够经过路由上下文 RoutingContext
来维护任何您但愿在处理器之间共享的数据。
如下是一个例子,一个处理器设置了一些数据,另外一个处理器获取它:
您可使用 put
方法向上下文设置任何对象,使用 get
方法从上下文中获取任何对象。
一个路径为 /some/path/other
的请求会同时匹配两个 Route
:
router.get("/some/path/*").handler(routingContext -> { routingContext.put("foo", "bar"); routingContext.next(); }); router.get("/some/path/other").handler(routingContext -> { String bar = routingContext.get("foo"); // 执行某些操做 routingContext.response().end(); });
router.get("/some/path/*").handler(routingContext -> { routingContext.put("foo", "bar"); routingContext.next(); }); router.get("/some/path/other").handler(routingContext -> { String bar = routingContext.get("foo"); // 执行某些操做 routingContext.response().end(); });
另外一种您能够访问上下文数据的方式是使用 data
方法。
(4) 到目前为止,经过上述的路由机制您能够顺序地处理您的请求,但某些状况下您可能须要回退。因为处理器的顺序是动态的,路由上下文并无暴露出任何关于前一个或后一个处理器的信息。惟一的方式是在当前的 Router
里重启 Route
的流程。
router.get("/some/path").handler(routingContext -> { routingContext.put("foo", "bar"); routingContext.next(); }); router.get("/some/path/B").handler(routingContext -> { routingContext.response().end(); }); router.get("/some/path").handler(routingContext -> { routingContext.reroute("/some/path/B"); });
从代码中能够看到,若是一个到达的请求包含路径 /some/path
,首先第一个处理器向上下文添加了值,而后路由到了下一个处理器。第二个处理器转发到了路径 /some/path/B
,该处理器最后结束了响应。
您可使用路径或者同时使用路径和方法来转发。注意,基于方法的重定向可能会带来安全问题,例如将一个一般安全的 GET 请求可能会成为 DELETE。
也能够在失败处理器中转发。因为转发的性质,在这种状况下,当前的状态码和失败缘由也会被重置。所以在转发后的处理器应该根据须要生成正确的状态码,例如:
router.get("/my-pretty-notfound-handler").handler(ctx -> { ctx.response() .setStatusCode(404) .end("NOT FOUND fancy html here!!!"); }); router.get().failureHandler(ctx -> { if (ctx.statusCode() == 404) { ctx.reroute("/my-pretty-notfound-handler"); } else { ctx.next(); } });
须要澄清的是,重定向是基于路径
的。也就是说,若是您须要在重定向的过程当中添加或者保持状态,您须要使用 RoutingContext
对象。例如您但愿使用一个新的参数重定向到另一个路径:
router.get("/final-target").handler(ctx -> { // 继续作某些事情 }); // 错误的方式! (会重定向到 /final-target 路径,但不包含查询参数) router.get().handler(ctx -> { ctx.reroute("/final-target?variable=value"); }); // 正确的方式 router.get().handler(ctx -> { ctx .put("variable", "value") .reroute("/final-target"); });
虽然在重定向时会警告您查询参数会丢失,可是重定向的过程仍然会执行。而且会从路径上裁剪掉全部的查询参数或 HTML 锚点。
当您有不少处理器的状况下,合理的方式是将它们分隔为多个 Router
。这也有利于您在多个不用的应用中经过设置不一样的根路径来复用处理器。
您能够经过将一个 Router
挂载到另外一个 Router
的挂载点上来实现。挂载的 Router 被称为子路由(Sub Router)。Sub router 上也能够挂载其余的 sub router。所以,您能够包含若干级别的 sub router。
让咱们看一个 sub router 挂载到另外一个 Router
上的例子:
这个 sub router 维护了一系列处理器,对应了一个虚构的 REST API。咱们会将它挂载到另外一个 Router
上。 例子忽略了 REST API 的具体实现:
Router restAPI = Router.router(vertx); restAPI.get("/products/:productID").handler(rc -> { // TODO 查找产品信息 rc.response().write(productJSON); }); restAPI.put("/products/:productID").handler(rc -> { // TODO 添加新的产品 rc.response().end(); }); restAPI.delete("/products/:productID").handler(rc -> { // TODO 删除产品 rc.response().end(); });
若是这个 Router
是一个顶级的 Router
,那么例如 /products/product1234
这种 URL 的 GET/PUT/DELETE 请求都会调用这个 API。
若是咱们已经有了一个网站包含如下的 Router
:
Router mainRouter = Router.router(vertx); // 处理静态资源 mainRouter.route("/static/*").handler(myStaticHandler); mainRouter.route(".*\\.templ").handler(myTemplateHandler);
咱们能够将这个 sub router 经过一个挂载点挂载到主 router 上,这个例子使用了 /preoductAPI
:
mainRouter.mountSubRouter("/productsAPI", restAPI);
这意味着这个 REST API 如今能够经过这种路径访问:/productsAPI/products/product1234
。
Vert.x Web 解析 Accept-Language
消息头并提供了一些识别客户端偏好的语言,以及提供经过 quality
排序的语言偏好列表的方法。
Route route = router.get("/localized").handler( rc -> { //虽然经过一个 switch 循环有点奇怪,咱们必须按顺序选择正确的本地化方式 for (LanguageHeader language : rc.acceptableLanguages()) { switch (language.tag()) { case "en": rc.response().end("Hello!"); return; case "fr": rc.response().end("Bonjour!"); return; case "pt": rc.response().end("Olá!"); return; case "es": rc.response().end("Hola!"); return; } } // 咱们不知道用户的语言,所以返回这个信息: rc.response().end("Sorry we don't speak: " + rc.preferredLocale()); });
方法 acceptableLocales
会返回客户端可以理解的排序好的语言列表。 若是您只关心用户偏好的语言,那么使用 preferredLocale
会返回列表的第一个元素。 若是用户没有提供,则返回空。
若是没有为请求匹配到任何路由,Vert.x Web 会声明一个 404 错误。
这能够被您本身实现的处理器处理,或者被咱们提供的专用错误处理器(failureHandler
)处理。 若是没有提供错误处理器,Vert.x Web 会发送一个基本的 404 (Not Found) 响应。
和设置处理器处理请求同样,您能够设置处理器处理路由过程当中的失败。
失败处理器和普通的处理器具备彻底同样的路由匹配规则。
例如您能够提供一个失败处理器只处理在某个路径上发生的失败,或某个 HTTP 方法。
这容许您在应用的不一样部分设置不一样的失败处理器。
下面例子中的失败处理器只会在路由路径为 /somepath/
的 GET 请求失败时被调用:
Route route = router.get("/somepath/*"); route.failureHandler(frc -> { // 若是在处理路径以 `/somepath/` 开头的请求过程当中发生错误,会调用这个处理器 });
当一个处理器抛出异常,或者一个处理器经过了 fail
方法指定了 HTTP 状态码时,会执行路由的失败处理。
从一个处理器捕捉到异常时会标记一个状态码为 500
的错误。
在处理这个错误时,RoutingContext
会被传递到失败处理器里,失败处理器能够经过获取到的错误或错误编码来构造失败的响应内容。
Route route1 = router.get("/somepath/path1/"); route1.handler(routingContext -> { // 这里抛出一个 RuntimeException throw new RuntimeException("something happened!"); }); Route route2 = router.get("/somepath/path2"); route2.handler(routingContext -> { // 这里故意将请求处理为失败状态 // 例如 403 - 禁止访问 routingContext.fail(403); }); // 定义一个失败处理器,上述的处理器发生错误时会调用这个处理器 Route route3 = router.get("/somepath/*"); route3.failureHandler(failureRoutingContext -> { int statusCode = failureRoutingContext.statusCode(); // 对于 RuntimeException 状态码会是 500,不然是 403 HttpServerResponse response = failureRoutingContext.response(); response.setStatusCode(statusCode).end("Sorry! Not today"); });
某些状况下失败处理器会因为使用了不支持的字符集做为状态消息而致使错误。在这种状况下,Vert.x Web 会将状态消息替换为状态码的默认消息。 这是为了保证 HTTP 协议的语义,而不至于崩溃并断开 socket 致使协议运行的不完整。
您可使用消息体处理器 BodyHandler
来获取请求的消息体,限制消息体大小,或者处理文件上传。
您须要保证消息体处理器可以匹配到全部您须要这个功能的请求。
因为它须要在全部异步执行以前处理请求的消息体,所以这个处理器要尽量早地设置到 router 上。
router.route().handler(BodyHandler.create());
若是您知道消息体的类型是 JSON,您可使用 getBodyAsJson
;若是您知道它的类型是字符串,您可使用 getBodyAsString
;不然能够经过 getBody
做为 Buffer
来处理。
若是要限制请求消息体的大小,能够在建立消息体处理器时使用 setBodyLimit
来指定消息体的最大字节数。这对于规避因为过大的消息体致使的内存溢出的问题颇有用。
若是尝试发送一个大于最大值的消息体,则会获得一个 HTTP 状态码 413 - Request Entity Too Large
的响应。
默认的没有消息体大小限制。
消息体处理器默认地会合并表单属性到请求的参数里。 若是您不须要这个行为,能够经过 setMergeFormAttributes
来禁用。
消息体处理器也能够用于处理 Multipart 的文件上传。
当消息体处理器匹配到请求时,全部上传的文件会被自动地写入到上传目录中,默认的该目录为 file-uploads
。
每个上传的文件会被自动生成一个文件名,并能够经过 RoutingContext
的 fileUploads
来得到。
如下是一个例子:
router.route().handler(BodyHandler.create()); router.post("/some/path/uploads").handler(routingContext -> { Set<FileUpload> uploads = routingContext.fileUploads(); // 执行上传处理 });
每个上传的文件经过一个 FileUpload
对象来描述,经过这个对象能够得到名称、文件名、大小等属性。
Vert.x Web 经过 Cookie 处理器 CookieHandler
来支持 cookie。
您须要保证 cookie 处理器器可以匹配到全部您须要这个功能的请求。
router.route().handler(CookieHandler.create());
您可使用 getCookie
来经过名称获取 cookie 值,或者使用 cookies
获取整个集合。
使用 removeCookie
来删除 cookie。
使用 addCookie
来添加 cookie。
当向响应中写入响应消息头时,cookie 的集合会自动被回写到响应里,这样浏览器就能够存储下来。
cookie 是使用 Cookie
对象来表述的。您能够经过它来获取名称、值、域名、路径或 cookie 的其余属性。
如下是一个查询和添加 cookie 的例子:
router.route().handler(CookieHandler.create()); router.route("some/path/").handler(routingContext -> { Cookie someCookie = routingContext.getCookie("mycookie"); String cookieValue = someCookie.getValue(); // 使用 cookie 执行某些操做 // 添加一个 cookie,会自动回写到响应里 routingContext.addCookie(Cookie.cookie("othercookie", "somevalue")); });
Vert.x Web 提供了开箱即用的会话(session)支持。
会话维持了 HTTP 请求和浏览器会话之间的关系,并提供了能够设置会话范围的信息的能力,例如一个购物篮。
Vert.x Web 使用会话 cookie(5) 来标示一个会话。会话 cookie 是临时的,当浏览器关闭时会被删除。
咱们不会在会话 cookie 中设置实际的会话数据,这个 cookie 只是在服务器上查找实际的会话数据时使用的标示。这个标示是一个经过安全的随机过程生成的 UUID,所以它是没法推测的(6)。
Cookie 会在 HTTP 请求和响应之间传递。所以经过 HTTPS 来使用会话功能是明智的。若是您尝试直接经过 HTTP 使用会话,Vert.x Web 会给于警告。
您须要在匹配的 Route
上注册会话处理器 SessionHandler
来启用会话功能,并确保它可以在应用逻辑以前执行。
会话处理器会建立会话 Cookie 并查找会话信息,您不须要本身来实现。
您须要提供一个会话存储对象来建立会话处理器。会话存储用于维持会话数据。
会话存储持有一个伪随机数生成器(PRNG)用于安全地生成会话标示。PRNG 是独立于存储的,这意味着对于给定的存储 A 的会话标示是不可以派发出存储 B 的会话标示的,由于他们具备不一样的种子和状态。
PRNG 默认使用混合模式,阻塞式地刷新种子,非阻塞式地生成随机数(7)。PRNG 会每隔 5 分钟使用一个新的 64 位的熵做为种子。这个策略能够经过系统属性来设置:
io.vertx.ext.auth.prng.algorithm
e.g.: SHA1PRNGio.vertx.ext.auth.prng.seed.interval
e.g.: 1000 (every second)io.vertx.ext.auth.prng.seed.bits
e.g.: 128大多数用户并不须要配置这些值,除非您发现应用的性能被 PRNG 的算法所影响。
Vert.x Web 提供了两种开箱即用的会话存储实现,您也能够编写您本身的实现。
该存储将会话保存在内存中,并只在当前实例中有效。
这个存储适用于您只有一个 Vert.x 实例的状况,或者您正在使用粘性会话。也就是说您能够配置您的负载均衡器来确保全部请求(来自同一用户的)永远被派发到同一个 Vert.x 实例上。
若是您不可以保证这一点,那么就不要使用这个存储。这会致使请求被派发到没法识别这个会话的服务器上。
本地会话存储基于本地的共享 Map来实现,并包含了一个用于清理过时会话的回收器。
回收的周期能够经过 LocalSessionStore
.create 来配置。
如下是一些建立 LocalSessionStore
的例子:
SessionStore store1 = LocalSessionStore.create(vertx); // 经过指定的 Map 名称建立了一个本地会话存储 // 这适用于您在同一个 Vert.x 实例中有多个应用,而且但愿不一样的应用使用不一样的 Map 的状况 SessionStore store2 = LocalSessionStore.create(vertx, "myapp3.sessionmap"); // 经过指定的 Map 名称建立了一个本地会话存储 // 设置了检查过时 Session 的周期为 10 秒 SessionStore store3 = LocalSessionStore.create(vertx, "myapp3.sessionmap", 10000);
该存储将会话保存在分布式 Map 中,该 Map 能够在 Vert.x 集群中共享访问。
这个存储适用于您没有使用粘性会话的状况。好比您的负载均衡器会未来自同一个浏览器的不一样请求转发到不一样的服务器上。
经过这个存储,您的会话能够被集群中的任何节点访问。
若是要使用集群会话存储,您须要确保您的 Vert.x 实例是集群模式的。
如下是一些建立 ClusteredSessionStore
的例子:
Vertx.clusteredVertx(new VertxOptions().setClustered(true), res -> { Vertx vertx = res.result(); // 建立了一个默认的集群会话存储 SessionStore store1 = ClusteredSessionStore.create(vertx); // 经过指定的 Map 名称建立了一个集群会话存储 // 这适用于您在集群中有多个应用,而且但愿不一样的应用使用不一样的 Map 的状况 SessionStore store2 = ClusteredSessionStore.create(vertx, "myclusteredapp3.sessionmap"); });
当您建立会话存储以后,您能够建立一个会话处理器,并添加到 Route
上。您须要确保会话处理器在您的应用处理器以前被执行。
因为会话处理器须要使用 Cookie 来查找会话,所以您还须要包含一个 Cookie 处理器。这个 Cookie 处理器须要在会话处理器以前被执行。
如下是例子:
Router router = Router.router(vertx); // 咱们首先须要一个 cookie 处理器 router.route().handler(CookieHandler.create()); // 用默认值建立一个集群会话存储 SessionStore store = ClusteredSessionStore.create(vertx); SessionHandler sessionHandler = SessionHandler.create(store); // 确保全部请求都会通过 session 处理器 router.route().handler(sessionHandler); // 您本身的应用处理器 router.route("/somepath/blah/").handler(routingContext -> { Session session = routingContext.session(); session.put("foo", "bar"); // etc });
会话处理器会自动从会话存储中查找会话(若是没有则建立),并在您的应用处理器执行以前设置在上下文中。
在您的处理器中,您能够经过 session
方法来访问会话对象。
您能够经过 put
方法来向会话中设置数据,经过 get
方法来获取数据,经过 remove
方法来删除数据。
会话中的键的类型必须是字符串。本地会话存储的值能够是任何类型;集群会话存储的值类型能够是基本类型,或者 Buffer
、JsonObject
、JsonArray
或可序列化对象。由于这些值须要在集群中进行序列化。
如下是操做会话数据的例子:
router.route().handler(CookieHandler.create()); router.route().handler(sessionHandler); // 您的应用处理器 router.route("/somepath/blah").handler(routingContext -> { Session session = routingContext.session(); // 向会话中设置值 session.put("foo", "bar"); // 从会话中获取值 int age = session.get("age"); // 从会话中删除值 JsonObject obj = session.remove("myobj"); });
在响应完成后会话会自动回写到存储中。
您可使用 destroy
方法来销毁一个会话。这会将这个会话同时从上下文和存储中删除。注意,在删除会话以后,下一次经过浏览器访问并通过会话处理器处理时,会自动建立新的会话。
若是会话在指定的周期内没有被访问,则会超时。
当请求到达,访问了会话,而且在响应完成向会话存储回写会话时,会话会被标记为被访问的。
您也能够经过 setAccessed
来人工指定会话被访问。
能够在建立会话处理器时配置超时时间。默认的超时时间是 30 分钟。
Vert.x Web 提供了若干开箱即用的处理器来处理认证和受权。
您须要一个 AuthProvider
实例来建立认证处理器。Auth Provider 用于为用户提供认证和受权。Vert.x 在 vertx-auth
项目中提供了若干开箱即用的 Auth Provider。完整的 Auth Provider 的配置和用法请参考 Vertx Auth 的文档。
如下是一个使用 Auth Provider 来建立认证处理器的例子:
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
咱们来假设您但愿全部路径为 /private
的请求都须要认证控制。为了实现这个,您须要确保您的认证处理器匹配这个路径,并在您的应用处理器以前执行:
router.route().handler(CookieHandler.create()); router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); router.route().handler(UserSessionHandler.create(authProvider)); AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider); // 全部路径以 `/private` 开头的请求会被保护 router.route("/private/*").handler(basicAuthHandler); router.route("/someotherpath").handler(routingContext -> { // 此处是公开的,不须要登陆 }); router.route("/private/somepath").handler(routingContext -> { // 此处须要登陆 // 这个值会返回 true boolean isAuthenticated = routingContext.user() != null; });
若是认证处理器完成了受权和认证,它会向 RoutingContext
中注入一个 User
对象。您能够经过 user
方法在您的处理器中获取到该对象。
若是您但愿在回话中存储用户对象,以免对全部的请求都执行认证过程,您须要使用会话处理器。确保它匹配了对应的路径,而且会在认证处理器以前执行。
一旦您获取到了 user
对象,您能够经过编程的方式来使用它的相关方法为用户受权。
若是您但愿用户登出,您能够调用上下文的 clearUser
方法。
HTTP基础认证是适用于简单应用的简单认证手段。
在这种认证方式下, 证书会以非加密的形式在 HTTP 请求中传输。所以,使用 HTTPS 而非 HTTP 来实现您的应用是很是必要的。
当用户请求一个须要受权的资源,基础认证处理器会返回一个包含 WWW-Authenticate
消息头的 401
响应。浏览器会显示一个登陆窗口并提示用户输入他们的用户名和密码。
在这以后,浏览器会从新发送这个请求,并将用户名和密码以 Base64 编码的形式包含在请求的Authorization
消息头里。
当基础认证处理器收到了这些信息,它会使用用户名和密码调用配置的 AuthProvider
来认证用户。若是认证成功则该处理器会尝试用户受权,若是也成功了则容许这个请求路由到后续的处理器里处理。不然,会返回一个 403
的响应拒绝访问。
在设置认证处理器时能够指定一系列访问资源时须要的权限。
重定向认证处理器用于当未登陆的用户尝试访问受保护的资源时将他们重定向到登陆页上。
当用户提交登陆表单,服务器会处理用户认证。若是成功,则将用户重定向到原始的资源上。
则您能够配置一个 RedirectAuthHandler
对象来使用重定向处理器。
您还须要配置用于处理登陆页面的处理器,以及实际处理登陆的处理器。咱们提供了一个内置的处理器 FormLoginHandler
来处理登陆的问题。
这里是一个简单的例子,使用了一个重定向认证处理器并使用默认的重定向 url /loginpage
。
router.route().handler(CookieHandler.create()); router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); router.route().handler(UserSessionHandler.create(authProvider)); AuthHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider); // 全部路径以 `/private` 开头的请求会被保护 router.route("/private/*").handler(redirectAuthHandler); // 处理登陆请求 // 您的登陆页须要 POST 登陆表单数据 router.post("/login").handler(FormLoginHandler.create(authProvider)); // 处理静态资源,例如您的登陆页 router.route().handler(StaticHandler.create()); router.route("/someotherpath").handler(routingContext -> { // 此处是公开的,不须要登陆 }); router.route("/private/somepath").handler(routingContext -> { // 此处须要登陆 // 这个值会返回 true boolean isAuthenticated = routingContext.user() != null; });
JWT 受权经过权限来保护资源不被未为受权的用户访问。
使用这个处理器涉及 2 个步骤:
注意,这两个处理器应该只能经过 HTTPS 访问。不然可能会引发由流量嗅探引发的会话劫持。
这里是一个派发令牌的例子:
Router router = Router.router(vertx); JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject() .put("type", "jceks") .put("path", "keystore.jceks") .put("password", "secret")); JWTAuth authProvider = JWTAuth.create(vertx, authConfig); router.route("/login").handler(ctx -> { // 这是一个例子,认证会由另外一个 provider 执行 if ("paulo".equals(ctx.request().getParam("username")) && "secret".equals(ctx.request().getParam("password"))) { ctx.response().end(authProvider.generateToken(new JsonObject().put("sub", "paulo"), new JWTOptions())); } else { ctx.fail(401); } });
注意,对于持有令牌的客户端,惟一须要作的是在 全部 后续的的 HTTP 请求中包含消息头 Authorization
并写入 Bearer <token>
,例如:
Router router = Router.router(vertx); JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject() .put("type", "jceks") .put("path", "keystore.jceks") .put("password", "secret")); JWTAuth authProvider = JWTAuth.create(vertx, authConfig); router.route("/protected/*").handler(JWTAuthHandler.create(authProvider)); router.route("/protected/somepage").handler(ctx -> { // 一些处理过程 });
JWT 容许您向令牌中添加任何您须要的信息,只须要在建立令牌时向 JsonObject
参数中添加数据便可。这样作服务器上不存在任何的会话状态,您能够在不依赖集群会话数据的状况下对应用进行扩展。
JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject() .put("type", "jceks") .put("path", "keystore.jceks") .put("password", "secret")); JWTAuth authProvider = JWTAuth.create(vertx, authConfig); authProvider.generateToken(new JsonObject().put("sub", "paulo").put("someKey", "some value"), new JWTOptions());
在消费时用一样的方式:
Handler<RoutingContext> handler = rc -> { String theSubject = rc.user().principal().getString("sub"); String someKey = rc.user().principal().getString("someKey"); };
您能够对认证处理器配置访问资源所需的权限。
默认的,若是不配置权限,那么只要登陆了就能够访问资源。不然,用户不只须要登陆,并且须要具备所需的权限。
如下的例子定义了一个应用,该应用的不一样部分须要不一样的权限。注意,权限的含义取决于您使用的的 Auth Provider。例如一些支持角色/权限的模型,另外一些多是其余的模型。
AuthHandler listProductsAuthHandler = RedirectAuthHandler.create(authProvider); listProductsAuthHandler.addAuthority("list_products"); // 须要 `list_products` 权限来列举产品 router.route("/listproducts/*").handler(listProductsAuthHandler); AuthHandler settingsAuthHandler = RedirectAuthHandler.create(authProvider); settingsAuthHandler.addAuthority("role:admin"); // 只有 `admin` 能够访问 `/private/settings` router.route("/private/settings/*").handler(settingsAuthHandler);
Vert.x Web 提供了一个开箱即用的处理器来提供静态的 Web 资源。您能够很是容易地编写静态的 Web 服务器。
您可使用静态资源处理器 StaticHandler
来提供诸如 .html
、.css
、.js
或其余类型的静态资源。
每个被静态资源处理器处理的请求都会返回文件系统的某个目录或 classpath 里的文件。文件的根目录是能够配置的,默认为 webroot
。
在如下的例子中,全部路径以 /static
开头的请求都会对应到 webroot
目录:
router.route("/static/*").handler(StaticHandler.create());
例如,对于一个路径为 /static/css/mystyles.css
的请求,静态处理器会在该路径中查找文件 webroot/css/mystyle.css
。
它也会在 classpath 中查找文件 webroot/css/mystyle.css
。这意味着您能够将全部的静态资源打包到一个 jar 文件(或 fat-jar)里进行分发。
当 Vert.x 在 classpath 中第一次找到一个资源时,会将它提取到一个磁盘的缓存目录中以免每一次都从新提取。
这个处理器可以处理范围请求。当客户端请求静态资源时,该处理器会添加一个范围单位的说明到响应的消息头 Accept-Ranges
里来通知客户端它支持范围请求。若是后续请求的消息头 Range
里包含了正确的单位以及起始、终止位置,则客户端将收到包含了的 Content-Range
消息头的部分响应。
默认的,为了让浏览器有效地缓存文件,静态处理器会设置缓存消息头。
Vert.x Web 会在响应里设置这些消息头:cache-control
、last-modified
、date
。
cache-control
的默认值为 max-age=86400
,也就是一天。能够经过 setMaxAgeSeconds
方法来配置。
当浏览器发送了携带消息头 if-modified-since
的 GET 或 HEAD 请求时,若是对应的资源在该日期以后没有修改过,则会返回一个 304
状态码通知浏览器使用本地的缓存资源。
若是不须要缓存的消息头,能够经过 setCachingEnabled
方法将其禁用。
若是启用了缓存处理,则 Vert.x Web 会将资源的最后修改日期缓存在内存里,以此来避免频繁地访问取磁盘来检查修改时间。
缓存有过时时间,在这个时间以后,会从新访问磁盘检查文件并更新缓存。
默认的,若是您的文件永远不会发生变化,则缓存内容会永远有效。
若是您的文件在服务器运行过程当中可能发生变化,您能够经过 setFilesReadOnly
方法设置文件的只读属性为 false。
您能够经过 setMaxCacheSize
方法来设置内存缓存的最大数量。经过 setCacheEntryTimeout
方法来设置缓存的过时时间。
全部访问根路径 /
的请求会被定位到索引页。默认的该文件为 index.html
。能够经过 setIndexPage
方法来设置。
默认的,全部资源都以 webroot
做为根目录。能够经过 setWebRoot
方法来配置。
默认的,处理器会为隐藏文件提供服务(文件名以 .
开头的文件)。
若是您不须要为隐藏文件提供服务,能够经过 setIncludeHidden
方法来配置。
静态资源处理器能够用于列举目录的文件。默认状况下该功能是关闭的。能够经过 setDirectoryListing
方法来启用。
当该功能启用时,会根据客户端请求的消息头 accept
所表示的类型来返回相应的结果。
例如对于 text/html
标示的请求,会使用经过 setDirectoryTemplate
方法设置的模板来渲染文件列表。
默认状况下,Vert.x 会使用当前工做目录的子目录 .vertx
来在磁盘上缓存经过 classpath 服务的静态资源。这对于在生产环境中经过 fat-jar 来部署的服务是很重要的。由于每一次都经过 classpath 来提取文件是低效的。
这在开发时会致使一个问题,当您在服务运行过程当中修改了静态内容,缓存的文件是不会被更新的。
您能够设置 vert.x 的 fileResolverCachingEnabled
选项为 true
来禁用文件缓存。为了向后兼容,它会从 vertx.disableFileCaching
这个系统属性里来提取默认值。例如,您若是从 IDE 来启动您的应用程序,能够在 IDE 的运行配置中来配置这个属性。
跨域资源共享(CORS,Cross Origin Resource Sharing)是一个安全机制。该机制容许了浏览器在一个域名下访问另外一个域名的资源。
Vert.x Web 提供了一个处理器 CorsHandler
来为您处理 CORS 协议。
这是一个例子:
router.route().handler(CorsHandler.create("vertx\\.io").allowedMethod(HttpMethod.GET)); router.route().handler(routingContext -> { // 您的应用处理 });
Vert.x Web 为若干流行的模板引擎提供了开箱即用的支持,经过这种方式来提供生成动态页面的能力。您也能够很容易地添加您本身的实现。
模板引擎 TemplateEngine
定义了使用模板引擎的接口,当渲染模板时会调用 render
方法。
最简单的使用模板的方式不是直接调用模板引擎,而是使用模板处理器 TemplateHandler
。这个处理器会根据 HTTP 请求的路径来调用模板引擎。
默认的,模板处理器会在 templates
目录中查找模板文件。这是能够配置的。
该处理器会返回渲染的结果,并默认设置 Content-Type
消息头为 text/html
。这也是能够配置的。
您须要在建立模板处理器时提供您须要使用的模板引擎的实例。
模板引擎的实现没有内嵌在 Vert.x Web 里,您须要配置您的项目来访问它们。Vert.x Web 提供了每一种模板引擎的配置。
如下是一个例子:
TemplateEngine engine = HandlebarsTemplateEngine.create(); TemplateHandler handler = TemplateHandler.create(engine); // 这会将全部以 `/dynamic` 开头的请求路由到模板处理器上 // 例如 /dynamic/graph.hbs 会查找模板 /templates/graph.hbs router.get("/dynamic/*").handler(handler); // 将全部以 `.hbs` 结尾的请求路由到模板处理器上 router.getWithRegex(".+\\.hbs").handler(handler);
您须要在您的项目中添加这些依赖来使用 MVEL 模板引擎:io.vertx:vertx-web-templ-mvel:3.4.2
。经过这个方法来建立 MVEL 模板引擎的实例:io.vertx.ext.web.templ.MVELTemplateEngine#create()
。
在使用 MVEL 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .templ
的文件。
在 MVEL 模板中能够经过 context
变量来访问路由上下文 RoutingContext
对象。这也意味着您能够基于上下文里的任何信息来渲染模板,包括请求、响应、会话或者上下文数据。
这是一个例子:
The request path is @{context.request().path()} The variable 'foo' from the session is @{context.session().get('foo')} The value 'bar' from the context data is @{context.get('bar')}
关于如何编写 MVEL 模板,请参考 MVEL 模板文档。
译者注:Jade 已改名为 Pug。
您须要在您的项目中添加这些依赖来使用 Jade 模板引擎:io.vertx:vertx-web-templ-jade:3.4.2
。经过这个方法来建立 Jade 模板引擎的实例:io.vertx.ext.web.templ.JadeTemplateEngine#create()
。
在使用 Jade 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .jade
的文件。
在 Jade 模板中能够经过 context
变量来访问路由上下文 RoutingContext
对象。这也意味着您能够基于上下文里的任何信息来渲染模板,包括请求、响应、会话或者上下文数据。
这是一个例子:
!!! 5 html head title= context.get('foo') + context.request().path() body
关于如何编写 Jade 模板,请参考 Jade4j 文档。
您须要在您的项目中添加这些依赖来使用 Handlebars:io.vertx:vertx-web-templ-handlebars:3.4.2
。经过这个方法来建立 Handlebars 模板引擎的实例:io.vertx.ext.web.templ.HandlebarsTemplateEngine#create()
。
在使用 Handlebars 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .hbs
的文件。
Handlebars 不容许在模板中随意地调用对象的方法,所以咱们不能像对待其余模板引擎同样将路由上下文传递到引擎里并让模板来识别它。
替代方案是,可使用 data
来访问上下文数据。
若是您要访问某些上下文数据里不存在的信息,好比请求的路径、请求参数或者会话等,您须要在模板处理器执行以前将他们添加到上下文数据里,例如:
TemplateHandler handler = TemplateHandler.create(engine); router.get("/dynamic").handler(routingContext -> { routingContext.put("request_path", routingContext.request().path()); routingContext.put("session_data", routingContext.session().data()); routingContext.next(); }); router.get("/dynamic/").handler(handler);
关于如何编写 Handlebars 模板,请参考 Handlebars Java 文档。
您须要在您的项目中添加这些依赖来使用 Thymeleaf:io.vertx:vertx-web-templ-thymeleaf:3.4.2
。经过这个方法来建立 Thymeleaf 模板引擎的实例:
io.vertx.ext.web.templ.ThymeleafTemplateEngine#create()。
在使用 Thymeleaf 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .html
的文件。
在 Thymeleaf 模板中能够经过 context
变量来访问路由上下文 RoutingContext
对象。这也意味着您能够基于上下文里的任何信息来渲染模板,包括请求、响应、会话或者上下文数据。
这是一个例子:
[snip] <p th:text="${context.get('foo')}"></p> <p th:text="${context.get('bar')}"></p> <p th:text="${context.normalisedPath()}"></p> <p th:text="${context.request().params().get('param1')}"></p> <p th:text="${context.request().params().get('param2')}"></p> [snip]
关于如何编写 Thymeleaf 模板,请参考 Thymeleaf 文档。
您须要在您的项目中添加这些依赖来使用 Apache FreeMarker:io.vertx:vertx-web-templ-freemarker:3.4.2
。经过这个方法来建立 Apache FreeMarker 模板引擎的实例:io.vertx.ext.web.templ.FreeMarkerTemplateEngine#create()
。
在使用 Apache FreeMarker 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .ftl
的文件。
在 Apache FreeMarker 模板中能够经过 context
变量来访问路由上下文 RoutingContext
对象。这也意味着您能够基于上下文里的任何信息来渲染模板,包括请求、响应、会话或者上下文数据。
这是一个例子:
[snip] <p th:text="${context.foo}"></p> <p th:text="${context.bar}"></p> <p th:text="${context.normalisedPath()}"></p> <p th:text="${context.request().params().param1}"></p> <p th:text="${context.request().params().param2}"></p> [snip]
关于如何编写 Apache FreeMarker 模板,请参考 Apache FreeMarker 文档。
您须要在您的项目中添加这些依赖来使用 Pebble:io.vertx:vertx-web-templ-pebble:3.4.0-SNAPSHOT
。经过这个方法来建立 Pebble 模板引擎的实例:io.vertx.ext.web.templ.PebbleTemplateEngine#create()
。
在使用 Pebble 模板引擎时,若是不指定模板文件的扩展名,则默认会查找扩展名为 .peb
的文件。
在 Pebble 模板中能够经过 context
变量来访问路由上下文 RoutingContext
对象。这也意味着您能够基于上下文里的任何信息来渲染模板,包括请求、响应、会话或者上下文数据。
这是一个例子:
[snip] <p th:text="{{context.foo}}"></p> <p th:text="{{context.bar}}"></p> <p th:text="{{context.normalisedPath()}}"></p> <p th:text="{{context.request().params().param1}}"></p> <p th:text="{{context.request().params().param2}}"></p> [snip]
关于如何编写 Pebble 模板,请参考 Pebble 文档。
在开发时,为了让每一次请求能够从新读取模板内容,您可能但愿禁用模板的缓存。这能够经过设置系统属性 io.vertx.ext.web.TemplateEngine.disableCache
为 true
来实现。
默认的这个值为 false
,也就是开启模板缓存。
您能够用模板处理器来渲染错误信息,或者使用 Vert.x Web 内置的一个 ”漂亮“ 的、开箱即用的错误处理器来渲染错误页面。
这个处理器是 ErrorHandler
。您只须要在须要覆盖到的路径上将它设置为失败处理器(9)来使用它。
Vert.x Web 提供了一个用于记录 HTTP 请求的处理器 LoggerHandler
。
默认的,请求会经过 Vert.x 日志来记录,或者也能够配置为 jul 日志、log4j 或 slf4j。详见 LoggerFormat
。
Vert.x Web 经过内置的处理器 FaviconHandler
来提供网页图标。
图标能够指定为文件系统上的某个路径,不然 Vert.x Web 默认会在 classpath 上寻找 favicon.ico
文件。这意味着您能够将图标打包到您的应用的 jar 包里。
Vert.x Web 提供了一个超时处理器,能够在处理时间过长时将请求超时。
经过 TimeoutHandler 对象来进行配置。
若是一个请求在响应以前超时,则会给客户端返回一个 503
的响应。
下面的例子设置了一个超时处理器。对于全部以 /foo
路径开头的请求,都会在执行 5 秒后自动超时。
router.route("/foo/").handler(TimeoutHandler.create(5000));
该处理器会将从接收到请求到写入响应的消息头之间的毫秒数写入到响应的 x-response-time
里,例如:
x-response-time: 1456ms
该处理器 ResponseContentTypeHandler
会自动设置响应的 Content-Type
消息头。假设咱们要构建一个 RESTful 的 Web 应用,咱们须要在全部处理器里设置 Content-Type
:
router.get("/api/books").produces("application/json").handler(rc -> { findBooks(ar -> { if (ar.succeeded()) { rc.response().putHeader("Content-Type", "application/json").end(toJson(ar.result())); } else { rc.fail(ar.cause()); } }); });
随着 API 接口数量的增加,设置 Content-Type
会变得很麻烦。能够经过在 Route
上添加 ResponseContentTypeHandler
来避免这个问题:
router.route("/api/*").handler(ResponseContentTypeHandler.create()); router.get("/api/books").produces("application/json").handler(rc -> { findBooks(ar -> { if (ar.succeeded()) { rc.response().end(toJson(ar.result())); } else { rc.fail(ar.cause()); } }); });
这个处理器会经过 getAcceptableContentType
方法来选择适当的 Content-Type
。所以,您能够很容易地使用同一个处理器来提供不一样类型的数据:
router.route("/api/*").handler(ResponseContentTypeHandler.create()); router.get("/api/books").produces("text/xml").produces("application/json").handler(rc -> { findBooks(ar -> { if (ar.succeeded()) { if (rc.getAcceptableContentType().equals("text/xml")) { rc.response().end(toXML(ar.result())); } else { rc.response().end(toJson(ar.result())); } } else { rc.fail(ar.cause()); } }); });
SockJS 是一个客户端的 JavaScript 库。它提供了相似 WebSocket 的接口为您和 SockJS 服务端创建链接。您没必要关注浏览器或网络是否真的是 WebSocket。
它提供了若干不一样的传输方式,并在运行时根据浏览器和网络的兼容性来选择使用哪一种传输方式处理。
全部这些对您是透明的,您只须要简单地使用相似 WebSocket 的接口。
请访问 SockJS 官方网站 来获取 SockJS 的详细信息。
Vert.x Web 提供了一个开箱即用的处理器 SockJSHandler
来让您在 Vert.x Web 应用中使用 SockJS。
您须要经过 SockJSHandler.create
方法为每个 SockJS 的应用建立这个处理器。您也能够在建立处理器时经过 SockJSHandlerOptions
对象来指定配置选项。
Router router = Router.router(vertx); SockJSHandlerOptions options = new SockJSHandlerOptions().setHeartbeatInterval(2000); SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options); router.route("/myapp/*").handler(sockJSHandler);
您能够在服务器端设置一个处理器,这个处理器会在每次客户端建立链接时被调用:
调用这个处理器的参数是一个 SockJSSocket
对象。这是一个相似套接字的接口,您能够向使用 NetSocket
和 WebSocket
那样经过它来读写数据。它实现了 ReadStream
和 WriteStream
接口,所以您能够将它套用(pump)到其余的读写流上。
下面的例子中的 SockJS 处理器直接使用了它读取到的数据进行回写:
Router router = Router.router(vertx); SockJSHandlerOptions options = new SockJSHandlerOptions().setHeartbeatInterval(2000); SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options); sockJSHandler.socketHandler(sockJSSocket -> { // 将数据回写 sockJSSocket.handler(sockJSSocket::write); }); router.route("/myapp/*").handler(sockJSHandler);
在客户端 JavaScript 环境里您须要经过 SockJS 的客户端库来创建链接。
完整的细节能够在 SockJS 的网站 中找到,简单来讲您会像这样使用:
var sock = new SockJS('http://mydomain.com/myapp'); sock.onopen = function() { console.log('open'); }; sock.onmessage = function(e) { console.log('message', e.data); }; sock.onclose = function() { console.log('close'); }; sock.send('test'); sock.close();
能够经过 SockJSHandlerOptions
对象来配置这个处理器的若干选项。
insertJSESSIONID
在 cookie 中插入一个 JSESSIONID,这样负载均衡器能够保证 SockJS 会话永远转发到正确的服务器上。默认值为 true
。
sessionTimeout
对于一个正在接收响应的客户端链接,若是一段时间内没有动做,则服务端会发出一个 close
事件。延时时间由这个配置决定。默认的服务端会在 5 秒以后发出这个 close
事件。(10)
heartbeatInterval
咱们会每隔一段时间发送一个心跳包,用来避免因为请求时间过长致使链接被代理和负载均衡器关闭。默认的每隔 25 秒发送一个心跳包,能够经过这个设置来控制频率。
maxBytesStreaming
大多数流式传输方式会在客户端保存响应的内容而且不会释放派发消息所使用的内存。这些传输方式须要按期执行垃圾回收。max_bytes_streaming
设置了每个 http 流式请求所须要发送的最小字节数。超过这个值则客户端须要打开一个新的请求。将这个值设置得太小会失去流式的处理能力,使这个流式的传输方式表现得像一个轮训的传输方式同样。默认值是 128K。
libraryURL
对于没有提供原生的跨域通讯支持的浏览器,会使用 iframe 来进行通讯。SockJS 服务器会提供一个简单的页面(在目标域名上)并放置在一个不可见的 iframe 里。在 iframe 里运行的代码和 SockJS 服务器运行在同一个域名下,所以不用担忧跨域的问题。这个 iframe 也须要加载 SockJS 的客户端 JavaScript 库,这个配置就是用于指定这个 URL 的。默认状况下会使用最新发布的压缩版本 http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js。
disabledTransports
这个参数用于禁用某些传输方式。可能的值包括 WEBSOCKET、EVENT_SOURCE、HTML_FILE、JSON_P、XHR。
Vert.x Web 提供了一个内置的叫作 Event Bus Bridge 的 SockJS 套接字处理器。该处理器用于将服务器端的 Vert.x 的 Event Bus 扩展到客户端的 JavaScript 运行环境里。
这将建立一个分布式的 Event Bus。这个 Event Bus 不只能够在多个 Vert.x 实例中使用,还能够经过运行在浏览器里的 JavaScript 访问。
由此,咱们能够围绕浏览器和服务器构建一个庞大的分布式 Event Bus。只要服务器之间的连接存在,浏览器不须要每一次都与同一个服务器创建连接。
这些是经过 Vert.x 提供的一个简单的客户端 JavaScript 库 vertx-eventbus.js
来实现的。它提供了一系列和服务器端的 Vert.x Event Bus 相似的 API。经过这些 API 能够发送或发布消息,或注册处理器来接收消息。
一个 SockJS 套接字处理器会被安装到 SockJSHandler
上。这个处理器用于处理 SockJS 的数据并把它桥接到服务器端的 event bus 上。
Router router = Router.router(vertx); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); BridgeOptions options = new BridgeOptions(); sockJSHandler.bridge(options); router.route("/eventbus/*").handler(sockJSHandler);
在客户端经过使用 vertx-eventbus.js
库来和 Event Bus 创建链接,并发送/接收消息:
<script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script> <script src='vertx-eventbus.js'></script> <script> var eb = new EventBus('http://localhost:8080/eventbus'); eb.onopen = function() { // 设置了一个接收数据的处理器 eb.registerHandler('some-address', function(error, message) { console.log('received a message: ' + JSON.stringify(message)); }); // 发送消息 eb.send('some-address', {name: 'tim', age: 587}); } </script>
这个例子作的第一件事是建立了一个 Event Bus 实例:
var eb = new EventBus('http://localhost:8080/eventbus');
构造函数中的参数是链接到 Event Bus 使用的 URI。因为咱们建立的桥接器是以 eventbus
做为前缀的,所以咱们须要将 URI 指向这里。
在链接打开以前,咱们什么也作不了。当它打开后,会回调 onopen
函数处理。
注意,不管是 SockJS 或是 EventBusBridge 都不支持自动重连
当你的服务器关闭时,你须要从新建立一个 EventBus 实例:
function setupEventBus() { var eb = new EventBus(); eb.onclose = function (e) { setTimeout(setupEventBus, 1000); //等待服务器重启 }; // 在这里设置处理器 }
您能够经过依赖管理器来获取客户端库:
pom.xml
文件里)<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> <version>3.4.2</version> <classifier>client</classifier> <type>js</type> </dependency>
build.gradle
文件里)compile 'io.vertx:vertx-web:3.4.2:client'
这个库也能够经过如下方式来获取:
注意, 这个 API 在 3.0.0 和 3.1.0 版本之间发生了变化,请检查变动日志。老版本的客户端仍然兼容,但新版本提供了更多的特性,而且更接近服务端的 Vert.x Event Bus API。
若是您像上面的例子同样启动一个桥接器,并试图发送消息,您会发现您的消息神秘地失踪了。发生了什么?
对于大多数的应用,您应该不但愿客户端的 JavaScript 代码能够发送任何消息到任何的服务端处理器或其余全部浏览器上。
例如,您可能在Event Bus 上注册了一个服务,用于访问和删除数据。但咱们并不但愿恶意的客户端可以经过这个服务来操做数据库中的数据。而且,咱们也不但愿客户端可以监听全部 event bus 上的地址。
为了解决这个问题,SockJS 默认的会拒绝全部的消息。您须要告诉桥接器哪些消息是能够经过的。(例外状况是,全部的回复消息都是能够经过的)。
换句话说,桥接器的行为像是配置了 deny-all 策略的防火墙。
为桥接器配置哪些消息容许经过是很容易的。
您能够经过调用桥接器时传入的 BridgeOptions
来配置匹配规则,指定哪些输入和输出的流量是容许经过的。
每个匹配规则对应一个 PermittedOptions
对象:
这个配置精确地定义了消息能够被发送到哪些地址。若是您须要经过精确的地址来控制消息的话,使用这个选项。
这个配置经过正则表达式来定义消息能够被发送到哪些地址。若是您须要经过正则表达式来控制消息的话,使用这个选项。若是指定了 address
,这个选项会被忽略。
这个配置经过消息的接口来控制消息是否能够发送。这个配置中定义的每个字段必须在消息中存在,而且值一致。这个配置只能用于 JSON 格式的消息。
对于一个输入的消息(例如经过客户端 JavaScript 发送到服务器),当消息到达时,Vert.x Web 会检查每一条输入许可。若是存在匹配,则消息能够经过。
对于一个输出的消息(例如经过服务器端发送给客户端 JavaScript),当消息发送时,Vert.x Web 会检查每一条输出许可。若是存在匹配,则消息能够经过。
实际的匹配过程以下:
若是指定了 address
字段,而且消息的目标地址与 address
精确匹配,则匹配成功。
若是没有指定 address
而是指定了 addressRegex
字段,而且消息的目标地址匹配了这个正则表达式,则匹配成功。
若是指定了 match
字段,而且消息中包含了 match 对象中的全部键值对,则匹配成功。
如下是例子:
Router router = Router.router(vertx); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); // 容许客户端向地址 `demo.orderMgr` 发送消息 PermittedOptions inboundPermitted1 = new PermittedOptions().setAddress("demo.orderMgr"); // 容许客户端向地址 `demo.orderMgr` 发送消息 // 而且 `action` 的值为 `find`、`collecton` 的值为 `albums` 消息。 PermittedOptions inboundPermitted2 = new PermittedOptions().setAddress("demo.persistor") .setMatch(new JsonObject().put("action", "find") .put("collection", "albums")); // 容许 `wibble` 值为 `foo` 的消息. PermittedOptions inboundPermitted3 = new PermittedOptions().setMatch(new JsonObject().put("wibble", "foo")); // 下面定义了如何容许服务端向客户端发送消息 // 容许向客户端发送地址为 `ticker.mystock` 的消息 PermittedOptions outboundPermitted1 = new PermittedOptions().setAddress("ticker.mystock"); // 容许向客户端发送地址以 `news.` 开头的消息(例如 news.europe, news.usa, 等) PermittedOptions outboundPermitted2 = new PermittedOptions().setAddressRegex("news\\..+"); // 将规则添加到 BridgeOptions 里 BridgeOptions options = new BridgeOptions(). addInboundPermitted(inboundPermitted1). addInboundPermitted(inboundPermitted1). addInboundPermitted(inboundPermitted3). addOutboundPermitted(outboundPermitted1). addOutboundPermitted(outboundPermitted2); sockJSHandler.bridge(options); router.route("/eventbus/*").handler(sockJSHandler);
Event Bus 桥接器可使用 Vert.x Web 的受权功能来配置消息的访问受权。同时支持输入和输出。
这能够经过向上文所述的匹配规则中加入额外的字段来指定该匹配须要哪些权限。
经过 setRequiredAuthority
方法来指定对于一个登陆用户,须要具备哪些权限才容许访问这个消息。
这是一个例子:
PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService"); // 仅当用户已登陆而且拥有权限 `place_orders` inboundPermitted.setRequiredAuthority("place_orders"); BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted);
用户须要登陆,并被受权才可以访问消息。所以,您须要配置一个 Vert.x 认证处理器来处理登陆和受权。例如:
Router router = Router.router(vertx); // 容许客户端向地址 `demo.orderService` 发送消息 PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService"); // 仅当用户已经登陆而且包含 `place_orders` 权限 inboundPermitted.setRequiredAuthority("place_orders"); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); sockJSHandler.bridge(new BridgeOptions(). addInboundPermitted(inboundPermitted)); // 设置基础认证处理器 router.route().handler(CookieHandler.create()); router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider); router.route("/eventbus/*").handler(basicAuthHandler); router.route("/eventbus/*").handler(sockJSHandler);
若是您须要在在桥接器发生事件的时候获得通知,您须要在调用 bridge
方法时提供一个处理器。
任何发生的事件都会被传递到这个处理器。事件由对象 BridgeEvent
来描述。
事件多是如下的某一种类型:
SOCKET_CREATED
当新的 SockJS 套接字建立时会发生该事件。
SOCKET_IDLE
当 SockJS 的套接字的空闲事件超过出事设置会发生该事件。
SOCKET_PING
当 SockJS 的套接字的 ping 时间戳被更新时会发生该事件。
SOCKET_CLOSED
当 SockJS 的套接字关闭时会发生该事件。
SEND
当试图将一个客户端消息发送到服务端时会发生该事件。
PUBLISH
当试图将一个客户端消息发布到服务端时会发生该事件。
RECEIVE
当试图将一个服务器端消息发布到客户端时会发生该事件。
REGISTER
当客户端试图注册一个处理器时会发生该事件。
UNREGISTER
当客户端试图注销一个处理器时会发生该事件。
您能够经过 type
方法来得到事件的类型,经过 getRawMessage
方法来得到消息原始内容。
消息的原始内容是一个以下结构的 JSON 对象:
{ "type": "send"|"publish"|"receive"|"register"|"unregister", "address": the event bus address being sent/published/registered/unregistered "body": the body of the message }
事件对象同时是一个 Future
实例。当您完成了对消息的处理,您能够用参数 true
来完成这个 Future
以执行后续的处理。
若是您不但愿事件继续处理,您能够用参数 false
来结束这个 Future
。这个特性能够用于定制您本身的消息过滤器、细粒度的受权或指标收集。
在下面的例子里,咱们拒绝掉了全部通过桥接器而且包含 “Armadillos” 一词的消息:
Router router = Router.router(vertx); // 容许客户端向地址 `demo.orderMgr` 发送消息 PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.someService"); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted); sockJSHandler.bridge(options, be -> { if (be.type() == BridgeEventType.PUBLISH || be.type() == BridgeEventType.RECEIVE) { if (be.getRawMessage().getString("body").equals("armadillos")) { // 拒绝该消息 be.complete(false); return; } } be.complete(true); }); router.route("/eventbus").handler(sockJSHandler);
Router router = Router.router(vertx); // 容许客户端向地址 `demo.orderMgr` 发送消息 PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.someService"); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted); sockJSHandler.bridge(options, be -> { if (be.type() == BridgeEventType.PUBLISH || be.type() == BridgeEventType.RECEIVE) { if (be.getRawMessage().getString("body").equals("armadillos")) { // 拒绝该消息 be.complete(false); return; } } be.complete(true); }); router.route("/eventbus").handler(sockJSHandler);
下面的例子展现了如何配置并处理 SOCKET_IDDLE
事件。注意,setPingTimeout(5000)
的做用是当 ping 消息在 5 秒内没有从客户端返回时触发 SOCKET_IDLE 事件。
Router router = Router.router(vertx); // 初始化 SockJS 处理器 SockJSHandler sockJSHandler = SockJSHandler.create(vertx); BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted).setPingTimeout(5000); sockJSHandler.bridge(options, be -> { if (be.type() == BridgeEventType.SOCKET_IDLE) { // 执行某些处理 } be.complete(true); }); router.route("/eventbus/*").handler(sockJSHandler);
在客户端 JavaScript 环境里您使用 vertx-eventbus.js
来建立到 Event Bus 的链接并发送和接收消息:
<script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script> <script src='vertx-eventbus.js'></script> <script> var eb = new EventBus('http://localhost:8080/eventbus', {"vertxbus_ping_interval": 300000}); // sends ping every 5 minutes. eb.onopen = function() { // 设置一个接收消息的回调函数 eb.registerHandler('some-address', function(error, message) { console.log('received a message: ' + JSON.stringify(message)); }); // 发送消息 eb.send('some-address', {name: 'tim', age: 587}); } </script>
在这个例子中,第一件事是建立了一个 Event Bus 实例:
var eb = new EventBus('http://localhost:8080/eventbus', {"vertxbus_ping_interval": 300000});
构造函数的第二个参数是告诉 SockJS 的库每隔 5 分钟发送一个 ping 消息。因为服务器端配置了指望每隔 5 秒收到一条 ping 消息,所以会在服务器端触发 SOCKET_IDLE
事件。
您也能够在处理事件时修改原始的消息内容,例如修改消息体。对于从客户端发送来的消息,您也能够修改消息的消息头,下面是一个例子:
Router router = Router.router(vertx); // 容许客户端向地址 `demo.orderService` 发送消息 PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService"); SockJSHandler sockJSHandler = SockJSHandler.create(vertx); BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted); sockJSHandler.bridge(options, be -> { if (be.type() == BridgeEventType.PUBLISH || be.type() == BridgeEventType.SEND) { // 添加消息头 JsonObject headers = new JsonObject().put("header1", "val").put("header2", "val2"); JsonObject rawMessage = be.getRawMessage(); rawMessage.put("headers", headers); be.setRawMessage(rawMessage); } be.complete(true); }); router.route("/eventbus").handler(sockJSHandler);
CSRF 某些时候也被称为 XSRF。它是一种能够再未受权的网站获取用户隐私数据的技术。Vet.x-Web 提供了一个处理器 CSRFHandler
是您能够避免跨站点的伪造请求。
这个处理器会向全部的 GET 请求的响应里加一个独一无二的令牌做为 Cookie。客户端会在消息头里包含这个令牌。因为令牌基于 Cookie,所以须要在 Router
上启用 Cookie 处理器。
当开发非单页面应用,并依赖客户端来发送 POST
请求时,这个消息头没办法在 HTML 表单里指定。为了解决这个问题,这个令牌的值也会经过表单属性来检查。这只会发生在请求中不存在这个消息头,而且表单中包含同名属性时。例如:
<form action="/submit" method="POST"> <input type="hidden" name="X-XSRF-TOKEN" value="abracadabra"> </form>
您须要将表单的属性设置为正确的值。填充这个值惟一的办法是经过上下文来获取键 X-XSRF-TOKEN
的值。这个键的名称也能够在初始化 CSRFHandler
时指定。
router.route().handler(CookieHandler.create()); router.route().handler(CSRFHandler.create("abracadabra")); router.route().handler(rc -> { });
虚机主机处理器会验证请求的主机名。若是匹配成功,则转发这个请求到注册的处理器上。不然,继续在原先的处理器链中执行。
处理器经过请求的消息头 Host
来进行匹配,并支持基于通配符的模式匹配。例如 *.vertx.io
或完整的域名 www.vertx.io
。
router.route().handler(VirtualHostHandler.create("*.vertx.io", routingContext -> { // 若是请求访问虚机主机 `*.vertx.io` ,执行某些处理 }));
OAuth2AuthHandler
帮助您快速地配置基于 OAuth2 协议的安全路由。这个处理器简化了获取 authCode 的流程。下面的例子用这个处理器实现了保护资源并经过 GitHub 来受权:
OAuth2Auth authProvider = GithubAuth.create(vertx, "CLIENT_ID", "CLIENT_SECRET"); // 在服务器上建立 oauth2 处理器 // 第二个参数是您提供给您的提供商的回调 URL OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "https://myserver.com/callback"); // 配置回调处理器来接收 GitHub 的回调 oauth2.setupCallback(router.route()); // 保护 `/protected` 路径下的资源 router.route("/protected/*").handler(oauth2); // 在 `/protected` 路径下挂载某些处理器 router.route("/protected/somepage").handler(rc -> { rc.response().end("Welcome to the protected resource!"); }); // 欢迎页 router.get("/").handler(ctx -> { ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Github</a>"); });
OAuth2Auth authProvider = GithubAuth.create(vertx, "CLIENT_ID", "CLIENT_SECRET"); // 在服务器上建立 oauth2 处理器 // 第二个参数是您提供给您的提供商的回调 URL OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "https://myserver.com/callback"); // 配置回调处理器来接收 GitHub 的回调 oauth2.setupCallback(router.route()); // 保护 `/protected` 路径下的资源 router.route("/protected/*").handler(oauth2); // 在 `/protected` 路径下挂载某些处理器 router.route("/protected/somepage").handler(rc -> { rc.response().end("Welcome to the protected resource!"); }); // 欢迎页 router.get("/").handler(ctx -> { ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Github</a>"); });
OAuth2AuthHandler
会配置一个正确的 OAuth2 回调,所以您不须要处理受权服务器的响应。一个很重要的事情是,来自受权服务器的响应只有一次有效。也就是说若是客户端对回调 URL 发起了重载操做,则会由于验证错误而请求失败。
经验法则是:当有效的回调执行时,通知客户端跳转到受保护的资源上。
就 OAuth2 规范的生态来看,使用其余的 OAuth2 提供商须要做出少量的修改。为此,Vertx Auth 提供了若干开箱即用的实现:
AzureADAuth
BoxAuth
DropboxAuth
FacebookAuth
FoursquareAuth
GithubAuth
GoogleAuth
InstagramAuth
KeycloakAuth
LinkedInAuth
MailchimpAuth
SalesforceAuth
ShopifyAuth
SoundcloudAuth
StripeAuth
TwitterAuth
若是您须要使用一个上述未列出的提供商,您也可使用基本的 API 来实现,例如:
OAuth2Auth authProvider = OAuth2Auth.create(vertx, OAuth2FlowType.AUTH_CODE, new OAuth2ClientOptions() .setClientID("CLIENT_ID") .setClientSecret("CLIENT_SECRET") .setSite("https://accounts.google.com") .setTokenPath("https://www.googleapis.com/oauth2/v3/token") .setAuthorizationPath("/o/oauth2/auth")); // 在域名 `http://localhost:8080` 上建立 oauth2 处理器 OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "http://localhost:8080"); // 配置须要的权限 oauth2.addAuthority("profile"); // 配置回调处理器来接收 Google 的回调 oauth2.setupCallback(router.get("/callback")); // 保护 `/protected` 路径下的资源 router.route("/protected/*").handler(oauth2); // 在 `/protected` 路径下挂载某些处理器 router.route("/protected/somepage").handler(rc -> { rc.response().end("Welcome to the protected resource!"); }); // 欢迎页 router.get("/").handler(ctx -> { ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Google</a>"); });
您须要手工提供全部关于您所使用的提供商的细节,但结果是同样的。
这个处理器会在您的应用上绑定回调的 URL。用法很简单,只须要为这个处理器提供一个路由(Route
),其余的配置都会自动完成。一个典型的状况是您的 OAuth2 提供商会须要您来提供您的应用的 callback url,则您的输入相似于 https://myserver.com/callback
。这是您的处理器的第二个参数。至此,您完成全部必须的配置,只须要经过 setupCallback
方法来启动它便可。
以上就是如何在您的服务器上绑定处理器 https://myserver.com:8447/callback。注意,端口号能够不使用默认值。
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(provider, "https://myserver.com:8447/callback"); // 容许该处理器为您处理回调地址 oauth2.setupCallback(router.route());
在这个例子中,Route
对象经过 Router.route()
建立。若是您须要完整的控制处理器的执行顺序(例如您指望它在处理链中首先被执行),您也能够先建立这个 Route
对象,而后将引用传进这个方法里。
一些 OAuth2 的提供商参考了 RFC6750 规范,使用 JWT 令牌来做为访问令牌。这对于须要混合基于客户端的受权和基于 API 的受权颇有用。例如您的应用提供了一些受保护的 HTML 文档,同时您又但愿他能够做为 API 被消费。在这种状况下,一个 API 不可以很容易的处理 OAuth2 须要的重定向握手,但能够提供令牌(11)。
只要提供商被配置为支持 JWT,OAuth 处理器会自动处理这个问题。
这意味着您的 API 能够经过提供值为 Bearer BASE64_ACCESS_TOKEN
的消息头 Authorization
来访问受保护的资源。
accept
方法。示例代码使用了 Java 8 Lambda 的 方法引用 语法。转发
。此处有别于 HTTP 的 Redirect 或 Proxy 等概念,只是进程内的逻辑跳转。session
的 Cookie。可参考 MSDN。vertx.executeBlocking
方法来按期刷新生成器的种子,在 Event Loop 线程中同步执行生成随机数的过程。Route.failureHandler
。route
一词同时具备名词和动词的含义。为了不混淆,原文中全部使用名词的地方都统一按照专有名词 Route / route 处理。原文中的动词统一译为 路由
。原文的最后几部分关于 SockJS
和 OAuth2
的内容写做风格明显和前文不一样,并且有些地方描述的很简略(例如 OAuth 流程的细节、SockJS 的不一样 Transport 之间的差别等)。本着翻译准确的原则,本译文没有进一步展开描述。