我的 Vert.x 代码风格 Vert.x Coding Style

386 阅读5分钟

记录自己在学习和使用 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 提供了非常多标准的认证方式,自定义一个也非常容易,这里实现一个非常简单的用户认证逻辑

  1. 从 HTTP Headers 中取出保存用户标识的自定义 header SERVICE-AUTH-USERNAME 的值 username
  2. 如果该 username 为空 (值为空或header不存在),返回 401
  3. 如果 username 不为空,但是该 username 不在用户服务里,返回 403
  4. 如果在用户服务里,把该 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 等静态资源也需要认证,指定了路由前缀。值得注意的是 UserAuthHandlerOrder,它应该在 LoggerHandlerBodyHandler等后面,在业务路由处理器 Handlers 前面——实际上,这里有一个来自 Vert.x 4.3 的官方说明:

image.png

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