Vert.x-Web简介
Vert.x-Web是用于构建Web应用程序的一系列模块,可以用来构建经典的服务端Web应用, RESTful应用, 实时的(服务端推送)Web应用,或任何您所能想到的Web应用类型。Vert.x Web的设计是强大的,非侵入式的,并且是完全可插拔的,可以只使用您需要的部分。 Vert.x Web不是一个Web容器。
要使用Vert.x Web,需要添加"vertx-web"依赖:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.5.10</version>
</dependency>
与NetServer类似,要创建一个Web服务器,首先需要创建HTTP服务器实例,并设置请求处理器,响应http请求。
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
HttpServer server = vertx.createHttpServer();
// HttpServer requestHandler(Handler<HttpServerRequest> handler);
server.requestHandler(request -> {
HttpServerResponse response = request.response();
response.putHeader("content-type", "text/plain");
response.end("Hello World!");
}).listen(8080);
}
http服务器实例监听8080端口,并设定了一个Handler<HttpServerRequest>
请求处理器,当有http请求时候,服务器实例将以HttpServerRequest
做为事件,传递给我们的编写处理器处理。通过浏览器访问http://localhost:8080/,将获取响应“Hello World!”。
Vert.x Web基本概念
在以上案例中,所有的http请求返回的都是"Hello World!"。实际场景中,不同的http请求表示请求/操作不同的资源或者页面,我们需要区分并进行相应的响应。将所有请求都放在一个处理器(方法)处理是不合适的,所以,Vert.x Web引入了Router
(路由器)概念,使整个编程逻辑更简单。
Router由多个路由Route
(路由)的对象组成。Router根据传入http请求查找匹配的路由,并将请求传递给该路由来处理。
路由可以持有一个与之关联的处理器(Handler <RoutingContext>
)用于处理请求。一个路由可以有多个处理器,开发者可以根据需要, 在一个处理器里结束这个响应(返回内容给客户端)或者把请求传递给下一个匹配的处理器(继续处理)。
传递给处理器的对象是RoutingContext
(路由上下文),每个http请求都有一个唯一对应的RoutingContext实例(对象),RoutingContext在对应的(路由)处理器间传递。 RoutingContext包含了请求(HttpServerRequest),响应(HttpServerResponse)等有用信息。并保存http请求生命周期内您想在多个处理器之间共享的数据。
我们来看一个http请求路由的案例:
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
配置一个路由, 匹配访问/some/path路径的HTTP请求
Route route = router.route("/some/path/");
为该路由关联一个Handler(处理器)。
route.handler(routingContext -> {
// 路由上下文的HttpServerRequest对象可以获取该请求的具体信息(例如请求参数)
HttpServerRequest request = routingContext.request();
String pageValue = request.getParam("page");
String limitValue = request.getParam("limit");
// 路由上下文的HttpServerResponse对象可以用来响应该请求。
HttpServerResponse response = routingContext.response();
// 开启分块响应
response.setChunked(true);
response.putHeader("content-type", "text/plain");
response.write(request.method().name() + " " + request.absoluteURI());
response.write(" page=" + pageValue + "; limit=" + limitValue);
response.end(); // 结束响应处理, 返回给客户端
});
server.requestHandler(router).listen(8080);
}
当通过浏览器访问http://127.0.0.1:8080/some/path?page=2&limit=10,浏览器将返回以下文本信息:
GET http://127.0.0.1:8080/some/path?page=2&limit=10 page=2; limit=10
上例中,response.end()
用于结束响应处理,只有调用end方法后,响应才会返回给浏览器,如果我们注释掉该方法,那么浏览器会一直"打转",处于等待响应状态。需要注意的是,我们这里使用了分段写入(调用了response.write方法),需要调用’response.setChunked(true)'开启分段写入,否则会抛异常。
一个Route支持多个处理器,如果一个请求还需要其它处理器继续处理,需要注释掉response.end()
,并调用routingContext.next()
,这时会以RoutingContext实例做为参数,调用其它(如果有)处理器继续处理,但不要忘记,在最后需要执行end方法,结束处理,将响应返回给浏览器。
基于路径路由
现实中,最常用的是根据不同的路径来访问不同的页面/资源。Vert.x支持多种基于路径的路由匹配规则。
基于精确路径的路由
只匹配路径一致的请求。例如:
router.route("/some/path/").handler(routingContext -> {
...
});
匹配案例说明:
### 指定匹配"/some/path/"
匹配:
http://127.0.0.1:8080/some/path/
http://127.0.0.1:8080/some/path/?page=1&limit=10
不匹配:
http://127.0.0.1:8080/some/path
http://127.0.0.1:8080/some/path/subpath
### 指定匹配"/some/path"
匹配:
http://127.0.0.1:8080/some/path/
http://127.0.0.1:8080/some/path
不匹配:
http://127.0.0.1:8080/some/path/subpath
基于路径前缀的路由
与基于精确路径的路由设置方法类似,只是指定路径以*
作为结尾, 例如:
Route route = router.route("/some/path/*");
匹配案例:
### 指定匹配/some/path/*
匹配:
http://127.0.0.1:8080/some/path/
http://127.0.0.1:8080/some/path
http://127.0.0.1:8080/some/path/pag.html
http://127.0.0.1:8080/some/path/subpath
不匹配:
http://127.0.0.1:8080/some/patha
http://127.0.0.1:8080/some/patha/
http://127.0.0.1:8080/some/patha/page.html
基于正则表达式的路由
通过router.routeWithRegex(“regex”)或者router.route().pathRegex(“regex”)方法基于正则表达式匹配请求。
Route route = router.route().pathRegex(".*foo");
Route route = router.routeWithRegex(".*foo");
匹配案例:
### 指定正则表达式".*foo",匹配foo结尾的路径。
匹配:
http://127.0.0.1:8080/foo
http://127.0.0.1:8080/foo?page=1&limit=10
http://127.0.0.1:8080/some/foo
http://127.0.0.1:8080/some/xfoo
不匹配:
http://127.0.0.1:8080/foo/
http://127.0.0.1:8080/foO
http://127.0.0.1:8080/foo/page.html
获取路径参数
对于Restfull API,我们常常需要获取路径传递的参数。
通过路径占位符获取
Vert.x Web可以通过占位符声明路径参数,并通过pathParam方法获取。占位符由 :
和参数名构成,参数名由字母,数字和下划线构成。
// GET /api/v2/ob/clusters/{id}/zones
router.route("/api/v2/ob/clusters/:id/zones").handler(routingContext -> {
String clusterId = routingContext.pathParam("id");
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/plain; charset=utf-8");
response.end("显示集群编号为" + clusterId + "的Zone列表。");
});
浏览器访问http://localhost:8080//api/v2/ob/clusters/123/zones,浏览器将返回"显示集群编号为123的Zone列表。"
通过正则表达式捕获组获取
复杂的路径可以通过正则表达式匹配,并通过正则表达式捕获组来获取路径参数。例如:
router.routeWithRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(routingContext ->{
String group0 = routingContext.pathParam("param0"); //第一个捕获组值
String group1 = routingContext.pathParam("param1"); //第二个捕获组值,以此类推...
//注意区别: url参数是通过HttpServerRequest.getParam(...)获取的
// HttpServerRequest request = routingContext.request();
// String page = request.getParam("page");
});
匹配案例:
### 指定正则表达式"\\/([^\\/]+)\\/([^\\/]+)",匹配任意/xxx/xxx格式的路径。
匹配:
http://127.0.0.1:8080/emp/1 => group0 = emp, group1 = 1
http://127.0.0.1:8080/foo/page1.html => group0 = foo, group1 = page1.html
http://127.0.0.1:8080/foo/page1?page=1&limit=12 => group0 = foo, group1 = page1
不匹配:
http://127.0.0.1:8080/api/emp/1
http://127.0.0.1:8080/foo/page1/
...
使用序号参数名在某些场景下可能会比较麻烦。 亦可在正则表达式路径中使用命名的捕捉组。
router.routeWithRegex("\\/(?<productType>[^\\/]+)\\/(?<productID>[^\\/]+)").handler(ctx -> {
// 通过组名替代了之前的param0, param1, param2, ...
String productType = ctx.pathParam("productType");
String productID = ctx.pathParam("productID");
});
基于HTTP方法的路由
Route默认会匹配所有的HTTP方法,在编写Restfull API时,通常会通过HTTP 请求方法来代表对资源的不同的操作,例如:
GET - 从服务器获取资源。用于请求数据而不对数据进行更改。
POST - 向服务器发送数据以创建新资源。
PUT - 向服务器发送数据以更新现有资源。
DELETE - 从服务器删除指定的资源。
...
Route是可以基于HTTP请求方法路由请求的,例如:
// api/v2/ob/clusters
// router.route().method(HttpMethod.POST).path("/api/v2/ob/clusters").handler(routingContext -> { //等效写法
router.route(HttpMethod.POST, "/api/v2/ob/clusters").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "application/json; charset=utf-8");
JsonObject resultObj = new JsonObject().put("data", "json array of clusters");
response.end(resultObj.toString());
});
我们通过Postman, 以POST方式访问http://127.0.0.1:8080/api/v2/ob/clusters,可以获取正确结果:
{
"data": "json array of clusters"
}
如果我们以POST以外的方法访问,如GET,则会报错"405 Method Not Allowed"。
可以组合路由匹配多种方法,例如:
router.route()
.method(HttpMethod.POST)
.method(HttpMethod.GET)
.path("/api/v2/ob/clusters")
.handler(routingContext -> {
// 同时路由匹配POST, GET方法
});
基于MIME类型的路由
HTTP可以通过content-type请求头,指定请求体(body)的MIME类型;通过accept请求头,用于表示客户端可以接受响应的MIME类型。
Route可以通过consumes方法配置基于content-type请求头的路由匹配,通过produces方法配置基于accept请求头的路由匹配。
// 必须设置BodyHandler(请求体处理器),才能接收到客户端提交的请求体
router.route().handler(BodyHandler.create());
router.route()
.path("/some/path2")
.consumes("text/html")
.consumes("text/plain")
.handler(routingContext -> {
RequestBody requestBody = routingContext.body();
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/plain");
String body = Objects.isNull(requestBody) ? "null": requestBody.asString();
response.end("Request Body: " + body);
});
我们通过PUT方法访问http://127.0.0.1:8080/some/path2,并指定请求体和请求体类型,页面可以正常处理请求。
设定请求体(Body)的内容:
Postman会根据你的请求头指定Content-Type,也可以修改。
如果修改"Content-Type"为"application/json",那么会报错"415 Unsupported Media Type"。
类似的,通过produces方法基于请求头accept进行匹配:
router.route()
.path("/some/path2")
.produces("text/html")
.handler(routingContext -> {
RequestBody requestBody = routingContext.body();
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/plain");
String body = Objects.isNull(requestBody) ? "null": requestBody.asString();
response.end("Request Body: " + body);
});
我们通过浏览器或者Postman访问的http://127.0.0.1:8080/some/path2都是正常的,因为默认的accept请求头包含允许所有类型*,*
。
如果使用Postman,强制指定Accept请求的值为"application/json",那么将返回"406 Not Acceptable"。
组合多个路由条件
路由条件是可以组合的:
router.route()
.path("/some/path3")
.method(HttpMethod.GET)
.consumes("text/plain")
.produces("text/html")
.handler(routingContext -> {
//...
});
路由顺序
默认情况下Route按照其加入到Router的顺序进行匹配。路由器会逐级检查每条路由是否匹配,如果匹配的话,该Route的处理器将被调用。
前提是处理器中不结束相应(不调用HttpServerResponse的end方法),并调用RoutingContext的next方法。
router.route().setName("/*").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.setChunked(true);
response.putHeader("content-type", "text/plain");
response.write("Route: " + routingContext.currentRoute().getName() + " handle ...\n");
routingContext.next();
});
router.route().setName("/some/*").path("/some/*").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("Route: " + routingContext.currentRoute().getName() + " handle ...\n");
routingContext.next();
});
router.route().path("/some/path1").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("Route: " + routingContext.currentRoute().getName() + " handle ...\n");
routingContext.next();
});
router.routeWithRegex("/some/path.*").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("Route: " + routingContext.currentRoute().getName() + " handle ...\n");
response.end();
});
当浏览器访问http://127.0.0.1:8080/some/path1,上例的4个路由都能匹配,他们将按加入Router向后顺利匹配。遇到结束响应处理(response.end)后结束匹配。浏览器响应的结果如下:
Route: /* handle ...
Route: /some/* handle ...
Route: /some/path1 handle ...
Route: /some/path.* handle ...
如果4个路由的处理器都直接结束响应处理,那么只会匹配最先加入的一条路由。
可以通过Route的order方法自定路由顺序或者说优先级。Route在创建时被分配的顺序与它们被添加到Router的顺序相对应:第一个Route编号为0,第二个Route编号为1,以此类推。通过给Route指定order您可以覆盖默认值,order可以为负值。例如:
router.route().order(-1).setName("/*").handler(routingContext -> { /* ...*/});
router.route().order(3).setName("/some/*").path("/some/*").handler(routingContext -> {/* ...*/});
router.route().order(1).path("/some/path1").handler(routingContext -> {/* ...*/});
router.routeWithRegex("/some/path.*").order(Integer.MAX_VALUE).handler(routingContext -> {/* ...*/});
指定优先级后,将以优先级小到大的顺序匹配:
Route: /* handle ...
Route: /some/path1 handle ...
Route: /some/* handle ...
Route: /some/path.* handle ...
重新路由
可以通过RoutingContext指定匹配一个路由(重新路由)。
router.route().path("/some/path").handler(ctx -> ctx.reroute("/some/path/B"));
router.route().path("/some/path/B").handler(ctx -> ctx.response().end("/some/path/B"));
浏览器访问http://127.0.0.1:8080/some/path,将返回"/some/path/B"。在错误处理时常会用到,如访问不存在的页面时候,自动跳转到主页或者错误页面。
错误处理
路由器(匹配)错误处理
如果没有路由符合任何特定请求,则Vert.x-Web将根据匹配失败发出错误消息(HTTP响应状态码 )。
HTTP响应状态码用来表明特定HTTP请求是否成功完成。具体代码可以参考: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
当浏览器访问一个不存在的路径(例如 http://127.0.0.1:8080/some/notfound),这时候HTTP服务器会返回status code 404,同时Vert.x Web默认错误处理器会响应"Resource not found"给浏览器,可以通过Router
的errorHandler方法来自定义错误处理:
router.errorHandler(404, routingContext -> {
HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/html; charset=utf-8");
response.end("<html><body><h1>啊哦~你想找的内容离你而去了哦(404, 内容不存在)</h1></body></html>");
});
路由错误处理
当路由关联的处理器抛出未捕获的异常,Vertx.x Web的默认会返回状态码:500,页面返回“Internal Server Error”,例如:
router.route().path("/some/path4").handler(routingContext -> {
HttpServerRequest request = routingContext.request();
String pageValue = request.getParam("page");
int page = Integer.parseInt(pageValue); // 整数解析错误会抛出RuntimeException,而且处理器没有try ... catch捕获异常。
HttpServerResponse response = routingContext.response();
response.end("current page: " + page);
});
浏览器访问: http://127.0.0.1:8080/some/path4,就会返回默认错误页面,服务器端日志系统输出:
2024-10-10 08:56:01 [严重] Unhandled exception in router
java.lang.NumberFormatException: null ⇒ 因为请求参数page为空。
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at vertx.web.Web2.lambda$0(Web2.java:17)
at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1285)
...
可以通过Route的failureHandler方法设置路由错误处理器:
router.route().path("/some/path4").handler(routingContext -> {
// ...
}).failureHandler(routingContext ->{
Throwable exception = routingContext.failure();
int statusCode = routingContext.statusCode();
服务器记录日志
HttpServerRequest request = routingContext.request();
String method = request.method().name();
String uri = request.absoluteURI();
LOGGER.log(Level.SEVERE, method + " " + uri + ", statusCode: " + statusCode, exception);
返回错误信息
HttpServerResponse response = routingContext.response();
response.setStatusCode(statusCode); //必须设置, 否则200 OK
// response.setStatusMessage(exception.getMessage()); // 可覆盖, 默认是statusCode对应的错误信息。
// {"error":{"code":500,"message":"Exception Messages Here..."}}
JsonArray errorArray = new JsonArray()
.add(new JsonObject().put("code", statusCode))
.add(new JsonObject().put("message", exception.getMessage()));
JsonObject respObj = new JsonObject().put("error", errorArray);
response.end(respObj.toString());
// response.end(); // 也可不返回任何错误信息给客户端,但记得结束响应。
});
如果浏览器访问: http://127.0.0.1:8080/some/path4?page=haha,那么请求状态(status code)为500,浏览器响应信息为:
{"error":[{"code":500},{"message":"For input string: \"haha\""}]}
服务器端的日志也更详细,包含用户请求信息:
2024-10-10 09:16:20 [严重] GET http://127.0.0.1:8080/some/path4?page=haha, statusCode: 500 ==>
java.lang.NumberFormatException: For input string: "haha"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at vertx.web.Web1.lambda$4(Web1.java:90)
...
Vert.x Web也自带了路由错误处理器(ErrorHandler):
router.route().path("/some/path4").handler(routingContext -> {
//...
}).failureHandler(ErrorHandler.create(vertx));
ErrorHandler服务器端不记录异常,浏览器返回"An unexpected error occurred 500 Internal Server Error"。
错误处理器也是根据路由条件匹配的,例如:
// 匹配所有的路由条件,所有的路由处理器异常都会触发该错误处理器。
router.route().failureHandler(routingContext ->{ /* ... */});
// 路径为/some/*的路由的错误处理器。
router.route().path("/some/*").failureHandler(routingContext ->{ /* ... */});