我个人很喜欢 Vert.X ,它速度快、生态丰富、代码质量高、技术前沿,相比 Spring 它:
- 没有同步,只有响应式编程
- 没有注解(声明式编程)和反射、代理(IOC、AOP)魔法
- 堆栈更少,打包体积更小,启动速度更快
- 工程结构灵活
- 有趣
- 速度更快 (这里是压测数据证明)
⭐目前 vert.x 发行版本是 vert.x 4,不过 vert.x 5 正在开发中(2023年底发行),其中一点是集成 Netty 5 和虚拟线程。可以在 Wiki Vert.x 5 中查看更多关于 vert.x 5 的事情。
Vert.x 5 base on Java11 ,Spring 6 base on Java 17。
对于生产环境下的业务开发,我们已经习惯了(Spring的)DI、命令式编程、项目结构和编程规范,无论是开发 web 服务还是 rpc 服务,我们都很熟悉,控制器、拦截器、服务层、数据层、配置、依赖管理、异常处理、如何调用其他对象/服务/数据库。。。
但是对于 Vert.X,它不是一套框架方案,它没有约定俗成的项目结构或编程规范,也没有像 Spring Boot 那样替你考虑并完成了默认的配置和服务,所以我写了这篇文章来总结下 Vert.X Web 开发中(自我感觉)不错的项目结构或编程规范。
💡如果你想寻找一个像 Spring 那样的框架来代替 Spring 可以考虑:
Spring-Web 提供的功能 Vert.x-Web 都可以做到,但是以 vert.x style 做到。
我寻找 Spring(或着说传统的) Web 开发中常见的主题/编程规范,探索它们发生在 Vert.X 中该怎么做。
如果你想,这里有一个简单基于 Vert.X 的 REST 应用案例 rest data faker,可以看看。
项目结构
类似传统的 Spring Boot 项目结构,各个功能模块可以按包划分
MainVerticle
- 作为应用启动准备工作,由 Launcher 加载执行,在它的start
方法里可以加载配置、启动HTTP服务器、配置路由、部署其他 verticles 等config
- 一些配置web
- Web 相关的工作,比如数据模型、异常处理器、身份验证处理器等exception
- 一些自定义异常*Handler
- 自定义的路由处理器,类似 spring 中的控制器类。调用其他服务层方法可使用 EventBus 通信,可以定义一个void mount(Router router)
方法来配置路由Handler 配置顺序
- 可参照 What's new in Vert.x 4.3 Vertx Web 章节垂直拆分
- 可以按照应用进行垂直拆分,把相关的 Web 层、服务层、数据访问层、模型类等放到一个包里util
- 一些工具类constant
- 一些常量annotation
- 一些注解
关于启动,可以使用 vert.x 的 Launcher 类,就像 start.vertx.io/ 默认的那样;也可以自定义含有 main 方法的启动类。
统一响应数据模型
跟 Spring 的一样,可以自定义统一的数据模型,比如:
{
"status": 200,
"message": "OK",
"data": {}
}
一般有两种请求响应模型方式:
- 请求成功时直接返回数据,失败时返回带有错误信息的数据模型
- 请求成功和失败时返回相同的数据模型
一般大部分场景用第二种。
在路由处理器的最后直接调用 context.json(data)
即可返回 200 json 数据。
异常处理
跟 Spring 一样,我们需要一些东西:
-
StatusResponseException
- 包含了响应状态码和失败信息的运行时异常,可以被异常处理器处理返回上边定义的统一响应数据模型。它可以有很多子类,来具体定义一些常见的情况,比如资源不存在、请求参数错误、身份认证异常等 -
ExceptionHandler
- 异常处理器,拿到RoutingContext
中的异常,根据异常设置具体的响应状态码和数据,当然还可以配置响应头 -
配置异常处理器
- 在配置Router
添加两行代码:router.route("/api/*").failureHandler(ExceptionHandler.create()); router.route().failureHandler(ErrorHandler.create(vertx));
-
使用
- 跟 Spring 一样,在Handler<RoutingContext>
中直接 throwStatusResponseException
异常既可以中断当前的路由处理,转而由ExceptionHandler
处理异常返回响应。
简单的异常处理示例
这是一个简单的异常处理示例,相关的异常类定义如下:
public class StatusResponseException extends RuntimeException{
private final int status;
private final String reason;
public StatusResponseException(int status) {
this(status, null);
}
public StatusResponseException(int status, String reason){
this(status, reason, null);
}
public StatusResponseException(int status, String reason, Throwable t){
super(reason, t);
this.status = status;
this.reason = reason;
}
public int status(){
return status;
}
public String reason(){
return reason;
}
public static StatusResponseException create(int status) {
return new StatusResponseException(status);
}
public static StatusResponseException create(int status, String reason, Object...args ) {
return new StatusResponseException(status, String.format(reason, args));
}
}
异常处理器
@Slf4j
public class ExceptionHandler implements Handler<RoutingContext> {
public static final String PROBLEM_CONTENT_TYPE = "application/problem+json";
public static ExceptionHandler create() {
return new ExceptionHandler();
}
@Override
public void handle(RoutingContext ctx) {
if (ctx.response().ended() ) return;
Throwable t = ctx.failure();
if (t instanceof StatusResponseException e) {
end(ctx, e.status(), e.reason(), null);
} else if (t != null) {
HttpServerRequest request = ctx.request();
log.warn("Handle {} {} happened a exception", request.method(), request.path(), t);
int statusCode = ctx.statusCode() == -1 ? 500 : ctx.statusCode();
end(ctx, statusCode, "服务器内部错误", t.getMessage());
} else {
int statusCode = ctx.statusCode() == -1 ? 500 : ctx.statusCode();
ctx.response().setStatusCode(statusCode).end();
}
}
private static void end(RoutingContext ctx, int statusCode, String message, String detail) {
if (ctx.response().ended()) return;
JsonObject error = new JsonObject()
.put("status", statusCode)
.put("message", message)
.put("detail", detail)
.put("path", ctx.request().path())
.put("timestamp", System.currentTimeMillis());
ctx.response().setStatusCode(statusCode);
ctx.response().putHeader("Content-Type", PROBLEM_CONTENT_TYPE);
ctx.json(error);
}
}
在处理异常时,
- 首先应该判断
RoutingContext
的响应写入是否已经关闭,否则在执行写入会报错 - 从
RoutingContext
中拿出异常,如果为自定义的StatusResponseException
,则根据异常构造包含错误信息的数据并返回 - 如果为其他异常,比如一些数据库错误或别的运行时异常,则构造默认的500异常返回。一般情况下应尽可能减少这种情况,尽量把所有可能出现的异常情况都考虑到,并配置相关的异常处理或转化为
StatusResponseException
- 如果没有异常,则查看
RoutingContext
是否配置了状态码(没有默认取500),直接返回响应
如下是一个返回 400 请求参数错误的响应:
Spring Boot 3 中 MVC 的错误响应支持 RFC7808,因此可以仿照它在 Vert.X 中实现支持 RFC7808 的异常处理器。
过滤器/拦截器
在 Vert.X Web 中没有 Spring 中以过滤器/拦截器命名的类,但是使用 Handler<RoutingContext>
请求处理器就可以达到相同的效果,我们以 LoggerHandler
的实现 LoggerHandlerImpl
为例,它用于记录每个请求的日志:
2023-07-07 19:22:56 [vert.x-eventloop-thread-1] WARN i.v.e.w.h.i.LoggerHandlerImpl - GET /api/exception 400 109 - 127 ms
handle
方法源码如下:
@Override
public void handle(RoutingContext context) {
// common logging data
long timestamp = System.currentTimeMillis();
String remoteClient = getClientAddress(context.request().remoteAddress());
HttpMethod method = context.request().method();
String uri = context.request().uri();
HttpVersion version = context.request().version();
if (immediate) {
log(context, timestamp, remoteClient, version, method, uri);
} else {
context.addBodyEndHandler(v -> log(context, timestamp, remoteClient, version, method, uri));
}
context.next();
}
handle
方法里做的事情相当于 Spring 拦截器 HandlerInterceptor
里 preHandle
做的事;之后通过 next
方法转到下一层请求处理器;配置 addBodyEndHandler
(此外还有 addEndHandler
和 addHeadersEndHandler
)相当于 Spring 拦截器里的 postHandle
。
需要注意的是,拦截器功能的请求处理器在 Router 中声明的 order 要高于自己的业务请求处理器。
在 Vert.X Web 中有很多内置的处理器可以类比 Spring 中的过滤器/拦截器,可以在 Vert.X Web 文档 中了解详情。
下面的身份认证将会使用此方式来实现路由拦截功能。
身份认证
请求参数验证
服务层调用
RPC调用
数据库调用
配置和启动
未完待续