使用Vert.x + SpringBoot編寫業(yè)務(wù)系統(tǒng)

這一期文章主要為大家介紹如何將Vert.x與SpringBoot結(jié)合起來編寫最最最常見的業(yè)務(wù)系統(tǒng),即數(shù)據(jù)庫增刪改查。

談兩句SpringBoot

SpringBoot大家都很熟了,一個(gè)快速開發(fā)框架,其最大的特點(diǎn)是可將Spring應(yīng)用打成可執(zhí)行jar包,從而不再依賴外部容器,如Tomcat??赡芙^大多數(shù)人在使用SpringBoot時(shí)一定離不了嵌入式Tomcat, 從而造成了一想到SpringBoot就會將其與SpringMVC聯(lián)系在一起的現(xiàn)象。其實(shí)我們可以只使用SpringBoot的一鍵執(zhí)行和提供Spring環(huán)境的特性,Web層直接替換成Vert.x. 此外,一些短時(shí)執(zhí)行的任務(wù)也可以用這種方式來寫,簡直不能更爽。

Vertx-web的請求路由

上一篇寫的Http Server是沒有路由的,所有的請求都會由同一個(gè)Handler處理。如果是只提供一個(gè)簡單的API服務(wù)這樣是沒有問題的,但在實(shí)際業(yè)務(wù)系統(tǒng)中一般接口數(shù)量都比較多,這時(shí)候就需要一個(gè)路由組件來將不同的Path, Method映射到不同的handler上,使用方法如下:

HttpServer server = vertx.createHttpServer();
        Router router = Router.router(vertx); // (0)
        router.route("/a/b/c/path") // (1)
                .handler(BodyHandler.create()) // (2)
                .handler(demoHandler) // (3)
                .blockingHandler(blockHandler); // (4)
        
        server.requestHandler(router::accept) // (5)
                .listen(8080);

(0): 構(gòu)造一個(gè)Router。

(1): 添加對/a/b/c/path的路由。這里也可以使用重載的帶有HTTP Method的方法。

(2): 當(dāng)收到path為/a/b/c/path的請求時(shí),先調(diào)用BodyHandler。這里BodyHandler是Vert.x提供的處理器,只有在請求處理鏈路的開頭添加了此Handler我們才能在后續(xù)的Handler中拿到請求體。

(3): 添加我們的業(yè)務(wù)處理器, 此處理器會在NIO線程中執(zhí)行。

(4): 添加含有阻塞調(diào)用的業(yè)務(wù)處理器,此處理器會在worker線程池中執(zhí)行,不會block NIO線程。

(5): 將Router的Handler設(shè)為整個(gè)HTTP Server的Handler。

其中Handler是一個(gè)函數(shù)式接口,定義如下:

@FunctionalInterface
public interface Handler<E> {

  /**
   * Something has happened, so handle it.
   *
   * @param event  the event to handle
   */
  void handle(E event);
}

可見,vertx添加請求處理器的過程非常簡單,而且寫起來也很爽。

不過這里還是會有一個(gè)問題,我們的服務(wù)會有大量的Handler, 大量的路由,這時(shí)候就只能在這里羅列一堆方法調(diào)用嗎?當(dāng)然不用,這些重復(fù)性的工作一定是可以在框架層面處理掉的,比如,你可以選擇使用我寫的 spring-boot-starter-vertx, 用了以后就可以像SpringMVC里@Controller注解一樣直接定義Handler了,starter會在啟動時(shí)自動掃描添加,像這樣:

@Component
// @BlockedHandler
@Slf4j
public class DemoHandler implements Handler<RoutingContext> {
    @Override
    public void handle(RoutingContext route) {
        log.info("invoke DemoHandler, path: {}", route.request().path());
        route.response().end("ok");
    }
}

如果Handler需要在worker線程池中調(diào)用,那么只需要添加一個(gè)@BlockedHandler注解即可。

是不是很方便?

如何處理數(shù)據(jù)庫查詢

