作者: 一字馬胡
轉載標志 【2017-11-26】
更新日志
日期 | 更新內容 | 備注 |
---|---|---|
2017-11-26 | 新建文章 | Spring 5 WebFlux demo |
Reactor
Spring 5的一大亮點是對響應式編程的支持,下面的圖片展示了傳統Spring Web MVC結構以及Spring 5中新增加的基于Reactive Streams的Spring WebFlux框架,可以使用webFlux模塊來構建異步的、非堵塞的、事件驅動的服務,在伸縮性方面表現非常好。
從上面的結構圖中可以看出,WebFlux模塊從上到下依次是Router Functions,WebFlux,Reactive Streams三個新組件,WebFlux模塊需要運行在實現了Servlet 3.1+規范的容器之上,Servlet 3.1規范中新增了對異步處理的支持,在新的Servlet規范中,Servlet線程不需要一直阻塞等待直到業務處理完成,也就是說,Servlet線程將不需要等待業務處理完成再進行結果輸出,然后再結束Servlet線程,而是在接到新的請求之后,Servlet線程可以將這個請求委托給另外一個線程(業務線程)來完成,Servlet線程將委托完成之后變返回到容器中去接收新的請求,Servlet 3.1 規范特別適用于那種業務處理非常耗時的場景之下,可以減少服務器資源的占用,并且提高并發處理速度,而對于那些能快速響應的場景收益并不大。下面介紹上圖中webFlux各個模塊:
- Router Functions: 對標@Controller,@RequestMapping等標準的Spring MVC注解,提供一套函數式風格的API,用于創建Router,Handler和Filter。
- WebFlux: 核心組件,協調上下游各個組件提供響應式編程支持。
- Reactive Streams: 一種支持背壓(Backpressure)的異步數據流處理標準,主流實現有RxJava和Reactor,Spring WebFlux默認集成的是Reactor。
上面提到WebFlux默認集成的Reactive Streams組件是Reactor,Reactor類似于RxJava 2.0,同屬于第四代響應式框架,下面主要介紹一下Reactor中的兩個關鍵概念,Flux以及Mono。
Flux
如果去查看源代碼的話,可以發現,Flux和Mono都實現了Reactor的Publisher接口,從這里可以看出,Flux和Mono屬于事件發布者,類似與生產者,對消費者提供訂閱接口,當有事件發生的時候,Flux或者Mono會通過回調消費者的相應的方法來通知消費者相應的事件,這也就是所謂的相應式編程模型,生產者和消費者減耦,它們之間通過實現一個共同的方法組來實現相互聯系(生產者通知事件是通過回調消費者的方法,而實現通知很多時候是通過代理)。
下面這張圖是Flux的工作流程圖:
可以從這張圖中很明顯的看出來Flux的工作模式,可以看出Flux可以emit很多item,并且這些item可以經過若干Operators然后才被subscrib,下面是使用Flux的一個小例子:
Flux.fromIterable(getSomeLongList())
.mergeWith(Flux.interval(100))
.doOnNext(serviceA::someObserver)
.map(d -> d * 2)
.take(3)
.onErrorResumeWith(errorHandler::fallback)
.doAfterTerminate(serviceM::incrementTerminate)
.subscribe(System.out::println);
Mono
下面的圖片展示了Mono的處理流程,可以很直觀的看出來Mono和Flux的區別:
Mono只能emit最多只能emit一個item,下面是使用Mono的一個小例子:
Mono.fromCallable(System::currentTimeMillis)
.flatMap(time -> Mono.first(serviceA.findRecent(time), serviceB.findRecent(time)))
.timeout(Duration.ofSeconds(3), errorHandler::fallback)
.doOnSuccess(r -> serviceM.incrementSuccess())
.subscribe(System.out::println);
WebFlux實戰
上文中簡單介紹了Reactor的兩個重要組件Flux和Mono,本文將介紹如何使用Spring 5的新組件WebFlux來進行應用開發,對于WebFlux底層的實現細節不在本文的分析范圍之內,當然本文也不會分析總結Spring 5的新特性,這些內容將在其他的文章中進行分析總結,下面將完整的描述一個使用WebFlux的步驟。
首先需要新建一個Spring項目,然后添加Spring 5的依賴,下面是添加的maven依賴:
<properties>
<spring.version>5.0.0.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
然后定義ViewModel類,下面是本文例子涉及的model類定義:
/**
* Created by hujian06 on 2017/11/23.
*
* the result model
*/
public class ResultModel {
private int id;
private String content;
public ResultModel() {
}
/**
* read property from json string
* @param id id
* @param content data
*/
public ResultModel(@JsonProperty("id") int id,
@JsonProperty("context") String content) {
this.id = id;
this.content = content;
}
}
public class ResultViewModel {
private int code;
private String message;
private ResultModel data;
}
上面的ResultViewModel類是最后將要返回的Vo類,包含了code、message以及data這三個標準返回內容,響應內容將以json格式返回。下面介紹Service的實現細節,可以從上面Vo類中的ResultModel中看出返回內容很簡單,就是id和Content,下面首先mock幾個數據:
//*************mock data**************//
private static List<ResultModel> resultModelList = new ArrayList<>();
static {
ResultModel model = new ResultModel();
model.setId(1);
model.setContent("This is first model");
resultModelList.add(model);
model = new ResultModel();
model.setId(2);
model.setContent("This is second model");
resultModelList.add(model);
}
在本例中要實現的接口包括查詢單個內容(根據id)、查詢所有內容、插入數據。下面分別介紹每一個接口的山西愛你細節,首先是根據id查詢單個內容的實現:
/**
* get the result by the pathVar {"id"}
* @param serverRequest the request
* @return the result model
*/
public Mono<ResultViewModel> extraResult(ServerRequest serverRequest) {
int id = Integer.parseInt(serverRequest.pathVariable("id"));
ResultModel model = null;
ResultViewModel resultViewModel;
for (ResultModel m : resultModelList) {
if (m.getId() == id) {
model = m;
break;
}
}
if (model != null) {
resultViewModel = new ResultViewModel(200, "ok", model);
} else {
resultViewModel = ResultViewModel.EMPTY_RESULT;
}
//return the result.
return Mono.just(resultViewModel);
}
需要注意的是,和傳統的MVC Controller不同,Reactive Controller操作的是非阻塞的ServerRequest和ServerResponse,而不再是Spring MVC里的HttpServletRequest和HttpServletResponse。上面的方法中最為關鍵的一點是最后的return語句,返回了一個Mono,并且這個Mono包含了查詢的結果。下面是查詢所有內容的方法細節:
/**
* return total result view
* @param serverRequest the request
* @return flux of total result model view
*/
public Flux<ResultViewModel> flowAllResult(ServerRequest serverRequest) {
List<ResultViewModel> result = new ArrayList<>();
for (ResultModel model : resultModelList) {
result.add(new ResultViewModel(200, "ok", model));
}
return Flux.fromIterable(result);
}
這個方法的實現就非常簡潔了,最后返回的內容是一個Flux,意味著這個方法會返回多個item,方法中使用了Flux的fromIterable靜態方法來構造Flux,還有很多其他的靜態方法來構造Flux,具體的內容可以參考源代碼。最后是插入一條內容的方法實現:
/**
* the "write" api
* @param serverRequest the request
* @return the write object
*/
public Mono<ResultViewModel> putItem(ServerRequest serverRequest) {
//get the object and put to list
Mono<ResultModel> model = serverRequest.bodyToMono(ResultModel.class);
final ResultModel[] data = new ResultModel[1];
model.doOnNext(new Consumer<ResultModel>() {
@Override
public void accept(ResultModel model) {
//check if we can put this data
boolean check = true;
for (ResultModel r : resultModelList) {
if (r.getId() == model.getId()) {
check= false;
break;
}
}
if (check) {
data[0] = model;
//put it!
resultModelList.add(model);
} else {
data[0] = null; //error
}
}
}).thenEmpty(Mono.empty());
ResultViewModel resultViewModel;
if (data[0] == null) { //error
resultViewModel = new ResultViewModel(200, "ok", data[0]);
} else { //success
resultViewModel = ResultViewModel.EMPTY_RESULT;
}
//return the result
return Mono.just(resultViewModel);
}
這個方法看起來優點費解,首先通過ServerRequest的body構造除了一個Mono(通過bodyToMono方法),然后通過調用這個Mono的doOnNext方法來進行具體的插入邏輯處理。這個時候就需要看Reactor的另外一個重要的角色Subscriber了,也就是所謂的訂閱者,或者消費者,下面是Subscriber提供的幾個方法:
/**
* Invoked after calling {@link Publisher#subscribe(Subscriber)}.
* <p>
* No data will start flowing until {@link Subscription#request(long)} is invoked.
* <p>
* It is the responsibility of this {@link Subscriber} instance to call {@link Subscription#request(long)} whenever more data is wanted.
* <p>
* The {@link Publisher} will send notifications only in response to {@link Subscription#request(long)}.
*
* @param s
* {@link Subscription} that allows requesting data via {@link Subscription#request(long)}
*/
public void onSubscribe(Subscription s);
/**
* Data notification sent by the {@link Publisher} in response to requests to {@link Subscription#request(long)}.
*
* @param t the element signaled
*/
public void onNext(T t);
/**
* Failed terminal state.
* <p>
* No further events will be sent even if {@link Subscription#request(long)} is invoked again.
*
* @param t the throwable signaled
*/
public void onError(Throwable t);
/**
* Successful terminal state.
* <p>
* No further events will be sent even if {@link Subscription#request(long)} is invoked again.
*/
public void onComplete();
結合所謂的響應式編程模型,publisher在做一件subscriber委托的事情的關鍵節點的時候需要通知subscribe,比如開始做、出錯、完成。關于響應式編程模型的具體分析總結,等完成了RxJava 2.0的相關分析總結之后再來補充。到此為止本例的Service已經編寫完成了,下面來編寫handler,handler其實是對Service的一層包裝,將返回類型包裝成ServerResponse,因為是包裝,所以只展示根據id查詢內容的接口的包裝細節:
/**
* get the result from service first, then trans the result to {@code ServerResponse}
* @param serverRequest the req
* @return the ServerResponse
*/
public Mono<ServerResponse> extraResult(ServerRequest serverRequest) {
//get the result from service
//todo : do some check here.
Mono<ResultViewModel> resultViewModelMono = resultService.extraResult(serverRequest);
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
//trans to ServerResponse and return.
//todo : too many code
return resultViewModelMono.flatMap(new Function<ResultViewModel, Mono<ServerResponse>>() {
@Override
public Mono<ServerResponse> apply(ResultViewModel resultViewModel) {
return ServerResponse
.ok()
.contentType(APPLICATION_JSON)
.body(fromObject(resultViewModel));
}
}).switchIfEmpty(notFound);
}
ServerResponse提供了豐富的靜態方法來支持將Reactor類型的結果轉換為ServerResponse,到目前為止,業務層面已經編寫完成,現在可以開始來進行router的編程了,router就和他的意義一樣就是用來路由的,將url路由給具體的handler來實現處理,WebFlux需要返回一個RouterFunction來進行設置路由信息,下面是本例子中使用到的RouterFunction細節:
/**
* build the router
* @return the router
*/
public RouterFunction<ServerResponse> buildResultRouter() {
return RouterFunctions
.route(RequestPredicates.GET("/s5/get/{id}")
.and(RequestPredicates
.accept(MediaType.APPLICATION_JSON_UTF8)), requestHandler::extraResult)
.andRoute(RequestPredicates.GET("/s5/list")
.and(RequestPredicates
.accept(MediaType.APPLICATION_JSON_UTF8)), requestHandler::listResult)
.andRoute(RequestPredicates.POST("/s5/put/")
.and(RequestPredicates
.accept(MediaType.APPLICATION_JSON_UTF8)), requestHandler::createView);
}
可以發現,其實就是將一個url和一個handler的具體方法綁定在一起來實現將一個url路由給一個handler方法進行處理,RequestPredicates提供了大量有用的靜態方法進行該部分的工作,具體的內容可以參考RequestPredicates的源碼以及在項目中多實踐積累。到目前為止,一個url請求可以路由到一個handler進行處理了,下面將使用Netty或者Tomcat來將這個例子運行起來,并且進行測試,文章開頭提到,WebFlux需要運行在實現了Servlet 3.1規范的容器中,而包括Tomcat、Jetty、Netty等都有實現,但是推薦使用Netty來運行WebFlux應用,因為Netty是非阻塞異步的,和WebFlux搭配效果更佳。所以下面的代碼展示了如何使用Netty來啟動例子:
public void nettyServer() {
RouterFunction<ServerResponse> router = buildResultRouter();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);
ReactorHttpHandlerAdapter httpHandlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
//create the netty server
HttpServer httpServer = HttpServer.create("localhost", 8600);
//start the netty http server
httpServer.newHandler(httpHandlerAdapter).block();
//block
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
如何想使用Tomcate來啟動例子,則可以參考下面的例子:
public void tomcatServer() {
RouterFunction<?> route = buildResultRouter();
HttpHandler httpHandler = toHttpHandler(route);
Tomcat tomcatServer = new Tomcat();
tomcatServer.setHostname("localhost");
tomcatServer.setPort(8600);
Context rootContext = tomcatServer.addContext("", System.getProperty("java.io.tmpdir"));
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet);
rootContext.addServletMapping("/", "httpHandlerServlet");
try {
tomcatServer.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
//block
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
運行項目之后,就可以測試是否成功了,下面是一個測試:
curl http://127.0.0.1:8600/s5/get/1
{
"code":200,
"message":"ok",
"data": {
"id":1,
"content":"This is first model"
}
}
curl http://127.0.0.1:8600/s5/list
[
{
"code":200,
"message":"ok",
"data": {
"id":1,
"content":"This is first model"
}
},
{
"code":200,
"message":"ok",
"data": {
"id":2,
"content":"This is second model"
}
}
]