在Vert.x 创建HTTP服务 中我们已经创建了一个简单的HttpServer,但这个HttpServer比较低级,对于请求参数解析、Session等常用功能都需要我们通过编码实现,也就是要重复造轮子,非常不方便。
Vert.x提供了Web开发组件vertx-web,提供了一堆Web开发中常用的功能。比如参数封装,路由,国际化,认证和授权,session和cookie以及模板等,可以非常方便的进行Vert.x Web开发。
本篇主要介绍Web开发中的路由功能,路由简单说就是把用户请求交给合适的处理器处理的组件。如下图所示
路由是Web开发中最基础也是最常用的功能,Vert.x提供了强大的路由功能,包括正则匹配,二级路由等。本文从两个方面来讲述路由,分别是路由的使用和路由的实现原理(源码阅读)
1. 基本路由实现
1.在pom文件中,加入vertx-web的依赖包
io.vertx
vertx-web
3.5.2
2.创建一个HttpServer
/**
* 简单的路由使用
*
* @author lenovo
*
*/
public class SimpleRouter extends AbstractVerticle {
@Override
public void start() throws Exception {
// 创建HttpServer
HttpServer server = vertx.createHttpServer();
// 创建路由对象
Router router = Router.router(vertx);
// 监听/index地址
router.route("/index").handler(request -> {
request.response().end("INDEX SUCCESS");
});
// 把请求交给路由处理--------------------(1)
server.requestHandler(router::accept);
server.listen(8888);
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new SimpleRouter());
}
}
上面这段代码还是比较好理解的,主要就是在Vert.x 创建HTTP服务的基础上增加了Router,并且最后把请求的处理交给Router来处理。这样当我们访问服务器时,就会根据匹配规则,找到对应的处理器来进行处理。
这里有一个地方,就是在代码中标(1)的部分,router::accept这个可能很多朋友不理解,这个也是JDK8的一个新特性,实际上就是一个语法糖,下面是一段不使用JDK8的代码,想必大家看了下面这段代码就都名白了。router是我们创建的router对象,然后把回调的值传给router对象的accept方法。
server.requestHandler(new Handler() {
@Override
public void handle(HttpServerRequest event) {
router.accept(event);
}
});
通过比较,我们会发现,使用JDK8 的新特性,代码上还是相对会简洁很多的。
学习都有一个2.8原则,就是说学习80%的知识可能只用20%的时间,路由也是一样,通过上面这些你实际上已经可以去进行路由了,所以也算是路由的80%的东西了,剩下的20%你可能需要花费更多的时间。
2. 限制HTTP请求方法
HTTP协议中定义的请求方法有GET POST PUT DELETE等,我们之前通过在浏览器地址栏输入的都是get请求,如果我们要限制,只能使用POST请求该如何处理呢?也非常简单,可以直接通过router对象提供了post方法来进行路径的匹配。
router.post("/post").handler(request -> {
request.response().end("post");
});
当我们在浏览器直接请求时,你会看到如下结果
当你使用POST发送时,结果如下;
除了post方法以外,还可以使用put,get等方法。除了直接使用这些方法以外,还有另外一种形式,也可以指定请求的方法,route方法有个重载,第一个参数是HttpMethdo,第二个参数是匹配的URL路径,如下:
router.route(HttpMethod.GET, "/method").handler(request -> {
request.response().end("method");
});
HttpMethod是一个枚举类,可用的枚举值如下
@VertxGen
public enum HttpMethod {
OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PATCH, OTHER
}
3. 二级路由
我们经常会遇到一些情形,比如要对产品做增删改查,我们可能会有如下路由规则的定义
Router restAPI = Router.router(vertx);
restAPI.get("/products/:productID").handler(rc -> {
// TODO Handle the lookup of the product....
rc.response().write(productJSON);
});
restAPI.put("/products/:productID").handler(rc -> {
// TODO Add a new product...
rc.response().end();
});
restAPI.delete("/products/:productID").handler(rc -> {
// TODO delete the product...
rc.response().end();
});
这样虽然能够满足我么的要求,但是,如果比较多的话明显的看起来比较乱,Vertx给我们提供了二级路由来解决这个问题。我们可以直接把上面的restAPI这个作为子路由,挂载到一个主路由中。也就是说,我们创建一个主路由,然后把上面的restAPI这个路由规则通过mount方法挂载就可以了。
mainRouter.mountSubRouter("/productsAPI", restAPI);
4. 指定路由顺序(Order)
我们可能会配置很多的路由规则,而Vert.x进行路由匹配的规则非常简单,默认就是当匹配成功之后就不再继续匹配了。比如我们有如下代码
router.route("/index/*").handler(request -> {
request.response().end("Index");
});
router.route("/index/main").handler(request -> {
request.response().end("IndexMain");
});
当我们访问:http://localhost:8080/index/main 的时候,我们希望匹配显然是下面的,我们希望返回的是 IndexMain,而实际情况是返回是Index。为了解决这个问题,我们有两种办法,第一个是把下面的代码放到上面,但这样可能也并不符合我们的习惯,还有另外一种方式就是制定order。
router.route("/index/*").order(2).handler(request -> {
request.response().end("Index");
});
router.route("/index/main").order(-1).handler(request -> {
request.response().end("IndexMain");
});
还有一种情况,如果想要所有匹配的规则都执行,可以通过requestContext 的next方法
router.route("/index/*").order(2).handler(request -> {
// request.response().end("Index");
System.out.println(1);
request.next(); // 调下一个匹配规则
});
router.route("/index/main").order(-1).handler(request -> {
// request.response().end("IndexMain");
System.out.println("2");
request.next(); // 调下一个匹配规则
});
5.参数获取
用户经常会上传一些参数,那么我们在Vertx中如何接收用户上传的参数呢?
第一种就是get请求直接拼接在URL后的参数,比如:http://localhost:8080/method?param=hello
router.route(HttpMethod.GET, "/method").handler(request -> {
String param = request.request().getParam("param");
System.out.println("接收到用户传递的参数为:" + param);
request.response().end("method");
});
第二种是获取路径的参数,比如:http://localhost:8080/method/xiaoming/xm123
// 获取参数
router.route(HttpMethod.GET, "/method/:user/:pass").handler(request -> {
String user = request.request().getParam("user");
String pass = request.request().getParam("pass");
request.response()
.putHeader("Content-type", "text/html;charset=utf-8")
.end("接收到的用户名为:" + user + " 接收到的密码为:" + pass);
});
第三种是获取到请求体中的数据,也就是post提交的数据。这个稍微有一些繁琐,首先要指定一个BodyHandle,然后才能通过requestContext对象来获取body体中的数据。
router.route().handler(BodyHandler.create()); // 在路由的最前面,指定body的处理器
获取body体的数据的方法有很多,可以获取到字符串,可以直接转成Json对象等等,下面是直接读取到字符串
router.post("/post").handler(request -> {
String res = request.getBodyAsString(); // 获取到body体的数据
System.out.println(res);
request.response().end("post");
});
6. 请求处理分发到具体的类
上面的代码,我们把接收用户请求和处理用户请求的逻辑放到了一个类里,这显然是不好的,会导致代码的结构不清晰,多个人在一起维护的时候变得困难。
我们可以把具体的请求处理逻辑和监听分开,使用单独的类来做业务的处理,这非常简单。我们来重写最开始处理index监听的代码、
1.创建一个类,实现Handler接口
public class IndexHandle implements Handler {
@Override
public void handle(RoutingContext event) {
event.response().end("IndexController");
}
}
2.修改监听,在监听到index请求时,将处理器指定为IndexHandle
// 监听/index地址
router.route("/index").handler(new IndexHandle());
这里的Handle是不是看起来就像我们之前所写的Controller,当然,这就是Controller。
7.特定的处理器
为了处理index地址的请求,我们创建了一个IndexHandler。对于不同的业务,可以创建不同的处理器来处理,但往往还有一些,比如静态资源处理,Session处理器,请求体处理器。像这些处理器几乎在所有的Web开发场景中都会使用到,而且这些处理器和业务无关,所以每次写就会重复造轮子,官方已经给我们提供了非常多的好用的处理器。如下图所示
如果你熟读源码,你会发现,Vert.x体系结构设计的非常美妙,有着非常好的扩展性。Vert.x的Web模块就可以看做是对核心模块的扩展,下面就看看Web模块中是如何实现路由功能的。
上面已经用过路由了,代码非常简单,为了便于大家的阅读,再次贴出来核心代码,如下
// 创建HttpServer
HttpServer server = vertx.createHttpServer();
// 创建路由对象
Router router = Router.router(vertx);
// 监听/index地址
router.route("/index").handler(request -> {
request.response().write(Buffer.buffer("INDEX SUCCESS")).end();
});
// 把请求交给路由处理
server.requestHandler(router);
这里先跟大家解释下,Vertx路由的使用,也就是本文上半部分的内容,是在18年7月份写的,而源码部分是在2019年4月份写的,在这个阶段里,Vert.x经历了几个版本的更新,API也有些许改变。其中,router就有了改版,当然新的版本也是兼容旧的版本的。之前把请求交给路由处理是这么写的
在新版本中已经提示过期了,新版本中的写法就是上面代码中的写法,直接把router传进入就可以了,不再需要使用方法引用的形式。
我们源码的阅读就从这里开始!
requestHandler接收的参数类型是 Handler
HttpServer requestHandler(Handler handler);
requestHandler可以直接接收router作为实参,说明router就一定是Handler
public interface Router extends Handler {}
既然继承了Handler接口,就一定需要实现Handler接口中定义的方法,而Handler接口中只定义了一个方法,就是
void handle(E event);
当请求进来的时候,会找到requestHandler,进而调用Handler接口的handle方法,下面就要看下Router实现类的handle方法的实现了
@Override
public void handle(HttpServerRequest request) {
if (log.isTraceEnabled()) log.trace("Router: " + System.identityHashCode(this) +
" accepting request " + request.method() + " " + request.absoluteURI());
new RoutingContextImpl(null, this, request, routes).next();
}
这里核心的代码就是创建了一个RoutingContext的实例,并调用其next方法,直接进入到next方法
@Override
public void next() {
if (!iterateNext()) {
checkHandleNoMatch();
}
}
凭我们使用路由的经验,我们可以指定多个路由,且每个路由都可以指定order,且在单个route中可以调用next方法来执行下一个匹配的路由,因此这里相当于一个路由链,通过next方法关联起来。
而iterateNext就是在执行匹配到的路由链,直到执行完毕最后一个,进入到checkHandlerNoMatch方法。
其实路由的核心就在于这个iterateNext方法的实现,代码比较多,分开来看,先看一部分,如下
while (iter.hasNext()) { // Search for more handlers RouteImpl route = iter.next(); currentRouteNextHandlerIndex.set(0); currentRouteNextFailureHandlerIndex.set(0); try { if (route.matches(this, mountPoint(), failed)) { if (log.isTraceEnabled()) log.trace("Route matches: " + route); try { currentRoute = route; if (log.isTraceEnabled()) log.trace("Calling the " + (failed ? "failure" : "") + " handler"); if (failed && currentRoute.hasNextFailureHandler(this)) { currentRouteNextFailureHandlerIndex.incrementAndGet(); route.handleFailure(this); } else if (currentRoute.hasNextContextHandler(this)) { currentRouteNextHandlerIndex.incrementAndGet(); route.handleContext(this); } else { continue; } } catch (Throwable t) { if (log.isTraceEnabled()) log.trace("Throwable thrown from handler", t); if (!failed) { if (log.isTraceEnabled()) log.trace("Failing the routing"); fail(t); } else { // Failure in handling failure! if (log.isTraceEnabled()) log.trace("Failure in handling failure"); unhandledFailure(-1, t, route.router()); } } return true; } } catch (Throwable e) { if (log.isTraceEnabled()) log.trace("IllegalArgumentException thrown during iteration", e); // Failure in matches algorithm (If the exception is instanceof IllegalArgumentException probably is a QueryStringDecoder error!) if (!this.response().ended()) unhandledFailure((e instanceof IllegalArgumentException) ? 400 : -1, e, route.router()); return true; } }
iter是成员变量
protected Iteratoriter;
是routes的迭代器
this.iter = routes.iterator();
那么while(iter.hasNext()) 就是要迭代所有的route,然后进行匹配,进而为匹配到的route,执行route的handle方法。如果一切条件成立,执行
进入到handleContext方法,代码如下
void handleContext(RoutingContextImplBase context) { HandlercontextHandler; synchronized (this) { contextHandler = contextHandlers.get(context.currentRouteNextHandlerIndex() - 1); } contextHandler.handle(context); }
先获取到当前的处理器,调用处理器的handle方法,也就是第一部分中我们所写的回调方法,类似于这种
这里所写的request,就是上面传入的context,也就是RoutingContext对象。
到这里,我们已经完成了一个从请求过来,交给路由处理,并执行处理器方法这么一整个流程。你对路由的理解是不是更深刻了呢?
Vert.x相关系列文章
(一)Vert.x 简明介绍 https://blog.csdn.net/king_kgh/article/details/80772657
(二)Vert.x创建简单的HTTP服务 https://blog.csdn.net/king_kgh/article/details/80804078
(三)Vert.x Web开发之路由 https://blog.csdn.net/king_kgh/article/details/80848571
(四)Vert.x TCP服务实现 https://blog.csdn.net/king_kgh/article/details/84870775
(五)Vert.x数据库访问 https://blog.csdn.net/king_kgh/article/details/84894599
(六)Vert.x认证和授权 https://blog.csdn.net/king_kgh/article/details/85218454
(七)Vert.x事件总线(Event Bus)与远程服务调用 https://blog.csdn.net/king_kgh/article/details/86993812
Vert.x 案例代码:https://github.com/happy-fly
笔者就职于一家互联网支付公司,公司的核心项目使用的是Vert.x技术体系。记得笔者刚进入公司,接触Vert.x的时候,找遍了大大小小的网站,发现市面上关于Vert.x的文档除了官方文档外,几乎找不到其他资料。当时就励志要出一个专栏来写写Vert.x,以此帮助在Vert.x上采坑的朋友。因为笔者能力有限,文章中难免会有错误和疏漏,如果您对文章有意见或者好的建议,可以直接留言或者发送邮件到[email protected]。如果您也对Vert.感兴趣,可以加入到我们,共同学习Vert.x,并推进国内开发者对Vert.x的认知。