Spring Boot 转 Vert.X 随笔

最近上尝试了把一个典型的 spring boot mvc 项目(提供静态文件/RESTful服务和只依赖sql数据库)转成用 vert.x 编写,发现了一些问题,至此记下。

Spring Boot web 和 Vert.X web 的结构

一个普通的 Spring web mvc 项目结构类似这样:

Spring Boot 转 Vert.X 随笔_第1张图片

  • controller层: 控制url路由映射,request、session、response 的读取、写入
  • service层: 封装了 repository 层并提供服务给 controller 层,执行业务相关的逻辑
  • repository层: 封装了对数据库的调用,并处理数据库表/行和POJO之间的映射
  • model/entity层: repository 层用来把数据库表/行映射的 POJO

一个 client request 过来,依次经过 controller -> service -> repository -> service -> controller(忽略web容器的TCP/HTTP处理),它们都是同步阻塞调用的,所以使用起来非常方便、易于理解和调试,但这也是和 Vert.X 最不同的地方。

一个 Vert.X web 项目结构类似这样:

额。。。好吧,我还没有见过比较规范性的 Vert.X web 项目目录,说下我自己设计的吧

在 vertx web 中,一个 client request 过来,会由 EventLoop 捕获,解析 url 交给对应的处理器就返回了继续 EventLoop 了,处理器或者叫 controller 异步调用 service (通过事件总线并写一个异步结果处理器)就返回了,service 再调用 repository,当repository(可能异步)获取到数据库结果后,返给 service, service 通过消息总线返还结果给 controller,controller 西德异步结果处理器会处理这个结果,比如把它放到 response body 里并表示 response end。之后,vertx web 的 HTTP 服务器会把这个 response 按HTTP协议封装好发给 client。

首先需要了解 Vert.X 的实例单元 verticle 分为三种

援引: Vert.X逐鹿记 https://lang-yu.gitbook.io/vert-x/01-index/01-4-verticle-instance

1.2. Verticle的分类

前文一直提到了Verticle的分类,但我们都没有梳理过,这里先谈谈它的分类。前边章节我们已经介绍了Actor模型,那么这个章节我们需要对Actor给个定义:Vert.x中的Actor(Verticle组件)是有种类的。根据官方教程的描述,Verticle主要分为三种:

Standard:标准类型,我称之为“哨兵”,在Vert.x中,它运行的线程池称为Event Loop,它有一个典型特征就是负责“监听”客户端产生的事件,在该机制中,客户端所有的请求都可抽象成事件(Event)。您可以在编程过程中,将您的代码放心大胆地放到Verticle中执行,因为Vert.x中的Verticle是线程安全的,它的每一个处理事件的处理器(Handler)独占单个线程,您可以把您编写的代码当成单线程代码来处理,甚至于不需要去考虑线程同步的问题。

Worker:工作线程,我称之为“士兵”,在Vert.x中,这种类型的Verticle运行的线程池称为Worker Pool,它不会直接处理客户产生的事件,只会接收Event Bus中发生的事件并去处理,在该机制中,Event Bus中所有的消息都可抽象成事件(Event)。——有时候这样的Verticle可以用于处理后台任务,而官方的推荐是工作线程本身是为了后台一些阻塞式代码(访问文件系统、数据库、网络)设计的,所以对于这种任务场景,尽可能使用Worker类型的Verticle

Multi-Threaded Worker:另一种工作线程,我称之为“神兵”,在Vert.x中,这种类型的Verticle依然是一种Worker,唯一不同的是它可以被多个线程同时执行,所以可以当做“升级的士兵”来对待,为此我们称为“神兵”。

