Spring WebFlux
在建構Web服務上,Spring 5引入了全新的程式設計架構,這就是SpringWebFlux。作為一款新型的Web服務開發架構,它與傳統的WebMVC相比有哪些差別呢?我們又應該如何基于WebFlux元件來開發響應式Web服務呢?本篇我們就将對這些話題展開讨論。
對比WebMVC和WebFlux架構
和其他主流的Web開發架構一樣,在Spring WebMVC中,對Web請求的處理機制也基于管道-過濾器(Pipe-Filter)架構模式。Spring WebMVC使用了Servlet中的過濾器鍊(FilterChain)來對請求進行攔截,FilterChain的定義如代碼清單5-8所示。
代碼清單5-8 FilterChain接口定義代碼
public interface FilterChain {
public void doFilter (ServletRequest request, ServletResponse
response) throws IOException, ServletException;
}
我們知道WebMVC運作在Servlet容器上,常用的容器包括Tomcat、JBoss等。
當HTTP請求通過Servlet容器時就會被轉換為一個ServletRequest對象,而處理的結果将以Servlet-Response對象的形式傳回。當ServletRequest通過過濾器鍊中的一系列過濾器之後,最終就會到達作為前端控制器的DispatcherServlet。DispatcherServlet是WebMVC的核心元件,擴充了Servlet對象,并持有一組HandlerMapping和HandlerAdapter。
當ServletRequest請求到達時,DispatcherServlet負責搜尋HandlerMapping執行個體并使用合适的HandlerAdapter對其進行适配。其中,HandlerMapping的作用是根據目前請求找到對應的處理器Handler,它隻定義了一個方法,如代碼清單5-9所示。
代碼清單5-9 HandlerMapping接口定義代碼
public interface HandlerMapping {
//找到與請求對應的Handler,封裝為一個HandlerExecutionChain傳回
HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception;
}
而HandlerAdapter根據給定的HttpServletRequest和HttpServletResponse對象真正調用給定的Handler,核心方法如代碼清單5-10所示。
代碼清單5-10 HandlerAdapter接口定義代碼
public interface HandlerAdapter {
//針對給定的請求/響應對象調用目标Handler
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception;
}
在執行過程中,DispatcherServlet會在應用上下文中搜尋所有HandlerMapping。日常開發過程中,最常用到的HandlerMapping包含BeanNameUrlHandlerMapping和Request-MappingHandlerMapping,前者負責檢測所有Controller并根據請求URL的比對規則映射到具體的Controller執行個體上,而後者基于@RequestMapping注解來找到目标Controller。
如果我們使用了RequestMappingHandlerMapping,那麼對應的HandlerAdapter就是RequestMappingHandlerAdapter,它負責将傳入的ServletRequest綁定到添加了@Request-Mapping注解的控制器方法上,進而實作對請求的正确響應。同時,HandlerAdapter還提供請求驗證和響應轉換等輔助性功能,使得Spring WebMVC架構在日常Web開發中非常實用。
基于上述讨論,我們可以梳理Spring WebMVC的整體架構如圖5-4所示。
圖5-4 Spring WebMVC的整體架構
就整體架構而言,Spring WebFlux和Spring WebMVC本質上并沒有什麼差別。事實上,前面介紹的HandlerMapping、HandlerAdapter等元件在WebFlux都有同名的響應式版本,這是WebFlux的一種設計理念,即在既有設計的基礎上提供新的實作版本,隻對部分需要增強和弱化的地方做調整。
WebFlux同樣提供了一個過濾器鍊WebFilterChain,如代碼清單5-11所示。
代碼清單5-11 WebFilterChain接口定義代碼
public interface WebFilterChain {
Mono<Void> filter(ServerWebExchange exchange);
}
這裡的ServerWebExchange相當于是一個上下文容器,儲存了全新的ServerHttp-Request、ServerHttpResponse對象以及一些架構運作時狀态資訊。
在WebFlux中,和DispatcherServlet相對應的元件是DispatcherHandler。與Dispatcher-Servlet類似,DispatcherHandler同樣使用了一套響應式版本的HandlerMapping和Handler-Adapter完成對請求的處理。請注意,這兩個接口是定義在org.springframework.web.reactive包中,而不是在原有的org.springframework.web包中。
響應式版本的HandlerMapping接口定義如代碼清單5-12所示,可以看到這裡傳回的是一個Mono對象,進而啟用了響應式行為模式。
代碼清單5-12 響應式HandlerMapping接口定義代碼
public interface HandlerMapping {
Mono<Object> getHandler(ServerWebExchange exchange);
}
同樣,我們找到響應式版本的HandlerAdapter,如代碼清單5-13所示。
代碼清單5-13 響應式HandlerAdapter接口定義代碼
public interface HandlerAdapter {
Mono<HandlerResult> handle(ServerWebExchange exchange, Object
handler);
}
相比WebMVC中ModelAndView這種比較模糊的傳回結果,這裡的HandlerResult代表處理結果,更加直接和明确。
WebFlux同樣實作了響應式版本的RequestMappingHandlerMapping和RequestMapping-HandlerAdapter,是以我們仍然可以采用注解的方法來建構Controller。另外,WebFlux還提供了RouterFunctionMapping和HandlerFunctionAdapter組合,專門用來提供基于函數式程式設計的開發模式。
Spring WebFlux的整體架構如圖5-5所示。
圖5-5 Spring WebFlux整體架構
建立響應式Web API
介紹完整體架構,本節讨論如何基于Spring WebFlux建立響應式WebAPI。在Spring Boot中,建立響應式HTTP端點有兩種實作方法,一種是傳統的基于注解的程式設計模式,一種則是全新的函數式的程式設計模型。
基于注解的程式設計模式非常簡單,其使用的注解都來自WebMVC架構,我們已經在第4章中做了詳細介紹。以4.1.1節所展示的UserController為例,我們對其簡單改造就能實作對應的響應式版本,如代碼清單5-14所示。
代碼清單5-14 響應式UserController實作代碼
@RestController
@RequestMapping(value="users")
public class UserController {
@GetMapping(value = "/{id}")
public Mono<User> getUserById(@PathVariable Long id) {
User user = new User();
...
return Mono.just(user);
}
}
注意這裡唯一的差別就是getUserById()方法的傳回值從普通的User對象轉變為了一個Mono<User>對象。是以,對于通過注解來實作響應式Web服務而言,開發人員并沒有任何的學習成本。接下來,我們重點介紹函數式的程式設計模型。
在使用函數式程式設計模型建立響應式Web API時,我們需要引入一組全新的程式設計對象,即ServerRequest、ServerResponse、HandlerFunction和RouterFunction。
其中,ServerRequest和ServerRequest是一組,前者代表請求對象,可以通路各種HTTP請求元素,包括請求方法、URI和參數;而後者提供對HTTP響應的通路。将ServerRequest和ServerResponse組合在一起就可以建立HandlerFunction。最後,當通過HandlerFunction建立完成請求的處理邏輯後,需要把具體請求與這種處理邏輯關聯起來,RouterFunction可以幫助我們實作這一目标。
RouterFunction與傳統Spring WebMVC中的@RequestMapping注解功能類似。
同樣,我們通過一些簡單的代碼示例來示範這些程式設計對象的使用方法,我們建立如代碼清單5-15所示的一個HandlerFunction。
代碼清單5-15 UserHandler實作代碼
@Configuration
public class UserHandler {
@Autowired
private UserService userService;
public Mono<ServerResponse> getUserById(ServerRequest request) {
Long userId = request.pathVariable("userId");
return
ServerResponse.ok().body(this.orderService.getUserById(userId),
User.class);
}
}
在上述代碼示例中,我們建立了一個UserHandler類,然後注入UserService并實作了一個getUserById()處理函數。
注意到,我們通過ServerRequest的pathVariable()方法從HTTP請求URL路徑中擷取了userId參數,這是ServerRequest最基礎的用法。同時,ServerRequest還提供了一系列bodyToMono()和bodyToFlux()方法實作對請求消息體進行通路。
另外,我們在這裡也看到了ServerResponse的基礎用法。我們可以通過ServerResponse的ok()方法建立代表200狀态碼的響應,然後通過它的body()方法來建構一個Mono<ServerResponse>對象。
最後,我們根據已經建立的UserHandler來實作RouterFunction,示例代碼如代碼清單5-16所示。
代碼清單5-16 RouterFunction實作代碼
RouterFunction<ServerResponse> userRoute =
route(GET("/users/{id}").and(accept(APPLICATION_JSON)),
userHandler::getUserById);
上述代碼的作用就是暴露了一個/users/{id}端點,然後通過UserHandler的getUserById()來響應這個端點。
當然,針對User對象,我們還可以建立各種HTTP端點,然後通過RouterFunction的andRoute()方法進行組合,實作效果如代碼清單5-17所示。
代碼清單5-17 組合RouterFunction實作代碼
RouterFunction<ServerResponse> userRoute =
route(GET("/users/{id}") .and(accept(APPLICATION_JSON)), userHandler::getUserById)
.andRoute(GET("/users").and(accept(APPLICATION_JSON)),
userHandler::getUsers)
.andRoute(POST("/users").and(contentType(APPLICATION_JSON)),
userHandler::createUser);
消費響應式Web API
我們已經在4.1.2節中介紹了用于實作遠端通路的RestTemplate模闆工具類。Rest-Template的主要問題在于不支援響應式流規範,也就無法提供非阻塞式的流式操作。在Spring WebFlux中,也專門存在一個執行響應式遠端通路的WebClient工具類,可以認為它就是RestTemplate的響應式更新版本。
和使用RestTemplate一樣,建立WebClient的過程也比較簡單,我們可以直接使用create()工廠方法來建立WebClient的執行個體,示例代碼如代碼清單5-18所示。
代碼清單5-18 建立WebClient代碼
WebClient webClient = WebClient.create();
同時,WebClient也提供了一組非常實用的工具方法來通路遠端響應式Web服務,日常開發過程中比較常用的包括retrieve()方法和exchange()方法。
retrieve()方法是擷取響應主體并對其進行解碼的最簡單方法,我們來看一個示例,如代碼清單5-19所示。
代碼清單5-19 使用retrieve()方法示例代碼
WebClient webClient = WebClient.create("http://localhost:8080");
Mono<User> user = webClient.get()
.uri("/users/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(User.class);
上述代碼使用JSON作為序列化方式,從遠端HTTP端點中擷取了一個Mono<User>對象。
相比retrieve()方法,exchange()方法對響應結果擁有更多的控制權,該響應結果是一個ClientResponse對象,包含了響應的狀态碼、Cookie等資訊,示例代碼如代碼清單5-20所示。
代碼清單5-20 使用exchange()方法示例代碼
Mono<User> result = webClient.get()
.uri("/users/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(User.class));
以上代碼示範了如何對響應結果執行flatMap操作符,通過這一操作符調用ClientResponse的bodyToMono()方法以擷取目标User對象。
Spring WebFlux案例分析
像Reactor這樣的響應式庫可以幫助我們建構一個異步的非阻塞流,并且為開發人員屏蔽掉底層的技術複雜度。
而基于Reactor架構的WebFlux進一步降低了開發響應式Web服務的難度。微服務架構的興起為WebFlux提供了很好的應用場景。我們知道在一個微服務系統中,存在數十乃至數百個獨立的微服務,它們互相通信以完成複雜的業務流程。這個過程勢必涉及大量的I/O操作。I/O操作,尤其是阻塞式I/O操作會整體增加系統的延遲并降低吞吐量。如果能夠在複雜的流程中內建非阻塞、異步通信機制,我們就可以高效處理跨服務之間的網絡請求。對于這種場景,WebFlux是一種非常有效的解決方案。
在本節中,我們将基于分布式環境下的遠端服務調用過程,來給出使用Spring WebFlux的具體案例。該案例模拟電商系統中訂單(Order)下單過程。
我們知道想要完成下單操作,至少需要明确目前訂單的使用者(User)資訊以及訂單中所包含的商品(Good)資訊。為了示範Web環境下的遠端調用過程,也考慮到案例系統的簡單性,我們将設計并實作兩個獨立的Web服務,一個是代表使用者資訊的user-service,一個是代表訂單的order-service。
顯然,order-service應該調用user-service中暴露的HTTP端點來完成下單操作,而訂單所需的商品資訊我們将采用模拟的方式生成資料。
1. 實作user-service
我們先來看user-service的實作過程。作為一個響應式Web服務,我們首先需要明确它所依賴的開發包。在Spring Boot應用程式中,我們需要引入代表WebFlux的spring-boot-starter-webflux依賴。同時,為了示範響應式資料通路過程,本案例将選擇使用MongoDB這款提供了Spring Data ReactiveRepository的NoSQL資料庫。
為此,我們需要同時引入傳統的spring-bootstarter-data-mongodb依賴以及代表響應式MongoDB的spring-boot-starterdata-mongodb-reactive依賴,如代碼清單5-21所示。
代碼清單5-21 案例中的相關依賴包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb
reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
在user-service中,代表領域實體的User類定義如代碼清單5-22所示,這裡使用的就是MongoDB所提供的各種注解,我們已經在前面使用過這些注解。
代碼清單5-22 User對象定義代碼
@Document("users")
public class User {
@Id
private String id;
@Field("userCode")
private String userCode;
@Field("userName")
private String userName;
}
有了實體類,接下來我們定義資料通路層元件UserRepository。請注意,這個User-Repository需要擴充響應式MongoDB所提供的ReactiveMongoRepository接口,如代碼清單5-23所示。
代碼清單5-23 UserRepository定義代碼
public interface UserRepository extends ReactiveMongoRepository<User,
String> {
Mono<User> findUserByUserName(String userName);
}
請注意,現在通過UserRepository擷取的使用者對象的類型都是Mono<User>或Flux<User>,而不是普通的User。ReactiveMongoRepository通過底層的響應式資料驅動程式確定所有的響應式對象能夠得到正确儲存和查詢。
在UserRepository的基礎上,實作Service層元件就比較簡單,隻是對UserRepository所提供方法進行簡單調用即可,如代碼清單5-24所示。
代碼清單5-24 UserService實作代碼
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Mono<User> getUserById(String userId) {
return userRepository.findById(userId);
}
public Mono<User> getUserByUserName(String userName) {
return userRepository.findUserByUserName(userName);
}
public void addUser(Mono<User> user){
user.flatMap(userRepository::save);
}
public void updateUser(Mono<User> user){
user.flatMap(userRepository::save);
} public void deleteUser(Mono<User> user){
user.flatMap(userRepository::delete);
}
}
同樣需要注意的是,這裡所有方法操作的業務對象類型也都是Mono<User>。而在addUser()、updateUser()和deleteUser()方法中,我們通過Reactor架構所提供的flatMap操作完成對UserRepository中對應方法的調用,這也是flatMap操作符非常典型的一種使用方式。
作為對外暴露HTTP端點的Controller層元件,UserController也并不複雜,這裡給出該類的實作代碼,如代碼清單5-25所示。
代碼清單5-25 UserController實作代碼
@RestController
@RequestMapping(value="users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value="/{userId}")
public Mono<User> getUserById(@PathVariable("userId") String
userId) {
return userService.getUserById(userId);
}
@GetMapping(value="/userName/{userName}")
public Mono<User> getUserByUserName(@PathVariable("userName")
String userName) {
return userService.getUserByUserName(userName);
}
@PostMapping(value = "/")
public void addUser(@RequestBody Mono<User> user) { userService.addUser(user);
}
@RequestMapping(value = "/", method = RequestMethod.PUT)
public void updateUser(@RequestBody Mono<User> user) {
userService.updateUser(user);
}
@RequestMapping(value = "/{userId}", method =
RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable("userId") String userId) {
User user = new User();
user.setId(userId);
userService.deleteUser(Mono.just(user));
}
}
當完成對user-service中所有元件的建構之後,我們也可以執行一些初始化操作,往MongoDB中插入部分User對象供後續進行使用。
2. 實作order-service
在整個案例中,order-service将調用user-service暴露的HTTP端點。是以我們将在order-service中建立一個UserWebClient類,專門用來通路遠端服務,如代碼清單5-26所示。
代碼清單5-26 UserWebClient實作代碼
@Service
public class UserWebClient {
public Mono<UserMapper> getUserById(String userId) {
Mono<UserMapper> userFromRemote = WebClient.create()
.get()
.uri("http://127.0.0.1:8082/users/{userId}", userId)
.retrieve() .bodyToMono(UserMapper.class);
return userFromRemote;
}
public Mono<UserMapper> getUserByUserName(String userName) {
Mono<UserMapper> userFromRemote = WebClient.create()
.get()
.uri("http://127.0.0.1:8082/users/userName/{userName}",userName)
.retrieve()
.bodyToMono(UserMapper.class);
return userFromRemote;
}
}
可以看到,這裡通過WebClient工具類完成了對user-service中HTTP端點的遠端通路。我們綜合使用了retrieve()和bodyToMono()等方法來實作這一目标。
在order-service中使用這個UserWebClient的方式如代碼清單5-27所示。
代碼清單5-27 OrderService實作代碼
@Service
public class OrderService {
@Autowired
UserWebClient userWebClient;
@Autowired
OrderRepository orderRepository;
public Mono<UserMapper> getUser(String userId) {
return userWebClient.getUserById(userId);
}
public Mono<Order> generateOrder(String userId, String goodName) { Order order = new Order();
//擷取遠端User資訊
Mono<UserMapper> user = getUser(userId);
//驗證目标使用者是否存在
if(user == null ) {
return Mono.just(order);
}
//生成有效Order
Mono<Order> monoOrder = user.flatMap(u -> {
order.setId(UUID.randomUUID().toString());
order.setUserId(userId);
order.setOrderNumber("OrderNumber");
order.setDeliveryAddress("DemoDeliveryAddress");
order.setGoodsName(goodName);
order.setCreateTime(new Date());
return Mono.just(order);
});
//儲存Order
return monoOrder.flatMap(orderRepository::save);
}
}
在上述generateOrder()方法中,我們對關鍵步驟代碼添加了注釋。
我們首先根據userId從user-service中擷取遠端User對象并進行校驗,如果該對象為空則直接傳回。反之,我們建構一個有效的Order對象并進行持久化。整個流程雖然很簡單,但已經充分展示了兩個服務之間進行響應式遠端互動的實作過程。
注意這裡再次使用了flatMap操作符完成對Order對象的處理。
本文給大家講解的内容是springweb服務應用響應式Web開發元件:Spring WebFlux
- 下文給大家講解的是springweb服務應用響應式Web開發元件:Spring RSocket