vertx-web 4.*入门
绪论
我是个spring程序员入门的java后端开发,我经常用springmvc作为我的框架开发restful接口,所以本文我会用springmvc中的一些类似的东西解释他们在vertx-web里面的作用。
首先vertx-web开发不像是springmvc那样通过给方法加注解来做url对处理方法的映射,而是通过为路由挂载方法来实现,每一部分称为一个handler,而且利用的是类似于责任链的把各个handler连在一起,类似于servlet中的filter那种。
我在这里摘抄一下vertx-web都能做什么
- 路由(基于方法,路径等)
- 基于正则表达式的路径匹配
- 从路径中提取参数
- 内容协商
- 处理消息体
- 消息体的长度限制
- Multipart 表单
- Multipart 文件上传
- 子路由
- 支持本地会话和集群会话
- 支持 CORS(跨域资源共享)
- 错误页面处理器
- HTTP基本/摘要认证
- 基于重定向的认证
- 授权处理器
- 基于 JWT 的授权
- 用户/角色/权限授权
- 网页图标处理器
- 支持服务端模板渲染,包括以下开箱即用的模板引擎:
- Handlebars
- Jade
- MVEL
- Thymeleaf
- Apache FreeMarker
- Pebble
- Rocker
- 响应时间处理器
- 静态文件服务,包括缓存逻辑以及目录监听
- 支持请求超时
- 支持 SockJS
- 桥接 Event-bus
- CSRF 跨域请求伪造
- 虚拟主机
在我们开始之前请把这个依赖加入你的配置文件里面,这里用maven做例子
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.0.2</version>
</dependency>
路由
首先是路由器合和路由,即Router和Route
回想一下我们启动一个httpserver的时候,是不是传入了一个类似于请求处理器一样的东西用于处理所有的请求。
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
//处理
});
server.listen(8080);
而Router在这里就是可以作为这个参数传入,其包含了多个Route,Router主要作用就是用于初始化请求上下文+分发请求到Route,类似于springmvc中的dispatchServlet——将请求路径对应的handler找出来处理请求。
改写一下上面的代码
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route().handler(ctx -> {
//处理
});
server.requestHandler(router).listen(8080);
责任链模式的Route
一个Route不止可以挂载一个handler,可以挂载多个,那么如何调用下一个呢?
我们先来看看handler方法的签名
Route handler(Handler<RoutingContext> requestHandler);
这个RoutingContext很重要,它保存了此次请求的上下文,下面还会多次提及这个点。
Route route = router.route("/some/path/");
route.handler(ctx -> {
HttpServerResponse response = ctx.response();
// 开启分块响应,因为我们将在执行其他处理器时添加数据
// 仅当有多个处理器输出时
response.setChunked(true);
response.write("route1\n");
// 延迟5秒后调用下一匹配route
ctx.vertx().setTimer(5000, tid -> ctx.next());
});
route.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route2\n");
ctx.response().end();
});
在上述的例子中, route1
向响应里写入了数据, 5秒之后 route2
向响应里写入了数据,然后结束响应,
route的那些东西
带HTTP方法的简单写法
如何写一个get方法的route
Router router = Router.router(vertx);
router.route("/get")
.method(HttpMethod.GET)
.handler(rc -> {});
如果这个方法是只对应一个方法,那么这么写有点麻烦,在springmvc里面我们可以拿@GetMapping代替@RequestMapping(method=RequestMethod.GET),类似的,我们可以用简便的使用,其本质和上面的一样,只不过封装成了一个函数
route.get("/get").handler(rc->{})
如果想要一个route匹配不止一个HTTP方法,可以多次调用method方法
如
Route route = router.route().method(HttpMethod.POST).method(HttpMethod.PUT);
route.handler(ctx -> {
// 所有 GET 或 POST 请求都会调用这个处理器
});
甚至支持自定义HTTP动词
Route route = router.route()
.method(HttpMethod.valueOf("SLEEP"))
.handler(ctx -> {
// 所有 SLEEP请求都会调用这个处理器
});
指定请求MIME类型的route
在springmvc里面我们可以通过@RequestMapping中的consumer属性来通过请求的content-type
来指定一个handler可以处理那些类型
在vertx-wbe里则是这样用的
//精准匹配
router.route()
.consumes("text/html")
.handler(ctx -> {
});
//多匹配
router.route()
.consumes("text/html")
.consumes("text/plain")
.handler(ctx -> {
});
//子类型匹配,其支持text/html或者text/plain都映射到这个上面
router.route()
.consumes("text/*")
.handler(ctx -> {
});
//顶层类型匹配,支持text/json或者application/json
router.route()
.consumes("*/json")
.handler(ctx -> {
});
指定响应MIME类型的route
在springmvc里面我们可以通过@RequestMapping中的produce属性来通过请求的content-type
来指定一个handler可以产生那些类型
在vertx-wbe里则是这样用的,同样可以使用子类型匹配和顶层类型匹配
router.route()
.produces("application/json")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.putHeader("content-type", "application/json");
response.end(someJSON);
});
当然如果请求头中Accept: application/json;q=0.7, text/html;q=0.8, text/plain
可以支持多个,则我们可以像处理conusmer
那样设定多个produces
我们可能需要获取当前真正的接受的类型,这种情况我们可以用getAcceptableContentType方法
router.route()
.produces("application/json")
.produces("text/html")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
// Get the actual MIME type acceptable
String acceptableContentType = ctx.getAcceptableContentType();
response.putHeader("content-type", acceptableContentType);
response.end(whatever);
});
路径书写
精确路由
很容易理解,就是所见即所得,保证路径一致的匹配
Route route = router.route().path("/some/path/");
route.handler(ctx -> {
// 这个处理器会被以下路径的请求调用:
// `/some/path/`
// `/some/path////////////`
//
// 但不包括:
// `/some/path` 路径末尾的斜线会被严格限制
// `/some/path/subdir`
});
// 路径结尾没有斜线的不会被严格限制
// 这意味着结尾的斜线是可选的
// 无论怎样都会匹配
Route route2 = router.route().path("/some/path");
route2.handler(ctx -> {
// 这个处理器会被以下路径的请求调用:
// `/some/path`
// `/some/path/`
// `/some/path//`
//
// 但不包括:
// `/some/path/subdir`
});
即原路径后面加/或者不加都能匹配
前缀匹配
当一个URI为/some/path/*是会匹配以这个开头的所有URI
比如/some/path/url 和/some/path/other/other,甚至可以匹配/some/path/////
但是不会匹配/some/path
Route route = router.route().path("/some/path/*");
正则匹配
路径包含正则式,其设置的方法有些特别
Route route = router.route().pathRegex(".*foo");
Route route = router.routeWithRegex(".*foo");
包含参数的路径
在springmvc中我们可以利用@RequestMapping参数中指定路径参数比如
/book/{bookId},这里面的bookId就是一个路径参数
在vertx-web里面也包含这个功能但是写起来有些不一样
Router router = Router.router(vertx);
router.route("/get/:bookType/:bookId")
.handler(rc -> {
String bookType = rc.pathParam("bookType");
String bookId = rc.pathParam("bookId");
rc.response().end(bookId+" "+bookType);
});
其中占位符由:
和参数名构成
参数名可以支持字母,数字,下划线
正则参数的捕获
先看例子
这个正则的意思式允许/foo/bar,不允许/foo 或者/foo/bar/fx这种的匹配
Router router = Router.router(vertx);
router.routeWithRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(ctx -> {
String productType = ctx.pathParam("param0");
String productID = ctx.pathParam("param1");
System.out.println(productID);
System.out.println(productType);
});
你可能会问这里面的()是什么,这个就是捕获的变量,从左到右,从0开始以param*的形式命名
当然也允许你自定义命名,即在()内部,正则式子之前以?<name>
命名
router
.routeWithRegex("\\/(?<productType>[^\\/]+)\\/(?<productID>[^\\/]+)")
.handler(ctx -> {
String productType = ctx.pathParam("productType");
String productID = ctx.pathParam("productID");
// 执行某些操作……
});
当然此时也允许以param0这种形式取得参数
route顺序
还记得上面那个多个route的例子吗?我用servlet filter举的例子,实际上其顺序决定情况和springmvc中的servlet filter order属性差不多。
默认情况下根据你挂载的顺序决定顺序,那么我怎么才可以自定义顺序呢?
很简单使用route.order(int)方法决定
Router router = Router.router(vertx);
router.route()
.order(1)
.handler(rc -> {
System.out.println(1);
});
router.route()
.order(-1)
.handler(rc -> {
System.out.println(0);
//注意记得手动调用下一个
rc.next();
});
这样实际上order为-1的route对应的会先执行
启用或者关闭route
route实例包含 两个方法enbale()
和disable
用于控制是否被匹配到
handler的那些东西
handler也是类似于pipline模式的,我们可以为route挂载多个handler,其执行顺序按照声明顺序执行,而且需要手动调用routingcontext实例的next
类似于如下伪代码
public void hander(RoutingContext context){
boolean isPass =....
if(isPass){
context.next();
}else{
//do something
}
}
简易阻塞式handler适配
vertx核心定理:不要阻塞eventloop,而route中的handler是直接在eventloop上执行的,当我们需要执行阻塞操作时,就需要放到worker线程执行,这样代码书写起来就会存在括号的嵌套,不太直观。
这样我们就可以这么做:
router.route()
.blockingHandler(rc -> {
});
他会自动把handler内部的方法放到worker线程池中执行
其本质上就是封装了一次和直接使用
vertx.executeBlocking()
操作是一样的。
请注意,就和vertx.executeBlocking()一样,默认情况下同一个verticle就是同一个eventloop下执行是顺序的,如果你想并行多个阻塞处理器需要调用其重载方法
Route blockingHandler(Handler<RoutingContext> requestHandler, boolean ordered)
设置ordered为false
处理请求体
在post,put等请求处理的时候我们往往需要获取到方法体做各种参数处理
在vertx里面获取请求体的基础方法为
routingContext.getBody();
其返回值为Buffer实例,即方法体的二进制形式
由于我们在restful风格里面用json进行信息交换比较多,所以封装了以下几个方法getBodyAsStirng()
,getBodyAsJson
等一系列转换API,其本质仍是先获取Body的Buffer再将其反序列化为对象
但是在默认情况下vertx-web并不处理body,也就是说上面那个基础方法的返回值为空
为了处理body,我们需要引入一个bodyhandler
router.route(HttpMethod.POST,"/empty")
.handler(rc -> {
//为空
System.out.println(rc.getBody() == null);
});
router.route(HttpMethod.POST,"/full")
.handler(BodyHandler.create())
.handler(rc -> {
//不为空
System.out.println(rc.getBody() == null);
});
其中不只是对诸如json这样的请求体支持,还有文件上传
router.route(HttpMethod.POST,"/file").handler(BodyHandler.create())
.handler(rc -> {
//把储存地址返回
String filename = rc.fileUploads()
.stream()
.map(FileUpload::uploadedFileName)
.collect(Collectors.joining("$"));
rc.response().end(filename);
});
其默认在运行目录下的创建一个up-uploads文件夹,其内部文件名为UUID生成的
BodyHandler文件上传方法细节*(源码分析)
首先create返回的是个BodyHandlerImpl实现类,我们直接去看他实现的Handler<RoutingContext>
的handler方法
@Override
public void handle(RoutingContext context) {
HttpServerRequest request = context.request();
//过滤不需要处理请求体的方法
if (request.headers().contains(HttpHeaders.UPGRADE, HttpHeaders.WEBSOCKET, true)) {
context.next();
return;
}
// we need to keep state since we can be called again on reroute
if (!((RoutingContextInternal) context).seenHandler(RoutingContextInternal.BODY_HANDLER)) {
long contentLength = ....
//关键在这里
BHandler handler = new BHandler(context, contentLength);
request.handler(handler);
request.endHandler(v -> handler.end());
((RoutingContextInternal) context).visitHandler(RoutingContextInternal.BODY_HANDLER);
} else {
// on reroute we need to re-merge the form params if that was desired
....
}
}
查看BHandler的细节,其为BodyHandlerImpl的一个内部类,既然是处理文件上传请求,就应该给request挂载了uploadHandler()方法,查询发现在构造器里面如果当前是文件上传就会挂载整个文件上传回调。
//判断是否为上传请求
if (isMultipart || isUrlEncoded) {
context.request().setExpectMultipart(true);
//这个字段控制是否处理上传,这个字段来源于BodyHandlerImpl
if (handlFileUploads) {
//这个方法控制生成的文件名
//由uploadsDir决定,也是来源于BodyHandlerImpl
makeUploadDir(context.vertx().fileSystem());
}
context.request().uploadHandler(upload -> {
if (bodyLimit != -1 && upload.isSizeAvailable()) {
// we can try to abort even before the upload starts
long size = uploadSize + upload.size();
if (size > bodyLimit) {
failed = true;
cancelAndCleanupFileUploads();
context.fail(413);
return;
}
}
if (handleFileUploads) {
// we actually upload to a file with a generated filename
//这就是文件名生成的逻辑以及如何写到目标上的
uploadCount.incrementAndGet();
String uploadedFileName = new File(uploadsDir, UUID.randomUUID().toString()).getPath();
FileUploadImpl fileUpload = new FileUploadImpl(uploadedFileName, upload);
fileUploads.add(fileUpload);
Future<Void> fut = upload.streamToFileSystem(uploadedFileName);
fut.onComplete(ar -> {
if (fut.succeeded()) {
uploadEnded();
} else {
cancelAndCleanupFileUploads();
context.fail(ar.cause());
}
});
}
});
}
BodyHandler文件上传方法总结
我们只能控制上传的大小 -> setBodyLimit,生产的文件夹名->setUploadDirectory
却不能控制文件名生成
通过对源码的理解,其实就是对request挂载upload处理函数
所以我们可以仿照原有的写一个可以允许我们使用上传文件自己名字的处理器
这样仿照一下就可以做到自由上传了
router.route(HttpMethod.POST,"/file")
.handler(context -> {
String lowerCaseContentType = context.request().getHeader(HttpHeaders.CONTENT_TYPE).toLowerCase(Locale.ROOT);
boolean isMultipart = lowerCaseContentType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString());
boolean isUrlEncoded = lowerCaseContentType.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString());
if (isMultipart|| isUrlEncoded) {
//设置分块,很重要
context.request().setExpectMultipart(true);
context.request().uploadHandler(fileUpload -> {
//第一个参数就是文件夹的名字
String path = new File("file-uploads", fileUpload.filename()).getPath();
fileUpload.streamToFileSystem(path);
});
}
});
路由错误处理handler
当你在正常的handler中抛出异常或者手动调用rc.fail时,即
router.route("/error")
.handler(rc-> {
// throw new RuntimeException(); 或者
rc.fail(new RuntimeException());
})
当然不希望手动处理,而是希望发挥pipline模式的优势,让各个handler各司其职,设置一个专门的错误处理器。
在vertx里面有两种方法
route层面
先讲针对于route的
router.route("/error")
.handler(rc-> {
// throw new RuntimeException();
rc.fail(new RuntimeException());
})
.failureHandler(rc -> {
Throwable throwable = rc.failure();
throwable.printStackTrace();
rc.end("route err");
});
这种就很简单,是专门处理单个route内的异常的
这样写可能有些麻烦,因为我们可能要对多个handler处理异常,而处理方法都一样。我们希望能够像springmvc那种一样有一个全局的异常处理
那么我们就不能针对route,而是为router挂载异常处理器
Router层面
先看个例子:
router.errorHandler(500,rc->{
rc.response().end("router");
});
其方法签名分为两部分,第一部分是对的处理的相应的status码,即500,404这种,默认情况下如果是直接抛出的异常是500,如果用rc.fail(code)设置了码则根据码选择处理器
优先级问题
根据注释,这个handler适用于
指定一个处理程序以处理特定状态码的错误。 您也可以使用状态码500来管理一般错误。当上下文失败且其他失败处理程序未编写回复或处理程序内部引发异常时,将调用处理程序。 您不得在错误处理程序内使用RoutingContext.next()这不会影响正常的故障路由逻辑。
这种情况
即未被处理的异常
所以说
router.route("/error")
.handler(rc-> {
// throw new RuntimeException();
rc.fail(new RuntimeException());
})
.failureHandler(rc -> {
Throwable throwable = rc.failure();
throwable.printStackTrace();
rc.end("route err");
});
router.errorHandler(500,rc->{
rc.response().end("router");
});
这种情况是由route处理异常
处理404等异常
在vertx-web里面404会有个默认处理返回值,我们可以替换掉它,即通过给router挂载错误处理
即
router.errorHandler(404, rc -> {
rc.end("404");
});
除此之外vertx-web还有一些默认的当匹配发生异常的错误可以以这种方式处理考虑处理
- 405 路径匹配但是请求方法不能匹配
- 406 方法和路径匹配,但是响应不能匹配请求的
accepet
- 415 方法和路径匹配,但是不能处理
content-type
- 400 方法和路径匹配,但是不能接收空请求体
处理session的handler
cookie-session作为经典的记录请求状态的模型,vertx-web也为其做了开箱即用的实现,多种储存形式则被抽象出一个公共的接口SessionStore
本地seesion储存
就像Tomcat等传统的servlet容器做的一样,将session储存在应用本地的内存中,而且与Vertx
实例绑定。
Vertx实例绑定的的意义:
当你的负载均衡可以确保用户请求可以固定访问同一个vertx的时候,就不需要共享了,但是一般来说一个应用常常以单Vertx实例启动。
其实现细节有两点值得注意(源码详见io.vertx.ext.web.sstore.impl.LocalSessionStoreImpl
)
-
其本质实现是shared local map 当你使用json配置的时候对应的键为
mapName
,其在初始化中的实际意义为localmap的值,对应源码为:localMap = vertx.sharedData().getLocalMap(options.getString("mapName", DEFAULT_SESSION_MAP_NAME));
即如果你想可以通过绑定的vertx直接拿到
另外一提其泛型形式为
LocalMap<String, Session> localMap
,即cookie-session。其默认值为vertx-web.sessions -
其含有一个过期时间 当你使用json配置的时候对应的键为
reaperInterval
,其内部实现就是一个vertx的timer,即eventloop的定时任务,所以不是特别特别精确,但是谁又会在乎多或者少一两秒呢?其默认值为1000ms
下面是一个创建本地储存的session的例子,其拥有四个重载函数,还有一个参数为json的
SessionStore store1 = LocalSessionStore.create(vertx);
SessionStore store2 = LocalSessionStore.create(vertx,"myapp3.sessionmap");
SessionStore store3 = LocalSessionStore.create(vertx,"myapp3.sessionmap",10000);
集群session储存
集群session前提是vertx实例为集群模式
其create方法与本地储存的一致,唯一不同的是过期时间改为了重试时间
Vertx.clusteredVertx(new VertxOptions())
.onSuccess(vertx -> { ClusteredSessionStore.create(vertx,ClusteredSessionStore.DEFAULT_SESSION_MAP_NAME,ClusteredSessionStore.DEFAULT_RETRY_TIMEOUT);
});
默认map值与local储存一致,重试超时则是5s
其余存储
以redis举例子
由于使用redis实现session共享或者其他需求的很多,所以在4.*加入了开箱即用的redis实现
通过阅读源码可得,其session中支持八大基本类型,字符串,buffer,ClusterSerializable子类,其余的类型不支持会报错
使用sessionhandler为你的应用加入cookie-session支持
第一步选择一个你喜欢的session储存方式,其次构造一个sessionhandler然后就像普通handler一样,把这个handler加入到route的handler链中
下面用本地储存做示例
Router router = Router.router(vertx);
SessionStore sessionStore = LocalSessionStore.create(vertx);
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
//配置cookie有关信息等,其余api请参照文档
.setCookieHttpOnlyFlag(true)
.setCookieSameSite(CookieSameSite.NONE);
router.route()
//会自动调用context.next()
.handler(sessionHandler);
获取session
必须在此之前存在sessionhandler才能获取到session
下面是一个简单的使用例子
Router router = Router.router(vertx);
router.route()
.handler(SessionHandler.create(SessionStore.create(vertx)))
.handler(rc -> {
Session session = rc.session();
session.put("dreamlike", "ocean");
String dreamlike = session.<String>get("dreamlike");
String remove = session.<String>remove("dreamlike");
});
请注意:
put和get是否允许自定义类型,取决于SessionStore类型,例如本地类型你可以随便放,其本质使用ConcurrentHashMap实现的,但是例如集群则是只支持基础类型(九个),Buffer,JsonObject,JsonArray和实现了ClusterSerializable的子类
使用前请务必查询是否支持
请求周期共享数据
对于传统的servlet规范(不讨论3.1的异步请求),其对于请求从开始到结束都由一个线程完成,无论是否阻塞都是,所以我们往往可以使用ThreadLocal建立请求上下文进行共享,而Vertx则是少数线程交叉执行不同任务,此时我们要想共享此次的请求上下文就不能使用ThreadLocal。
为了解决这种情况,可以使用下面这种方法,利用session传值
router.route()
.handler(SessionHandler.create(SessionStore.create(vertx)))
.handler(rc -> {
Session session = rc.session();
session.put("dreamlike", "ocean");
rc.next();
})
.handler(rc -> {
Session session = rc.session();
String dreamlike = session.<String>get("dreamlike");
//todo something
String remove = session.<String>remove("dreamlike");
});
虽然有点蠢但是具体需求已经达到的,那么有没有更好的方法呢?
是有的,vertx-web中的RoutingContext可以持有任意的数据以便于你在多个handler中共享
router.route()
.handler(rc -> {
rc.put("string", "string");
rc.next();
})
.handler(rc -> {
String s = rc.<String>get("string");
});
你甚至可以通过rc.data()方法获取到所有的上下文数据
源码*
详见io.vertx.ext.web.impl.RoutingContextImpl#getData()
其本质就是一个hashmap,为什么是hashmap?
首先一个连接进来就和一个eventloo线程相绑定,准确来说是注册到其内部的selector,其数据读写都是在单线程中执行,详见netty源码
而vertx的promise会记录当前的context线程,回调依旧会放回原线程,这样读写都无需加锁,hashmap就够用了,同时意味着什么?意味着如果你跨线程使用rc.put等方法则会导致线程不安全问题
跨域处理器
先看一个例子
Router router = Router.router(vertx);
router.route()
.handler(CorsHandler.create().allowedMethod(HttpMethod.GET).allowCredentials(true));
通过allow*方法配置跨域中的各种限制
默认情况下不传入allow origin会有以下效果
当allowCredentials设置为true,其响应头中的access-control-allow-orgin为请求时的orgin
当设置为false时,其值为*
快捷方法
自动attachment
router.route()
.handler(rc -> {
String filename = /。。
rc.attachment(filename);
rc.vertx().fileSystem().readFile(filename)
.onSuccess(rc.response()::end);
});
源码很简单,就是自动解析文件名获取类型放到响应头里面
Future便捷响应
因为vertx是全异步环境,所以很多时候我们的业务handler最终的返回值是个Future类型,我们则需要根据这个future状态挂载回调进行返回响应,就会导致lambda体过长
而使用respond方法就可以帮我们自动挂载,自动写入返回流中
其方法参数为routingcontext -> Future的一个函数式接口
router.route()
.respond(rc -> Future.succeededFuture(213));
其内部实现很简单就是失败或者抛出异常就rc.fail()
而成功则会取出future内容根据其中的结果有不同的处理方法
- Buffer ->如果没有content-type则设置为application/octet-stream,然后调用rc.end()写入
- string ->如果没有 content-type则设置为text/html,然后调用rc.end()写入
- 其余情况,直接json序列化然后发出去
重定向
rc.redirect("https://securesite.com/");
也是响应头和响应码300解决的
返回json
rc.json(object)
序列化为json后写入响应流,请注意:会调用response.end()
还有一些,请参照文档