也就是说我们的 HTTP服务器,或者说是 mvc 中的 controller层,处理url路由和 request/session/response 的,是属于 Standard verticle,对于 standard verticle 来说,重要的就是不要阻塞它,也就是说你不能像 spring mvc 里的那样直接在 controller 的方法里使用 service 对象调用阻塞的数据库服务。
而负责调用数据库的就是 worker verticle(通过vertx.deployVerticle(ServiceVerticle.class, new DeploymentOptions().setWorker(true).setInstances(4));可设定),它支持执行阻塞代码。(BTW:无论standard还是worker verticle,当某段代码执行时间超过一定阈值还没结束时,都会抛出一个阻塞时间过长的异常,可能有错欢迎指正)

这里还是介绍下我的目录结构,比较简单,名字还是有点延续 spring mvc 的,可能以后技术长进了会变
Spring Boot 转 Vert.X 随笔_第2张图片

1. controller层

它的功能和 spring mvc 的类似,只不过模式不同。

在 spring mvc 中,是基于 servlet 的 web 服务器(3.0/3.1之后也提供了异步处理和非阻塞IO能力),我们通常使用注解来实现配置路由、request/response参数等,使用实例变量来直接调用 service 服务并获取结果。

而 vert.x web 并不是这样,vert.x 的 web 服务器是 Netty(插句话 Spring WebFulx 也是 Netty,不过它能用 spring mvc 的一些注解,所以写起来比 vert.x web 舒服),路由映射配置不支持注解需要在方法内直接写,并配置对应的的 Handler(函数式接口)。
spring mvc 的控制器单元是一个个加了@RequestMapping()的方法,而 vert.x web 的控制器单元是一个个 Handler 实现类(当然它是个函数式接口,因此用方法引用的话就跟编写方法一样了);spring mvc 的方法里参数是 HttpRequest 和 HttpResponse,或者我们更通常用 @PathVariable 等注解使用它们而省去 HttpRequest, 相比 spring mvc 可以有多个方法参数 vert.x web 的方法参数只有一个 RoutingContext ,它里面包含了很多东西,获取 request、reponse、cookie、session、路径或请求参数、request body等都有直接 api 非常容易(也支持路由转发),能给你很大的自由度。

vert.x web 是一个 Standard verticle,因此你不能直接放进去一个 service 实例变量,让控制器方法去调用它来操作数据库,因此要想和 service 层通信,需要使用 VertX 的事件总线 EventBus。

这里简单介绍下 EventBus(详细的可看Vert.X逐鹿记),如果说 Spring 的核心之一是使用 IOC/DI 来实现应用解耦,那么Vert.X就是使用 EventBus 来实现应用解耦,它是个牛逼的东西。在 DI 中,Bean 之间只依赖接口,且容器自动实例化具体的接口实现并注入给需要的Bean,让我们不用每次都自己手动管理各个依赖。但 Vert.X 的想法并不是这样,你都不需要知道你的依赖长什么样,你只需要往 EventBus 上喊一句话“我要这个数据”,然后就等待别人去回你(对应请求响应通信),或者大喊一句“伙计们帮我打印下这个日志”,然后就不用管了会有群人来帮你打印(对应发布订阅模型)。基于EventBus的解耦,有一个很大的优点,这是 spring 的 DI 做不到的,EventBus 可以是分布式的、跨语言的。也就是说,每个发送给 EventBus 的消息需要实现 ClusterSerializable 接口(通常String、JsonObject、JsonArray就够用了,你也可以自己实现该接口),来保证能够序列化和反序列化在各节点之间传送,而且你用 java 向一个 EventBus 里发送一个消息,我用 js或python 可以直接从 EventBus 里获取到这个消息。PS: 我写过一个实现 EventDriver 和 EventBus 的文章,大家可以瞅瞅 Event Driven 模式详解与一个设计实现

我们在 controller 层的 Handler 里通过向 EventBus 指定地址发送一个 request,
之后写一个回调处理器处理 response 可以完成通信,收到该 request 并处理、回复的是 service 层的 worker verticle,它通过在 EventBus 的该地址注册消费者做到此时,这样就能完成并解耦 controller 和 service 的通信、调用。

