Vert.x(vertx) Web开发-路由

在Vert.x 创建HTTP服务 中我们已经创建了一个简单的HttpServer,但这个HttpServer比较低级,对于请求参数解析、Session等常用功能都需要我们通过编码实现,也就是要重复造轮子,非常不方便。

Vert.x提供了Web开发组件vertx-web,提供了一堆Web开发中常用的功能。比如参数封装,路由,国际化,认证和授权,session和cookie以及模板等,可以非常方便的进行Vert.x Web开发。

本篇主要介绍Web开发中的路由功能,路由简单说就是把用户请求交给合适的处理器处理的组件。如下图所示

Vert.x(vertx) Web开发-路由_第1张图片

 

路由是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");
});

当我们在浏览器直接请求时,你会看到如下结果

Vert.x(vertx) Web开发-路由_第2张图片

当你使用POST发送时,结果如下;

Vert.x(vertx) Web开发-路由_第3张图片    Vert.x(vertx) Web开发-路由_第4张图片

除了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(vertx) Web开发-路由_第5张图片

二、实现原理

如果你熟读源码,你会发现,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 类型,看router类的定义,果然如我们所猜想,如下

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方法关联起来。

Vert.x(vertx) Web开发-路由_第6张图片

而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 Iterator iter;

是routes的迭代器

this.iter = routes.iterator();

那么while(iter.hasNext()) 就是要迭代所有的route,然后进行匹配,进而为匹配到的route,执行route的handle方法。如果一切条件成立,执行

Vert.x(vertx) Web开发-路由_第7张图片

进入到handleContext方法,代码如下

void handleContext(RoutingContextImplBase context) {
  Handler contextHandler;

  synchronized (this) {
    contextHandler = contextHandlers.get(context.currentRouteNextHandlerIndex() - 1);
  }

  contextHandler.handle(context);
}

先获取到当前的处理器,调用处理器的handle方法,也就是第一部分中我们所写的回调方法,类似于这种

Vert.x(vertx) Web开发-路由_第8张图片

这里所写的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的认知。
 

你可能感兴趣的:(Java)