這一期文章主要為大家介紹如何將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查詢。麻雀雖小五臟俱全!