Vert.x,Web - Router

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 ->{ /* ... */});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值