Vert.x Web
定义:基于Vert.x Core基础上的框架,相比原生vertx更适合作为web应用的开发
路由器(Router)
定义:路由器将找到匹配该请求的第一个路由,并将请求传递给该路由,并持有一个对应的处理器用于接收请求,然后结束响应或把请求传递给下一个匹配的处理器
Vertx vertx = Vertx.vertx(); HttpServer server = vertx.createHttpServer(); //创建路由器 Router router = Router.router(vertx); //通过路由器路由,这里接收所有的请求方法和路径 //通过disable或enable启用或关闭 router.route().disable().enable().handler(ctx -> { // 所有的请求都会调用这个处理器处理 HttpServerResponse response = ctx.response(); response.putHeader("content-type", "text/plain"); // 结束响应 response.end("Hello World!"); }); server.requestHandler(router).listen(8080,"localhost");
把请求传递给下一个匹配的处理器
//创建路由 Route route = router.route("/some/path/"); route.handler(ctx -> { //处理multipart类型的表单数据 ctx.request().setExpectMultipart(true); 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(); });
通过respond快速响应json或pojo数据
router .get("/some/path") // 这里响应处理器自动将content type设置成 "application/json"、Pojo序列化成json .respond( ctx -> Future.succeededFuture(new Pojo()));
使用阻塞式处理器: Vert.x 会使用 Worker Pool 中的线程而不是 Event Loop 线程来执行阻塞式操作
//false表示不关心阻塞式处理器的执行顺序,可以以并行的方式执行 router.route().blockingHandler(ctx -> { // 执行某些同步的耗时操作 service.doSomethingThatBlocks(); // 响应JSON数据 ctx.json(new JsonObject().put("hello", "vert.x")); },false);
匹配
路径匹配
- 精准匹配:
Route route = router.route().path("/some/path/");
- 前缀匹配:
Route route = router.route().path("/some/path/*");
- 正则表达式匹配:
Route route = router.route().pathRegex(".*foo");
- 精准匹配:
捕捉路径参数
router //:(占位符)+参数名 .route(HttpMethod.GET, "/fetch/:param1-:param2") .handler(ctx -> { //捕捉路径参数 String param1 = ctx.pathParam("param1"); String param2 = ctx.pathParam("param2"); log.info("param1:"+param1+" param2:"+param2); });
MIME类型匹配
通过consumes方法表示该处理器可接受的请求的MIME类型
router.route() //只处理content-type为text/html 或 text/x 的请求 .consumes("text/html") .consumes("text/*") .handler(ctx -> {});
通过produces表达响应的MIME类型的路由,通常与Content-type处理器匹配
router.route() // .produces("application/json") .handler(ctx -> { HttpServerResponse response = ctx.response(); //要与produces设置的内容匹配 response.putHeader("content-type", "application/json"); response.end("你好"); });
路由匹配失败:如果没有路由符合任何特定请求,则Vert.x-Web将根据匹配失败发出错误消息,可通过
Router
的errorHandler
方法处理。- 404 没有匹配的路径
- 405 路径匹配但是请求方法不匹配
- 406 路径匹配,请求方法匹配但是它无法提供内容类型与
Accept
请求头匹配的响应 - 415 路径匹配,请求方法匹配但是它不能接受
Content-type
- 400 路径匹配,请求方法匹配但是它接收空方法体
路由
路由顺序:
如果有多个路由,会按照定义的顺序来匹配其中一个,或者通过
route.order()
方法来设置顺序,如果调用(RoutingContext)ctx.next()
,则会传递给下一个路由,也可以指定一个路由最后调用route.last()
重新路由
ctx.reroute
重新路由后状态代码和失败原因将会重置router.get("/home").handler(ctx -> ctx.response() .setStatusCode(404) .end("not found dashboard html here!!!")); router.get("/dashboard").failureHandler(ctx -> { if (ctx.statusCode() == 404) { //访问/dashboard失败,重新路由到/home,并传递variable变量给下一个处理器 ctx.put("variable","value"); ctx.reroute("/home"); } else { ctx.next(); } });
子路由器:将路由器挂载在其他路由当中
使用规则:
- 路由路径必须以通配符结尾。
- 允许使用参数,但不能使用复杂的正则表达式模式。
- 不能在父路由器中定义处理器
- 只能挂载一个子路由器
Router mainRouter = Router.router(vertx); //子路由 Router restAPI = Router.router(vertx); restAPI.get("/products/:productID").handler(ctx -> { log.info("get"); ctx.response().end("get"); }); restAPI.post("/products/:productID").handler(ctx -> { log.info("post"); ctx.response().end("post"); }); //将restAPI路由器挂载到mainRouter中的路由,当访问productsAPI/products/123时,会调用子路由器的处理器 mainRouter.route("/productsAPI/*").subRouter(restAPI);
错误处理:当路由器的某个路径的handler处理器出现异常时,与该请求路径匹配的 错误处理器(通过
failureHandler
注册)会捕获到这些错误进行统一处理router.route("/path/123").handler(ctx -> { ctx.fail(403); }) router.route("/path/*").handler(ctx -> { throw new RuntimeException("test"); }).failureHandler(ctx -> { //能匹配到“/path/123”的路由 ctx.response().setStatusCode(500).end(); log.error("handler处理出错"); });
请求体处理器(BodyHandler):获取请求体, 限制请求体大小以及处理文件上传
BodyHandler bodyHandler = BodyHandler.create(); //限制请求体大小 bodyHandler.setBodyLimit(1024*10); //不将请求的表单属性直接合并为参数,一般用于文件上传 bodyHandler.setMergeFormAttributes(false); //注册该请求体处理器 router.route().handler(bodyHandler); router.post().handler(ctx -> { //获取请求体 RequestBody body = ctx.body(); JsonObject jsonObject = body.asJsonObject(); //读取文件上传的信息,每个文件上传均由一个FileUpload实例描述 List<FileUpload> uploads = ctx.fileUploads(); });
允许跨域:
//跨域处理器 CorsHandler corsHandler = CorsHandler.create(); //配置允许的客户端来源 corsHandler .addOrigin("http://localhost:8080") .allowCredentials(true) .allowedMethod(HttpMethod.GET); //注册跨域处理器 router.route().handler(corsHandler);
静态资源处理:
Vert.x首次在类路径中找到资源时,它将提取该资源并缓存在磁盘上的临时目录中
//静态资源处理器,并设置类路径中的根路径 StaticHandler staticHandler = StaticHandler.create("static"); //配置主页,默认为index.html staticHandler.setIndexPage("index.html"); //设置服务端的缓存(缓存文件的元数据,最后一次修改时间、文件大小等)过期时间 staticHandler.setCacheEntryTimeout(1000); //设置客户端浏览器的缓存(缓存文件内容)最大期限值 staticHandler.setMaxAgeSeconds(1000); //设置缓存最大条目数 staticHandler.setMaxCacheSize(10); //注册StaticHandler router.route("/static/*").handler(staticHandler);
配置缓存:默认情况下,静态资源处理器会在响应头设置缓存信息(
cache-control
,last-modified
, 和date
),使浏览器能有效缓存文件,如果不需要处理缓存头,则可以使用setCachingEnabled
将其禁用。cache-control
:缓存多长时间,默认情况下设置为max-age=86400
,可通过setMaxAgeSeconds
配置。Last-Modified
:指的是请求的资源内容本身最后被修改的时间date
:服务器端响应的生成时间戳,作为缓存有效期计算(max-age
)的基准
如果浏览器发送带有
if-modified-since
标头的GET或HEAD请求(表示资源在上次被修改的时间),如果至此资源没有被修改过,则返回304
状态码,指示浏览器使用其本地缓存的资源。目录列表:
客户端可以获取目录列表,使用
setDirectoryListing
启用,当目录列表被启用时,返回的内容取决于accept
标头中的内容类型。对于目录列表,可以使用
setDirectoryTemplate
配置用于呈现目录列表页面的模板关闭在磁盘上的文件缓存:
VertxOptions options = new VertxOptions(); options.setFileSystemOptions(new FileSystemOptions().setFileCachingEnabled(false));
本地化
定义:Vert.x Web解析
Accept-Language
请求头, 来确定哪个是客户端的首选语言环境。Route route = router.get("/localized").handler(ctx -> { for (LanguageHeader language : ctx.acceptableLanguages()) { switch (language.tag()) { case "en": ctx.response().end("Hello!"); return; case "fr": ctx.response().end("Bonjour!"); return; case "pt": ctx.response().end("Olá!"); return; case "es": ctx.response().end("Hola!"); return; } } ctx.response().end("我们不知道用户使用的语言,因此请告知: " + ctx.preferredLanguage()); });
上下文数据
在前一个处理器中put对象,然后再下一个处理器中get该对象,这两个处理器必须通过
ctx.next()
进行联系router.get("/some/path").handler(ctx -> { ctx.put("foo", "bar"); ctx.next(); }); router.get("/some/path").handler(ctx -> { String bar = ctx.get("foo"); // 用bar对象做一些事情 ctx.response().end(); });
或者通过元数据传递,但元数据的作用域在route内,所以不同route无法传递该元数据
router .route("/metadata/route") .putMetadata("metadata-key", "123") .handler(ctx -> { Route route = ctx.currentRoute(); String value = route.getMetadata("metadata-key"); log.info(value); ctx.next(); }).handler(ctx -> { Route route = ctx.currentRoute(); String value = route.getMetadata("metadata-key"); log.info(value); ctx.end(); });
attachment函数:触发用户浏览器去下载文件或者用本地配置的默认应用程序打开它。
router.route("/").handler(ctx -> { vertx.fileSystem().readFile("C:\\Users\\MyPDF.pdf", result -> { if (result.succeeded()) { ctx.attachment("MyPDF.pdf").end(result.result()); } else { ctx.fail(result.cause()); } }); });
content-type校验:
ctx.is("text/html"); ctx.is("application/json");
验证客户端缓存请求是否新鲜,如果新鲜说明数据未背修改,可以返回304状态码
//更新响应头字段Last-Modified(最后一次修改的时间) if(修改数据){ ctx.lastModified(Instant.now()); } //对请求头中If-Modified-Since和If-None-Match和响应头中的Last-Modified和ETag进行比较,如果相同说明新鲜 if (ctx.isFresh()) { //由于客户端缓存是新鲜的,所以可以直接停止并返回304 ctx.response().setStatusCode(304).end(); return; }
操作会话
处理Cookie
HttpServerRequest request = ctx.request(); //通过getCookid()获取指定cookie Cookie name = request.getCookie("name"); String value = name.getValue(); //通过cookies()获取所有cookie Set<Cookie> cookies = request.cookies(); //添加一个cookie到响应中 ctx.response().addCookie(Cookie.cookie("othercookie", "somevalue"));
处理Session
Session的存储器:处理Session需要有一个存储Session的存储器
本地Session储存器:当只有一个vertx实力且使用粘性session保证HTTP请求路由到同一vertx实例上时(一致性hash),此存储是合适的,基于sharedata里的local maps实现
// 创建一个本地session储存器,制定了local shared map名和设置了10s的清理周期用于清理过期session SessionStore store3 = LocalSessionStore.create( vertx, "myapp.sessionmap", 10000);
集群Session存储器:如果不使用粘性会话时,使用该存储器,基于sharedata里的async maps实现
Vertx.clusteredVertx(new VertxOptions(), res -> { Vertx vertx = res.result(); // 指定分布式map的名字创建集群session存储器 SessionStore store2 = ClusteredSessionStore.create( vertx, "myclusteredapp.sessionmap"); });
其他存储:
- 使用redis作为后端集中存储
- cookie存储器,所有会话数据都通过cookie发送会客户端
Session处理器:确保session从session储存器中被自动地找出来,然后在到达应用程序处理器之前将其放置在路由上下文中,响应完成之后 session 会被自动写回到储存器中。
Router router = Router.router(vertx); //使用默认配置创建一个集群session储存器 SessionStore store = ClusteredSessionStore.create(vertx); SessionHandler sessionHandler = SessionHandler.create(store); // session处理器控制用于session的cookie // 比如它可以包含同站策略的配置 sessionHandler.setCookieSameSite(CookieSameSite.STRICT); // 仅在需要使用会话时创建会话 sessionHandler.setLazySession(true); // 确保所有请求都可以路由经过这个session处理器 router.route().handler(sessionHandler); // 经过这个session处理器后开始正式处理 router.route("/somepath").handler(ctx -> { Session session = ctx.session(); // 放置一些数据到session中 session.put("foo", "bar"); // 获取session中的数据 int age = session.get("age"); // 从session中移除数据 JsonObject obj = session.remove("myobj"); // 销毁session session.destroy(); });
认证授权
JWT示例:
// 配置 JWTAuth JWTAuthOptions config = new JWTAuthOptions() // 使用 Keystore // .setKeyStore(new KeyStoreOptions() // .setPath("keystore.jceks") // .setPassword("secret")) // 使用对称密钥/非对称密钥对 .addPubSecKey(new PubSecKeyOptions() .setAlgorithm("HS256") // 签名算法 .setBuffer("ailu") // 密钥 ); // 可选:配置 JWT 验证选项 (例如 issuer, audience,过期时间) JWTOptions jwtOptions = new JWTOptions().setExpiresInSeconds(1); config.setJWTOptions(jwtOptions); JWTAuth jwtAuth = JWTAuth.create(vertx, config); // 以'/private/'路径开头的请求都会被保护 router.route("/private/*").handler(ctx -> { // 生成token:String token = jwtAuth.generateToken(new JsonObject().put("id", "1")); //从cookie中获取Jwt token String token = ctx.request().getCookie("token").getValue(); JsonObject authInfo = new JsonObject().put("token", token); //进行验证 Future<User> authenticate = jwtAuth.authenticate(authInfo); authenticate.andThen(res -> { log.info("Authentication result: " + res.result()); }); });
多租户
根据客户端的请求头数据,为不同的用户做不同事
//根据客户端的X-Tenant请求头字段 MultiTenantHandler multiTenantHandler = MultiTenantHandler.create("X-Tenant") .addTenantHandler("tenant-A", ctx -> { // 为租户A做一些事情 log.info("tenant-A"); }) .addTenantHandler("tenant-B", ctx -> { // 为租户B做一些事情 log.info("tenant-B"); }) .addDefaultHandler(ctx -> { // 当没有租户匹配时,做一些事情 log.info("none"); }); //注册对应的处理器 router.route("/").handler(multiTenantHandler);
几种处理器
日志处理器
记录请求的日志信息:
[Sat, 12 Apr 2025 14:03:18 GMT] "GET / HTTP/1.1" 200 19 "-" "PostmanRuntime/7.43.0"
定义:
router.route("/").handler(LoggerHandler.create())
超时处理器
- 请求超时,会给客户端返回一个 503 的响应
- 定义:
router.route("/").handler(TimeoutHandler.create(5000))
响应时间处理器
- 从接收请求到写入响应的毫秒数写入到响应的
x-response-time
- 定义:
router.route("/").handler(ResponseTimeHandler.create())
Content-type处理器
配合produces方法,自动设置最匹配的响应的
Content-Type
消息头定义:
//注册Content-type处理器 router.route("/api/*").handler(ResponseContentTypeHandler.create()); router .get("/api/books") //表达响应头的Content-type字段需求 .produces("text/xml") .produces("application/json") .handler(ctx -> findBooks() .onSuccess(books -> { //获取请求可接受的Content-type类型 if (ctx.getAcceptableContentType().equals("text/xml")) { ctx.response().end(toXML(books)); } else { ctx.response().end(toJson(books)); } }) .onFailure(ctx::fail));