這一期文章主要爲大家介紹如何將Vert.x與SpringBoot結合起來編寫最最最常見的業務系統,即數據庫增刪改查。
談兩句SpringBoot
SpringBoot大家都很熟了,一個快速開發框架,其最大的特點是可將Spring應用打成可執行jar包,從而不再依賴外部容器,如Tomcat。可能絕大多數人在使用SpringBoot時一定離不了嵌入式Tomcat, 從而造成了一想到SpringBoot就會將其與SpringMVC聯繫在一起的現象。其實我們可以只使用SpringBoot的一鍵執行和提供Spring環境的特性,Web層直接替換成Vert.x. 此外,一些短時執行的任務也可以用這種方式來寫,簡直不能更爽。
Vertx-web的請求路由
上一篇寫的Http Server是沒有路由的,所有的請求都會由同一個Handler處理。如果是隻提供一個簡單的API服務這樣是沒有問題的,但在實際業務系統中一般接口數量都比較多,這時候就需要一個路由組件來將不同的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): 構造一個Router。
(1): 添加對/a/b/c/path
的路由。這裏也可以使用重載的帶有HTTP Method的方法。
(2): 當收到path爲/a/b/c/path
的請求時,先調用BodyHandler
。這裏BodyHandler
是Vert.x提供的處理器,只有在請求處理鏈路的開頭添加了此Handler我們才能在後續的Handler中拿到請求體。
(3): 添加我們的業務處理器, 此處理器會在NIO線程中執行。
(4): 添加含有阻塞調用的業務處理器,此處理器會在worker線程池中執行,不會block NIO線程。
(5): 將Router的Handler設爲整個HTTP Server的Handler。
其中Handler是一個函數式接口,定義如下:
@FunctionalInterface
public interface Handler<E> {
/**
* Something has happened, so handle it.
*
* @param event the event to handle
*/
void handle(E event);
}
可見,vertx添加請求處理器的過程非常簡單,而且寫起來也很爽。
不過這裏還是會有一個問題,我們的服務會有大量的Handler, 大量的路由,這時候就只能在這裏羅列一堆方法調用嗎?當然不用,這些重複性的工作一定是可以在框架層面處理掉的,比如,你可以選擇使用我寫的 spring-boot-starter-vertx, 用了以後就可以像SpringMVC裏@Controller
註解一樣直接定義Handler了,starter會在啓動時自動掃描添加,像這樣:
@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線程池中調用,那麼只需要添加一個@BlockedHandler
註解即可。
是不是很方便?
如何處理數據庫查詢
數據庫查詢操作其實代表的是阻塞調用。在Vert.x Web應用中,我們有兩種方式來處理block調用,一種就是像上面說的那樣,將含有block調用的代碼集中到一個Handler中然後註冊成blockingHandler; 另一種則是像node.js那樣使用回調。其中回調又可以分成兩類,一是使用 vert.x封裝好的異步JDBC,裏面所有的block操作都會多一個註冊回調方法的參數。這種方式我個人覺得如果你是剛剛入坑vert.x話那就很不推薦,因爲它會讓你對回調產生恐懼。這裏建議使用更傳統點的方法,即繼續使用你喜歡的持久化框架,只不過是在調用時將這部分代碼提交到worker線程池中執行。下面我們用代碼說話。
假設我們需要做兩次數據庫查詢,只有拿到兩次查詢的結果才能執行後面的邏輯,這種情況在業務系統中是非常常見的。用回調方式的代碼長這樣:
@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();
// 執行block調用
route.vertx() // (1)
.executeBlocking(
fut -> {
String result = demoService.blockingLogic(1); // (2)
fut.complete(result); // (3)
},
fut1.completer() // (4)
);
// 執行block調用
route.vertx()
.executeBlocking(
fut -> {
String result = demoService.blockingLogic(2);
fut.complete(result);
},
fut2.completer()
);
// 組合結果
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): 構造兩個Future對象,用來協調、同步兩個異步調用
(1): 從一下文中獲取vertx對象,調用executeBlocking()
方法向worker線程池中提交任務
(2): 調用會發生阻塞的數據庫查詢方法
(3): 調用傳進來的Future對象的complete()
方法通知vert.x業務邏輯成功完成,並將執行的結果對象傳遞進來
(4): 將我們定義的Future對象標記爲成功或失敗。如果(2)或(3)拋出了異常,那麼fut2會被標記爲失敗,反之成功
(5): 使用CompositeFuture
工具類的all()
方法將上面定義的兩個Feature組合起來,並註冊一個回調方法,此方法會在fut1, fut2全部成功或有一個失敗時觸發。
(6): 回調觸發後,要先判斷結果是成功還是失敗
(7): 取出兩個DB查詢的結果
可以看到,業務邏輯被各種回調分散到了不同的Handler中。這就是使用異步框架最主要的工程代價。
組合起來
我寫了一個Demo web項目,它基於上面提到的 spring-boot-starter-vertx項目,提供一個GET /user?username=xxx
查詢接口,使用Mybatis做持久化層實現DB查詢。麻雀雖小五臟俱全!