记录自己在学习和使用 Vert.x 中觉得还算优雅的 Coding Styles
自定义配置 Vert.x 内置的 ObjectMapper
可以让 Vert.x 的 json 支持 Java8 Time模块、自定义日期时间格式等,就像 Spring 配置 ObjectMapper Bean 那样个性化 Json 处理,通常这块代码应该放在 vertx 部署 verticles 之前,当然记得添加 jackson 依赖
JavaTimeModule module = new JavaTimeModule();
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(f));
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(f));
DatabindCodec.mapper().registerModule(module);
DatabindCodec.prettyMapper().registerModule(module);
DatabindCodec.mapper().findAndRegisterModules();
DatabindCodec.prettyMapper().findAndRegisterModules();
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<!-- Parameter names -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.13.2</version>
</dependency>
<!-- Java 8 Date/time -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.2</version>
</dependency>
<!-- Java 8 Datatypes -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.13.2</version>
</dependency>
Web HTTP Endpoint 的管理
Vert.x 使用函数式风格来组装HTTP路由处理器,Spring 也存在类似的 Functional Endpoints | Spring MVC Docs,我发现一个比较方便管理 path 和请求处理器 的方式:
和 Spring Controller 一样,我们把处理相同东西的请求处理器都封装在一个类中,命名格式 **RouteHandler
,比如 ApplicationRouteHandler
——这是我写过的一个处理 Application
RESTful 接口的“控制器类”:
@Slf4j
public class ApplicationRouteHandler {
public static final String PATH = "/api/application";
private final ApplicationDetailService service;
public ApplicationRouteHandler(ApplicationDetailService service) {
this.service = Objects.requireNonNull(service);
}
public static ApplicationRouteHandler create(ApplicationDetailService service) {
return new ApplicationRouteHandler(service);
}
public void mount(Router router) {
router.get("/api/application/:id").handler(this::get);
router.get("/api/application").handler(this::getList);
router.post("/api/application").handler(this::post);
router.put("/api/application/:id").handler(this::put);
router.delete("/api/application/:id").handler(this::delete);
log.info("{} has mounted route /api/application", this);
}
private void get(RoutingContext ctx) {
try {
long id = Long.parseLong(ctx.pathParam("id"));
var app = service.get(id);
ctx.json(app);
} catch (NumberFormatException e) {
throw new StatusResponseException(400, "Invalid path param [id]");
} catch (NoSuchElementException e) {
throw new StatusResponseException(404, "Resource not existed");
}
}
private void getList(RoutingContext ctx) {
int pageIndex = NumberUtil.parseInt(ctx.queryParams().get("pageIndex"), 1);
int pageSize = NumberUtil.parseInt(ctx.queryParams().get("pageSize"), 20);
var totalable = service.get(pageIndex, pageSize);
ctx.json(PageableModel.of(totalable, pageIndex, pageSize));
}
private void post(RoutingContext ctx) {
var app = ctx.body().asPojo(ApplicationDetail.class);
if (!checkApplicationDetail(app)) {
throw new StatusResponseException(400, "Invalid data");
}
String creator = ctx.user().get("name");
service.create(app.group, app.name, app.intro, creator);
log.info("Create new {}", app);
ctx.response().setStatusCode(201).end();
}
private void put(RoutingContext ctx) {
try {
long id = Long.parseLong(ctx.pathParam("id"));
if (!ctx.body().asJsonObject().containsKey("intro")) {
throw new StatusResponseException(400, "Invalid data");
}
var intro = ctx.body().asJsonObject().getString("intro");
service.updateIntro(id, intro);
ctx.response().setStatusCode(200).end();
} catch (NumberFormatException e) {
throw new StatusResponseException(400, "Invalid path param [id]");
} catch (NoSuchElementException e) {
throw new StatusResponseException(404, "Resource not existed");
}
}
private void delete(RoutingContext ctx) {
try {
long id = Long.parseLong(ctx.pathParam("id"));
service.delete(id);
ctx.response().setStatusCode(200).end();
} catch (NumberFormatException e) {
throw new StatusResponseException(400, "Invalid path param [id]");
}
}
private static boolean checkApplicationDetail(ApplicationDetail app) {
return !StringUtil.isBlank(app.group, app.name);
}
}
这里注入了一个
service
对象在请求处理器里直接使用(因为写的是一个demo service里用的 List、Map 做存储),实际使用中可以使用EventBus
和 Vert.x Web 中其他 Handler (如 BodyHandler、ErrorHandler、TimeoutHandler、LoggerHandler) 一样,提供一个 create
工厂方法。核心是 mount
方法,它用来把该 Handler 里所有的请求处理器挂在到对应的 HTTP Path 中,ApplicationRouteHandler
在这里被使用
/**
* WebVerticle
*/
public class WebVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
Router router = Router.router(vertx);
ApplicationRouteHandler.create(applicationDetailService).mount(router);
}
Web 自定义认证处理器
Vert.x 提供了非常多标准的认证方式,自定义一个也非常容易,这里实现一个非常简单的用户认证逻辑
- 从 HTTP Headers 中取出保存用户标识的自定义 header
SERVICE-AUTH-USERNAME
的值username
- 如果该
username
为空 (值为空或header不存在),返回 401 - 如果
username
不为空,但是该username
不在用户服务里,返回 403 - 如果在用户服务里,把该
username
的用户信息放到 Vert.x 的RoutingContext
中,继续向下处理
代码如下:
public class SimpleUserAuthHandler implements UserAuthHandler {
public static final String AUTH_HEADER_NAME = "SERVICE-AUTH-USERNAME";
private final SimpleUserStore simpleUserStore;
public SimpleUserAuthHandler(SimpleUserStore simpleUserStore) {
this.simpleUserStore = Objects.requireNonNull(simpleUserStore);
}
@Override
public void handle(RoutingContext ctx) {
String name = ctx.request().getHeader(AUTH_HEADER_NAME);
if (name == null) {
ctx.response().setStatusCode(401).end();
} else {
var opional = simpleUserStore.findByName(name);
if (opional.isEmpty()) {
ctx.response().setStatusCode(403).end();
} else {
ctx.setUser(User.create(opional.get().toJson()));
ctx.next();
}
}
}
}
使用:
router.route("/api/*").handler(UserAuthHandler.create(simpleUserStore));
这里为了避免像 index.html
等静态资源也需要认证,指定了路由前缀。值得注意的是 UserAuthHandler
的 Order,它应该在 LoggerHandler
、BodyHandler
等后面,在业务路由处理器 Handlers 前面——实际上,这里有一个来自 Vert.x 4.3 的官方说明:
Web 异常处理
像 Spring 那样,vertx web 可以捕获请求处理器中的未捕获异常,进行全局处理,返回自定义的信息,如
{
"status": 404,
"message": "Resource not existed",
"detail": null,
"path": "/api/application/6",
"timestamp": 1659276219253 }
ErrorModel
- Rest error model
public record ErrorModel(
int status,
String message,
String detail
) {
public static ErrorModel of(int status, String message){
return of(status, message, null);
}
public static ErrorModel of(int status, String message, String detail) {
return new ErrorModel(status, message, detail);
}
public JsonObject toJson(){
return new JsonObject()
.put("status", status)
.put("message", message)
.put("detail", detail);
}
}
StatusResponseException
- 包含响应信息的异常
public class StatusResponseException extends RuntimeException{
private final int status;
private final String reason;
public StatusResponseException(int status, String reason){
super(reason);
this.status = status;
this.reason = reason;
}
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 build(int status, String reason, Object...args ) {
return new StatusResponseException(status, String.format(reason, args));
}
}
声明异常处理器:
public class WebVerticle extends AbstractVerticle {
private static final Logger log = LoggerFactory.getLogger(WebVerticle.class);
@Override
public void start(Promise<Void> startPromise) throws Exception {
router.route("/api/*").failureHandler(WebVerticle::handleFailure);
router.route().failureHandler(ErrorHandler.create(vertx, true));
}
private static void handleFailure(RoutingContext ctx){
Throwable t = ctx.failure();
if (t instanceof StatusResponseException e){
JsonObject error = ErrorModel.of(e.status(), e.reason())
.toJson()
.put("path", ctx.request().path())
.put("timestamp", System.currentTimeMillis());
ctx.response().setStatusCode(e.status());
ctx.json(error);
} else {
HttpServerRequest request = ctx.request();
log.warn("{} {} 发生了异常", request.method(), request.path(), t);
int statusCode = ctx.statusCode();
statusCode = statusCode == -1 ? 500 : statusCode;
JsonObject error = ErrorModel.of(statusCode, "内部服务错误", t.getMessage())
.toJson()
.put("path", ctx.request().path())
.put("timestamp", System.currentTimeMillis());
ctx.response().setStatusCode(statusCode);
ctx.json(error);
}
}
}
注意异常处理器的 Order,它通常都是在最后声明的。
接下来看看使用:
// router.get("/api/application/:id").handler(this::get);
private void get(RoutingContext ctx) {
try {
long id = Long.parseLong(ctx.pathParam("id"));
var app = service.get(id);
ctx.json(app);
} catch (NumberFormatException e) {
throw new StatusResponseException(400, "Invalid path param [id]");
} catch (NoSuchElementException e) {
throw new StatusResponseException(404, "Resource not existed");
}
}
分别请求
- GET /api/application/abc
- GET /api/application/123456
返回:
-
{ "status": 400, "message": "Invalid path param [id]", "detail": null, "path": "/api/application/abc", "timestamp": 1659277011836 }
-
{ "status": 404, "message": "Resource not existed", "detail": null, "path": "/api/application/123456", "timestamp": 1659277098625 }
部署 verticle 处理
这里打印了每个 verticle 部署结果日志,如果失败了则会退出程序
public class Launcher {
private static final Logger log = LoggerFactory.getLogger(Launcher.class);
public static void main(String[] args) throws Exception {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(() -> {
WebVerticle webVerticle = new WebVerticle();
// other handle
return webVerticle;
}, new DeploymentOptions().setInstances(3))
.onComplete(handleDeployComplete(WebVerticle.class));
}
static Handler<AsyncResult<String>> handleDeployComplete(Class<? extends Verticle> verticleClass){
return ar -> {
if (ar.failed()) {
log.error("{} deploy failed", verticleClass.getSimpleName(), ar.cause());
log.error("The JVM exited");
System.exit(-1);
} else {
log.info("{} deploy succeed [ID={}]", verticleClass.getSimpleName(), ar.result());
}
};
}
}