Vert.x学习(四)—— Vertx.Web,网页应用开发

发布于:2025-04-21 ⋅ 阅读:(15) ⋅ 点赞:(0)

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将根据匹配失败发出错误消息,可通过RoutererrorHandler方法处理。

    • 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();
    });
    

认证授权

  • 文档地址:通用认证和授权 | Eclipse Vert.x

  • 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));
    


网站公告

今日签到

点亮在社区的每一天
去签到