比如一个 Handler 返回一个关于学生信息的 json 响应,它通过 EventBus 来调用位于 service 层的服务。

    /**
     * 返回指定 `id` 的学生基本信息,id为学号
     */
    void respondInfoById(RoutingContext routingContext){
        String id = routingContext.pathParam("id");
        bus.<JsonObject>request("topic.studentinfo.id", id, reply -> {
            if(reply.succeeded()){
                JsonObject student = reply.result().body();
                routingContext.response().putHeader("content-type", "application/json").end(OK(student));
            } else{
                routingContext.response().putHeader("content-type", "application/json").end(ERROR(null));
            }
        });
    }

service 向事件总线的地址 topic.studentinfo.id 注册一个消息处理器,调用 repository 服务(下面再说)获取数据库里的数据,并回复消息(因为EventBus传递的消息需要实现特定的接口,因此这里把POJO序列化成 JsonObject 了)给 controller。

    bus.<String>consumer("topic.studentinfo.id", msg -> {
      Future<StudentInfo> future = repository.queryInfoById(msg.body());
      StudentInfo student = null;
      try{
          student = future.get(5, TimeUnit.SECONDS);
      }catch(Exception e){
      }finally{
          msg.reply(JsonObject.mapFrom(student));
      }
    });

在编写 controller 层代码的时候,在 web verticle 里,我只配置和编写了了 Route 和 HttpServer,没有在该类里直接写各种各样的 Handler

  final public class WebVerticle extends AbstractVerticle {
    
    public static final String HOST = "localhost";
    public static final int PORT = 8085;
    private StudentHandlers sh;
    Logger logger = LoggerFactory.getLogger(WebVerticle.class);

    @Override
    public void start() throws Exception {
        
        sh = new StudentHandlers(vertx.eventBus());

        Router router = Router.router(vertx);
        router.route().handler(BodyHandler.create());

        router.get("/api/student/info/:id").handler(sh::respondInfoById);
        router.get("/api/student/info/department/:department").handler(sh::respondInfosByDepartment);
  		// others route ...

        startHTTPServer(router);
    }

    private void startHTTPServer(Router router){
        vertx.createHttpServer().requestHandler(router).listen(PORT, HOST, result -> {
            if (result.succeeded()) {
                logger.info("WEB 服务器启动成功!port: " + result.result().actualPort());
            } else {
                logger.error("WEB 服务器启动失败!case: " + result.cause().getMessage());
            }
        });
    }

}

而是把服务目的相同的 Handler 都放到同一个类中编写、管理,之后直接在 route 中放入特定的方法引用即可,这样做比较容易编写和管理。

public class StudentHandlers {

  private EventBus bus;

  StudentHandlers(EventBus bus){
      this.bus = bus;
  }

  /**
   * 返回指定 `id` 的学生基本信息,id为学号
   */
  void respondInfoById(RoutingContext routingContext){
      String id = routingContext.pathParam("id");
      bus.<JsonObject>request("topic.studentinfo.id", id, reply -> {
          if(reply.succeeded()){
              JsonObject student = reply.result().body();
              routingContext.response().putHeader("content-type", "application/json").end(OK(student));
          } else{
              routingContext.response().putHeader("content-type", "application/json").end(ERROR(null));
          }
      });
  }
  
  // others handler ...
}

这些处理器方法可以是实例方法,也可以是静态方法,因为我想用 web Verticle 里的 vertx.EventBus 实例,因此就在构造器中注入了这个 EventBus,使用了实例方法。当然,我觉得可以完全改成静态方法,因为 RoutingContext 中可以获取到 vertx 单例,进而得到 EventBus。

2. service层