數(shù)據(jù)庫查詢操作其實(shí)代表的是阻塞調(diào)用。在Vert.x Web應(yīng)用中,我們有兩種方式來處理block調(diào)用,一種就是像上面說的那樣,將含有block調(diào)用的代碼集中到一個(gè)Handler中然后注冊成blockingHandler; 另一種則是像node.js那樣使用回調(diào)。其中回調(diào)又可以分成兩類,一是使用 vert.x封裝好的異步JDBC,里面所有的block操作都會多一個(gè)注冊回調(diào)方法的參數(shù)。這種方式我個(gè)人覺得如果你是剛剛?cè)肟觱ert.x話那就很不推薦,因?yàn)樗鼤屇銓卣{(diào)產(chǎn)生恐懼。這里建議使用更傳統(tǒng)點(diǎn)的方法,即繼續(xù)使用你喜歡的持久化框架,只不過是在調(diào)用時(shí)將這部分代碼提交到worker線程池中執(zhí)行。下面我們用代碼說話。

假設(shè)我們需要做兩次數(shù)據(jù)庫查詢,只有拿到兩次查詢的結(jié)果才能執(zhí)行后面的邏輯,這種情況在業(yè)務(wù)系統(tǒng)中是非常常見的。用回調(diào)方式的代碼長這樣:

@Override
    public void handle(RoutingContext route) {
        log.info("invoke DemoHandler, path: {}", route.request().path());

        // (0)
        Future<String> fut1 = Future.future();
        Future<String> fut2 = Future.future();

        // 執(zhí)行block調(diào)用
        route.vertx() // (1)
                .executeBlocking(
                        fut -> {
                            String result = demoService.blockingLogic(1); // (2)
                            fut.complete(result); // (3)
                        },
                        fut1.completer()  // (4)
                );

        // 執(zhí)行block調(diào)用
        route.vertx()
                .executeBlocking(
                        fut -> {
                            String result = demoService.blockingLogic(2);
                            fut.complete(result);
                        },
                        fut2.completer()
                );

        // 組合結(jié)果
        CompositeFuture.all(fut1, fut2).setHandler(ar -> { // (5)
            if (!ar.succeeded()) { // (6)
                log.error("", ar.cause());
                route.response().end("error");
                return;
            }

            log.info("final step");

            List<String> resultList = ar.result().list(); // (7)
            route.response().end(resultList.toString());
        });
    }

(0): 構(gòu)造兩個(gè)Future對象,用來協(xié)調(diào)、同步兩個(gè)異步調(diào)用

(1): 從一下文中獲取vertx對象,調(diào)用executeBlocking()方法向worker線程池中提交任務(wù)

(2): 調(diào)用會發(fā)生阻塞的數(shù)據(jù)庫查詢方法

(3): 調(diào)用傳進(jìn)來的Future對象的complete()方法通知vert.x業(yè)務(wù)邏輯成功完成,并將執(zhí)行的結(jié)果對象傳遞進(jìn)來

(4): 將我們定義的Future對象標(biāo)記為成功或失敗。如果(2)或(3)拋出了異常,那么fut2會被標(biāo)記為失敗,反之成功

(5): 使用CompositeFuture工具類的all()方法將上面定義的兩個(gè)Feature組合起來,并注冊一個(gè)回調(diào)方法,此方法會在fut1, fut2全部成功或有一個(gè)失敗時(shí)觸發(fā)。

(6): 回調(diào)觸發(fā)后,要先判斷結(jié)果是成功還是失敗

(7): 取出兩個(gè)DB查詢的結(jié)果

可以看到,業(yè)務(wù)邏輯被各種回調(diào)分散到了不同的Handler中。這就是使用異步框架最主要的工程代價(jià)。

組合起來

我寫了一個(gè)Demo web項(xiàng)目,它基于上面提到的 spring-boot-starter-vertx項(xiàng)目,提供一個(gè)GET /user?username=xxx查詢接口,使用Mybatis做持久化層實(shí)現(xiàn)DB查詢。麻雀雖小五臟俱全!

<https://github.com/wanghongfei/all-about-vertx-4

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容