Vert.x -- web的介绍(三)
时间:2023-02-01 16:30:00
组件介绍
Vert.x Web 一系列用于基础 Vert.x 构建 Web 构建模块的应用。
它可以想象成现代和可伸缩的 Web 瑞士军刀的应用。
Vert.x Core 为操作提供了一系列底层功能 HTTP,部分应用就够了。
Vert.x Web 基于 Vert.x Core,为了更容易发展现实,提供了一系列更丰富的功能 Web 应用。
它继承了 Vert.x 2.x 里的 Yoke 灵感来自于特点 Node.js 的框架 Express 和 Ruby 的框架 Sinatra 等等。
Vert.x Web 设计强大,非侵入性,完全可插拔。Vert.x Web 不是容器,只能用需要的部分。
您可以使用 Vert.x Web 来构建经典的服务端 Web 应用、RESTful 实时应用(服务端推送)Web 应用程序,或者你能想到的任何类型 Web 应用程序类型的选择取决于你,而不是 Vert.x Web。
Vert.x Web 非常适合写作 RESTful HTTP 微服务,但我们不强迫你这样做。
Vert.x Web 一些关键特征包括:
- 路由(基于方法(1)、路径等)
- 基于正则表达式的路径匹配
- 从路径中提取参数
- 内容协商(2)
- 处理消息体
- 限制消息体的长度
- 接收和解析 Cookie
- Multipart 表单
- Multipart 文件上传
- 子路由
- 支持本地会话和集群会话
- 支持 CORS(跨域资源共享)
- 页面处理器错误
- HTTP基本认证
- 基于重定向认证
- 授权处理器
- 基于 JWT 的授权
- 授权用户/角色/权限
- 网页图标处理器
- 支持服务端模板渲染,包括以下开箱即用模板引擎:
- Handlebars
- Jade
- MVEL
- Thymeleaf
- Apache FreeMarker
- Pebble
- 响应时间处理器
- 静态文件服务包括缓存逻辑和目录监控
- 支持超时请求
- 支持 SockJS
- 桥接 Event-bus
- CSRF 伪造跨境请求
- 虚拟主机
Vert.x Web 为了处理器,大多数特性都实现了(Handler),所以你可以随时实现自己的处理器。随着时间的推移,我们预计会有更多的处理器。
本手册将讨论上述所有特征。
使用 Vert.x Web
在使用 Vert.x Web 在此之前,您需要在描述文件中添加依赖项:
- Maven(在
pom.xml
文件中):
io.vertx vertx-web 3.4.2
- Gradle(在
build.gradle
文件中):
dependencies { compile 'io.vertx:vertx-web:3.4.2' }
回顾 Vert.x Core 的 HTTP 服务端
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 验证它是否像预期的那样工作。
Vert.x Web 的基本概念
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);
它和上面一起使用 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
。
基于 HTTP Method 的路由
默认的,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
最后执行。
基于请求媒体类型(MIME types)的路由
您可以使用 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)。
基于客户端可接受媒体类型(MIME types acceptable)的路由
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 请求
});
启用和停用 Route
您可以通过 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();
});
另一种您可以访问上下文数据的方式是使用 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
会返回列表的第一个元素。
如果用户没有提供,则返回空。
默认的 404 处理器
如果没有为请求匹配到任何路由,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 uploads = routingContext.fileUploads();
// 执行上传处理
});
每一个上传的文件通过一个 FileUpload
对象来描述,通过这个对象可以获得名称、文件名、大小等属性。
处理 Cookie
Vert.x Web 通过 Cookie 处理器 CookieHandler
来支持 cookie。
您需要保证 cookie 处理器器能够匹配到所有您需要这个功能的请求。
router.route().handler(CookieHandler.create());
操作 Cookie
您可以使用 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基础认证是适用于简单应用的简单认证手段。
在这种认证方式下, 证书会以非加密的形式在 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 授权
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
,例如:
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 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 模板引擎
您需要在您的项目中添加这些依赖来使用 MVEL 模板引擎:io.vertx:vertx-web-templ-mvel:3.4.2
。通过这个方法来创建 MVEL 模板引擎的实例:io.vertx.ext.web.templ.MVELTemplateEngine#create()
。
在使用 MVEL 模板引擎时,如果不指定模板文件的扩展名,则默认会查找扩展名为 .t