有些就像上面说的一样,通过 EventBus 来和 controller 层通信。

  public class StudentInfoServiceVerticle extends AbstractVerticle {
    
    private StudentInfoRepository repository = null;

    @Override
    public void start() throws Exception {

        JDBCClient client = DBConfig.getJDBCClient(vertx);

        client.getConnection( res -> {
            if(res.succeeded()){
                System.out.println("数据库连接成功");
                repository = new StudentInfoRepository(client);
            }else{
                System.out.println("数据库连接失败");
                vertx.close();
            }
        });

        EventBus bus = vertx.eventBus();

        bus.<String>consumer("topic.studentinfo.id", msg -> {
            Future<StudentInfo> future = repository.queryInfoById(msg.body());
            StudentInfo student = null;
            try{
                student = future.get(5, TimeUnit.SECONDS);
            }catch(Exception e){
            }finally{
                msg.reply(JsonObject.mapFrom(student));
            }
        });
  
  		// others bus consumer ...
  	}
  }

这个 repository 层是什么?我没有把它设计为一个 verticle,它是一个普通的java类,把里面需要传入一个 JDBCClient 实例,而JDBCClient实例得创建需要 vert.x 实例,因此对 JDBCClient 实例得创建放到了 service 层,因为 service 是一个 verticle 拥有 vertx 实例。关于JDBCClient 可以参考https://vertx.io/docs/vertx-jdbc-client/java/。

service 层和 repository 层比较紧密,接下来看 repository 的一个简单代码。

3. repository层

public class StudentInfoRepository {

  public static final String QUERY_INFO_BY_ID_SQL = "SELECT * FROM INFO WHERE XH = ?";
  // others SQL String
  private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(15);
  private JDBCClient client = null;

  public StudentInfoRepository(JDBCClient client) {
      this.client = client;
  }

  /**
   * 异步获取 student
   * 
   * @param id
   * @return
   */
  public Future<StudentInfo> queryInfoById(String id) {
      JsonArray params = new JsonArray().add(id);
      CompletableFuture<StudentInfo> future = new CompletableFuture<>();
      EXECUTOR.execute(() -> {
          client.queryWithParams(QUERY_INFO_BY_ID_SQL, params, res -> {
              if (res.succeeded()) {
                  ResultSet resultSet = res.result();
                  StudentInfo student = OrmUtils.toStudent(resultSet);
                  future.complete(student);
              } else if (res.failed()) {
                  System.out.println("查询失败");
                  future.completeExceptionally(res.cause());
              }
          });
      });
      return future;
  }

	// other methods ...
}

这一层的编写,非常依赖于 JDBC 客户端,我这里用的是 Vert.x JDBC client,得到的数据库数据需要我自己手动转化为POJO,为此我写了一个简单的工具类 OrmUtils。

需要注意的是,SQL数据库驱动JDBC需要是支持异步的,否则使用这样的写法意义不大,速度没什么提升还不如直接用ORM框架呢,但是用(同步)ORM框架的话,Vert.X的异步编程也发挥不出啥优势了。

目前 Vert.X 有几款支持异步的JDBC客户端:
Spring Boot 转 Vert.X 随笔_第3张图片

Hibernate ORM框架也有支持异步的了: Hibernate Reactive

Spring Boot 转 Vert.X 随笔_第4张图片

en,最后在说下一个细节,为什么我在 respository 使用了一个 ExecutorService 线程池,来执行
client.queryWithParams 操作?好吧,本来我以为 client.queryc 操作会直接分配一个线程去执行,但是我发现在 serveice 层调用 future.get() 时,client.query 里的回调函数一直没有执行,它只有等到 service 里的代码执行完之后才会开始运行,但这样 future.get() 会一直阻塞,因为 future.complete(student) 是在client.query 操作里才会完成的时,没有设定结果,那么 get() 会一直阻塞,service 层代码无法执行结束,这样 client.query 里代码也就无法执行。我也不懂为什么会这样,反正头疼了半天,最后只能再开一个线程池来运行 client.query 了。

4. model层

这里和 spring mvc 的一样,只不过没法使用 JPA 注解自己手动编写映射挺麻烦的,我还没见到异步 JPA 实现。

启动

spring boot 应用和 vert.x 应用的启动还是挺容器的,都是在外层包下写一个启动类,运行 main 方法。

你可能感兴趣的:(Coding,Adventure,spring,java,vertx,异步编程,异步jdbc)