目錄
在互聯網的應用中,存在電商和金融等企業,這些企業對於業務的嚴謹性要求特別高,因爲他們的業務關係到用戶和商家的賬戶以及財產的安全,多以它們對於數據的一致性十分重視,在存在併發的時候,就需要通過鎖或者其他機制保證一些重要數據的一致性,但這也會造成性能的下降。對於另外一些互聯網應用則不一樣,如遊戲、視頻、新聞和廣告的網站,它們一般不會涉及賬戶和財產的問題,也就是不需要很高的數據一致性,但是對於併發數和響應速度卻十分在意,而使用傳統的開發模式會引入一致性機制,造成性能下降,爲此一些軟件設計者提出了響應式編程的理念。
爲了適應響應式編程的潮流,Spring 5發佈了新一代響應式的Web框架——Spring WebFlux,不過在討論WebFlux之前,需要先了解一下RxJava和Reactor,而Reactor是Spring WebFlux的默認實現,所以我們主要討論一下Reactor。
1.基礎概念
響應式編程是一種面向數據流和變化傳播的編程範式。對於響應式框架,是基於響應式宣言的理念所產生的編程方式。響應式宣言分爲4大理念:
- 靈敏的:可以快速響應的,只要有任何可能,系統都應該能夠儘可能快地做出響應。
- 可恢復的:系統在運行中可能出現問題,但是能夠有很強大的容錯機制和修復機制保持響應性。
- 可伸縮的:在任何負載下,響應式編程都可以根據自身壓力變化,請求少時,通過減少資源釋放服務器壓力,負載大時能夠通過擴展算法和軟硬件的方式擴展服務壓力,以經濟實惠的方式實現可伸縮性。
- 消息驅動的:響應式編程存在異步消息機制,事件之間的協作是通過消息進行連接的。
基於這些理念,響應式編程提出了各種模型來滿足響應式編程的理念,其中著名的有Reactor和RxJava,Spring5就是基於它們構建WebFlux,而默認情況下它會使用Reactor。
1.1 Reactor模型
傳統的編程模型中,當請求量大於系統能夠承受的最大線程數時,大量的線程就只能夠在隊列中等待或者被系統殺死,無法及時響應用戶。爲了克服這個問題,提出了Reactor(反應器)模式,其模型圖如下圖所示:
首先客戶端會先向服務器註冊其感興趣的事件(Event),這樣客戶端就訂閱了對應的事件,只是訂閱事件並不會給服務器發送請求。當客戶端發生一些已經註冊的事件時,就會觸發服務器的響應。當觸發服務器響應時,服務器存在一個Selector線程,這個線程只是負責輪詢客戶端發送過來的事件,並不處理請求,當它接收到有客戶端事件時,就會轉到對應的請求處理器,然後啓用另外一個線程運行處理器。因爲Selector線程只是輪詢,並不處理複雜的業務邏輯,所以它可以在輪詢後對請求做到實時響應,速度十分快。由於事件存在很多種,所以請求處理器也存在多個,因此還需要進行區分事件的類型,所以Selector存在一個路由問題。當請求處理器處理業務時,結果最終也會轉換爲數據流發送到客戶端。
從上面可以看出,Reactor是一種基於事件的模型,對於服務器線程而言,它也是一種異步的,首先是Selector線程輪詢到事件,然後通過路由找到處理器去運行對應的邏輯,處理器最後所返回的結果會轉換爲數據流。
1.2 Spring WebFlux概述
Spring WebFlux是Spring 5推出的新一代Web響應式編程框架,它十分適合那些需要高併發和大量請求的互聯網應用,特別是那些需要高速響應而對業務邏輯要求並不十分嚴格的網站,如遊戲、視頻和新聞瀏覽網站等。
下圖是Spring MVC和Spring WebFlux的對比:
WebFlux包含一個路由分發層,也就是根據請求的事件,決定採用什麼類的什麼方法處理客戶端發送過來的事件請求,類似於Reactor中的Selector,業務邏輯處理完成後,再將結果轉換爲數據流。
WebFlux需要能夠支持Servlet3.1+的容器,如Tomcat、Jetty和Undertow等,而在Java異步編程的領域,使用得最多的是Netty,所以WebFlux的starter中默認是依賴於Netty庫的。
1.3 WebHandler接口和運行流程
與Spring MVC使用DispatcherServlet不同的是Spring WebFlux使用的是WebHandler。它與DispatcherServlet十分類似。只是WebHandler是一個接口,爲此Spring WebFlux爲其提供了幾個實現類,以便於在不同的場景下使用。這幾個實現類中DispatcherHandler是我們關注的核心,它與DispatcherServlet是十分接近的 ,我們首先看一下DispatcherHandler的核心代碼handle方法:
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
從源碼中我們可以看到,與Spring MVC一樣,都是從HandlerMapping中找到對應的處理器,找到處理器之後就會通過invokeHandler方法運行處理器,最後得到了處理結果的handleResult方法,通過它將結果轉變爲對應的數據流序列。下圖是它大概的流程圖:
下面我們以MongoDB作爲響應式編程的數據源,這裏之所以使用MongoDB,是因爲Spring WebFlux只能支持Spring Data Reactive,它是一種非阻塞的響應數據方式。遺憾的是,因爲數據庫的開發往往是阻塞的,所以Spring Data Reactive並不能對數據庫的開發給予有效支持。但Spring Data Reactive可以支持Redis、MongoDB等NoSQL的開發,而Redis功能受限,更加適合作爲緩存使用,所以我們選擇使用MongoDB作爲Spring WebFlux的實例,這也是最廣泛的使用方式。
2.開發WebFlux服務端
在Spring WebFlux中,存在兩種開發方式,一種是類似於Spring MVC的模式,另一種則是函數功能性編程,無論哪種都是允許的。我們這裏使用類似於Spring MVC的模式,其中@Controller、@ResponseMapping、@GetMapping、@PostMapping等Spring MVC註解依然有效,這就爲構建WebFlux的應用帶來了便利。
2.1 依賴導入
首先導入相關依賴:
<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-tomcat</artifactId>
</dependency>
<!--MongoDb jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
我們以MongoDB作爲響應式編程的數據源,所以引入了JPA和MongoDB的starter。與此同時將WebFlux的starter包引入進來了,而它會依賴於Spring Web的包,這裏還引入了Tomcat作爲默認的服務器。
2.2 開發持久層
這裏採用MongoDB作爲開發的數據源,先定義POJO:
package com.martin.flux.pojo;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.io.Serializable;
/**
* @author: martin
* @date: 2019/11/3 10:44
* @description:
*/
@Document(collection = "users")
@Data
public class UserDO implements Serializable {
@Id
private Long id;
@Field("user_name")
private String userName;
private String note;
/**
* 定義了性別的枚舉,可以通過typeHandler轉換
*/
private String sex;
}
這裏採用JPA作爲持久層,而Spring WebFlux爲響應式提供了接口ReactiveMongoRepository,這樣就可以通過繼承它聲明瞭一個JPA接口:
package com.martin.flux.dao;
import com.martin.flux.pojo.UserDO;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
/**
* @author: martin
* @date: 2020/2/7
*/
@Repository
public interface UserDao extends ReactiveMongoRepository<UserDO, Long> {
Flux<UserDO> findByUserNameLikeAndNoteLike(String userName, String note);
}
findByUserNameLikeAndNoteLike方法是一個按照JPA規則命名的方法,它的作用就是使用用戶名和備註進行模糊查詢。
2.3 開發服務層
先定義用戶服務接口UserService:
package com.martin.flux.service;
import com.martin.flux.pojo.UserDO;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: martin
* @date: 2020/2/7
*/
public interface UserService {
Mono<UserDO> getUser(Long id);
Mono<UserDO> insertUser(UserDO userDO);
Mono<UserDO> updateUser(UserDO userDO);
Mono<Void> deleteUser(Long id);
Flux<UserDO> findUsers(String userName, String note);
}
然後實現這個接口:
package com.martin.flux.service;
import com.martin.flux.dao.UserDao;
import com.martin.flux.pojo.UserDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: martin
* @date: 2020/2/7
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public Mono<UserDO> getUser(Long id) {
return userDao.findById(id);
}
@Override
public Mono<UserDO> insertUser(UserDO userDO) {
return userDao.save(userDO);
}
@Override
public Mono<UserDO> updateUser(UserDO userDO) {
return userDao.save(userDO);
}
@Override
public Mono<Void> deleteUser(Long id) {
return userDao.deleteById(id);
}
@Override
public Flux<UserDO> findUsers(String userName, String note) {
return userDao.findByUserNameLikeAndNoteLike(userName, note);
}
}
2.4 開發控制層
對於WebFlux而言,使用Rest風格更加合適,這裏使用的也是Rest風格的控制器。首先我們定義前端視圖模型UserVO:
package com.martin.flux.pojo;
import lombok.Data;
/**
* @author: martin
* @date: 2020/2/7
*/
@Data
public class UserVO {
private Long id;
private String userName;
private int sexCode;
private String sexName;
private String note;
}
接下來定義用戶控制器;
package com.martin.flux.controller;
import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import com.martin.flux.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: martin
* @date: 2020/2/7
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
//獲取用戶
@GetMapping("/user/{id}")
public Mono<UserVO> getUser(@PathVariable Long id) {
return userService.getUser(id).map(u -> translate(u));
}
//新增用戶
@PostMapping("/user")
public Mono<UserVO> insertUser(@RequestBody UserDO userDO) {
return userService.insertUser(userDO).map(u -> translate(userDO));
}
//更新用戶
@PutMapping("/user")
public Mono<UserVO> updateUser(@RequestBody UserDO userDO) {
return userService.updateUser(userDO).map(u -> translate(userDO));
}
//刪除用戶
@PutMapping("/user/{id}")
public Mono<Void> updateUser(@PathVariable Long id) {
return userService.deleteUser(id);
}
//查詢用戶
@GetMapping("/user/{userName}/{note}")
public Flux<UserVO> findUsers(@PathVariable String userName, @PathVariable String note) {
return userService.findUsers(userName, note).map(u -> translate(u));
}
private UserVO translate(UserDO userDO) {
UserVO userVO = new UserVO();
userVO.setId(userDO.getId());
userVO.setUserName(userDO.getUserName());
userVO.setSexCode(Integer.valueOf(userDO.getSex()));
userVO.setNote(userDO.getNote());
return userVO;
}
}
這裏的@RestController代表採用Rest風格的控制器,這樣Spring就知道將返回的內容轉換爲JSON數據序列。但是應注意的是,這裏的方法返回的或者是Flux<UserVO>或者是Mono<User>,Mono是一個0~1個數據流序列,而Flux是一個0~N個數據流序列。
2.5 配置服務
Spring Boot配置文件如下:
spring.data.mongodb.host=localhost
spring.data.mongodb.username=dbAdmin
spring.data.mongodb.password=password
spring.data.mongodb.port=27017
spring.data.mongodb.database=test-it
啓動文件配置如下:
package com.martin.flux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
//定義掃描包
@SpringBootApplication(scanBasePackages = "com.martin.flux")
//因爲引入了JPA,所以默認情況下,需要配置數據源
//排除原有自動配置的數據源
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
//在WebFlux下,驅動MongoDB的JPA接口
@EnableReactiveMongoRepositories(basePackages = "com.martin.flux.dao")
public class FluxApplication {
public static void main(String[] args) {
SpringApplication.run(FluxApplication.class, args);
}
}
因爲引入了JPA,所以在默認的情況下Spring Boot會嘗試裝配關係數據庫數據源(DataSource),而這裏使用的是MongoDB並沒有使用關係數據庫,所以使用@EnableAutoConfiguration去排除數據源的初始化,否則就會得到錯誤的啓動日誌。在WebFlux中使用響應式的MongoDB的JPA接口,需要使用註解@EnableReactiveMongoRepositories進行驅動,還定義了掃描的包,這樣就可以將代碼掃描到IOC容器中了。
MongoDB數據庫中導入如下數據:
我們啓動應用,並訪問http://localhost:8080/user/1,頁面會有如下返回結果:
顯然運行成功了,這樣就完成了簡單的WebFlux開發。
2.6 客戶端開發——WebClient
爲了方便測試,Spring WebFlux爲我們提供了WebClient類,它是一個比RestTemplate更爲強大的類,通過它就可以請求後端的服務。實例代碼如下:
package com.martin.flux;
import com.martin.flux.pojo.SexEnum;
import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* @author: martin
* @date: 2020/2/9
*/
@Slf4j
public class FluxTest {
public static void main(String[] args) {
WebClient client = WebClient.create("http://localhost:8080");
//新建一個用戶
UserDO newUser = new UserDO();
newUser.setId(2L);
newUser.setUserName("mars");
newUser.setSex(String.valueOf(SexEnum.FEMALE.getCode()));
//新增用戶
insertUser(client, newUser);
getUser(client, 2L);
//更新一個用戶
UserDO updateUser = new UserDO();
updateUser.setId(1L);
updateUser.setUserName("mars");
updateUser.setSex(String.valueOf(SexEnum.FEMALE.getCode()));
updateUser.setNote("mars老師");
updateUser(client, updateUser);
//查詢用戶
findUsers(client, "mars", "mars老師");
//刪除用戶
deleteUser(client, 2L);
}
private static void insertUser(WebClient client, UserDO newUser) {
Mono<UserVO> userVOMono = client.post()
.uri("/user")
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(Mono.just(newUser), UserDO.class)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("insert user success:{}", userVO.getUserName());
}
private static void getUser(WebClient client, long id) {
Mono<UserVO> userVOMono = client.get()
.uri("/user/{id}", id)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("get user success:{}", userVO.getUserName());
}
private static void updateUser(WebClient client, UserDO userDO) {
Mono<UserVO> userVOMono = client.put()
.uri("/user")
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(Mono.just(userDO), UserDO.class)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("update user success:{}", userVO.getUserName());
}
private static void findUsers(WebClient client, String userName, String note) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("userName", userName);
paramMap.put("note", note);
Mono<UserVO> userVOMono = client.get()
.uri("/user/{userName}/{note}", paramMap)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("find user success:{}", userVO.getUserName());
}
private static void deleteUser(WebClient client, long id) {
Mono<Void> voidMono = client.delete()
.uri("/user/{id}", id)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(Void.class);
Void voidResult = voidMono.block();
log.error("delete user success:{}", voidMono);
}
}
這裏採用了WebClient的靜態方法create來創建WebClient對象,該方法的參數是網站的基礎URI,這樣就能夠定位基礎的URI。Mono<UserVO>對象的初始化代碼,只是給後端註冊一個事件而已,並不會發送請求。使用Mono的block方法則是發送觸發事件,這樣服務器纔會響應事件,將數據流傳送到客戶端中。
3.深入WebFlux服務端開發
正如上面的例子,服務端的開發十分接近Spring MVC,在大部分情況下可以參考Spring MVC的內容。但Web Flux也有一些特殊的處理,比如新增加了參數轉換和驗證規則等,此外還有一些錯誤的處理。這些都是在實際開發中最常見到的內容,需要進一步地學習它們。
3.1 類型轉換器——Converter
例如,現在來實現一個類型轉換,約定用戶將以字符串格式{id}-{userName}-{sex}-{note}進行傳遞,然後通過類型轉換器(Converter)得到用戶數據。實現代碼如下:
package com.martin.flux.config;
import com.martin.common.constant.MarkCodeEnum;
import com.martin.flux.pojo.UserDO;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
* @author: martin
* @date: 2020/2/9
*/
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {
//註冊Converter
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(string2UserConverter());
}
/**
* 定義String轉換成UserDO類型轉換器
* 如果加上@Bean註解,Spring Boot會自動識別爲類型轉換器
*
* @return
*/
public Converter<?, ?> string2UserConverter() {
Converter<String, UserDO> converter = (s -> {
String[] args = s.split(MarkCodeEnum.SEGMENT.getCode());
UserDO userDO = new UserDO();
Long id = Long.valueOf(args[0]);
userDO.setId(id);
userDO.setUserName(args[1]);
userDO.setSex(args[2]);
userDO.setNote(args[3]);
return userDO;
});
return converter;
}
}
上面的代碼實現了WebFluxConfigurer 接口,並覆蓋了addFormatters方法。該方法是加載轉換器和格式化器的,這裏使用了方法string2UserConverter來定義了一個Converter,這樣就能夠將字符串按照約定的格式轉換爲用戶類。我們也可以繼承WebFluxConfigurer接口,將string2UserConverter方法添加@Bean註解,這樣Spring Boot會自動識別這個Bean爲轉換器,就不需要實現addFormatters方法手動註冊了。
爲了測試轉換器,在UserController中添加如下方法:
//新增用戶
@PostMapping("/user2/{user}")
public Mono<UserVO> insertUser2(@PathVariable("user") UserDO userDO) {
return userService.insertUser(userDO).map(u -> translate(userDO));
}
WebClient測試代碼如下:
package com.martin.flux;
import com.martin.flux.pojo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
/**
* @author: martin
* @date: 2020/2/9
*/
@Slf4j
public class ConvertTest {
public static void main(String[] args) {
WebClient client = WebClient.create("http://localhost:8080");
Mono<UserVO> userVOMono = client.post()
.uri("/user2/{user}", "3-convert-1-轉換器測試")
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("user name is:{}", userVO.getUserName());
}
}
我們傳入和服務端約定格式,就能夠將用戶信息存入到MongDB中了。關於日期格式化器,WebFlux允許我們通過application.properties進行配置,例如:
spring.webflux.date-format=yyyy-MM-dd
3.2 驗證器——Validator
有時候需要對參數進行驗證,比如我們需要驗證一下用戶名稱是否爲空。這個時候可以使用Spring MVC的Validator機制,首先新建用戶驗證器UserValidator:
package com.martin.flux.config;
import com.martin.flux.pojo.UserDO;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
/**
* @author: martin
* @date: 2020/2/9
*/
public class UserValidator implements Validator {
//確定支持的驗證類型
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(UserDO.class);
}
//驗證邏輯
@Override
public void validate(Object target, Errors errors) {
UserDO userDO = (UserDO) target;
if (StringUtils.isEmpty(userDO.getUserName())) {
errors.rejectValue("userName", null, "用戶名不能爲空");
}
}
}
在WebFluxConfig中覆蓋WebFluxConfigurer接口的getValidator方法,代碼如下;
@Override
public Validator getValidator() {
return new UserValidator();
}
上面只是定義了驗證器,但並沒有啓用,爲了測試該驗證器,需要在Controller對應的方法中加入@Valid註解,實例代碼如下;
//新增用戶
@PostMapping("/user3/")
public Mono<UserVO> insertUser3(@Valid @RequestBody UserDO userDO) {
return userService.insertUser(userDO).map(u -> translate(userDO));
}
這樣Spring就會啓用UserValidator進行參數驗證。但這裏是在全局中加入驗證器,有時候我們希望使用局部驗證器,而不是使用全局驗證器,這是可以仿照Spring MVC的辦法使用註解@InitBinder,將類和驗證器進行綁定。我們刪除在WebFluxConfig中的getValidator方法,然後在Controller中加入如下代碼:
@InitBinder
public void initBinder(DataBinder binder) {
binder.setValidator(new UserValidator());
}
這樣UserValidator就只能對當前的控制器有效,而不是全局有效了。
3.3 訪問靜態資源
有時候我們還需要訪問一些文件,如圖片、配置內容等。這時可以覆蓋WebFluxConfigurer的addResourceHandlers方法,代碼如下:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/static/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
通過這樣的限定,就可以直接通過URI來訪問/resources/static/下的靜態資源,而在Spring的上下文機制中還可以直接訪問/public路徑,它將能夠訪問classpath:/static/下的資源。這裏還設置了緩存的時限,設置爲1年(365天)。爲了區分靜態資源設置一個前綴,這樣便能把靜態資源和動態資源區分出來,爲了我們可以在application.properties文件中進行配置:
spring.webflux.static-path-pattern=/static/**
這樣在訪問靜態資源的時候就需要加入/static/前綴了。
4.深入客戶端開發
上面的客戶端開發只是考慮了正常的狀態,而一些特殊的要求卻沒有考慮,比如如何設置請求頭,服務端發生了錯誤,應當如何處理等。這些都是實踐中經常發生的,所以很有必要討論一下。
4.1 處理服務端錯誤和轉換
客戶端處理服務端錯誤的實例代碼如下:
private static void getUser(WebClient client, long id) {
Mono<UserVO> userVOMono = client.get()
.uri("/user/{id}", id)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.onStatus(httpStatus -> httpStatus.is4xxClientError() || httpStatus.is5xxServerError(), clientResponse -> Mono.empty())
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
if (userVO == null) {
System.out.println("[用戶名稱]" + userVO.getUserName());
} else {
System.out.println("服務端沒有返回該用戶");
}
}
這裏採用了onStatus方法,這個方法是監控服務器返回的方法。它的兩個參數都採用了Lambda表達式的方式,第一個Lambda表達式的參數是HttpStatus類型,它需要返回的是boolean值;第二個Lambda表達式參數爲ClientResponse類型的,它是在第一個Lambda表達式返回true時觸發,這裏是讓結果轉換爲空。
4.2 設置請求頭
有時候需要給HTTP請求頭設置一些屬性,以便於服務端的獲取。實例代碼如下:
//更新用戶
@PutMapping("/user/name")
public Mono<UserVO> updateUserByHeader(@RequestHeader("id") Long id, @RequestHeader("userName") String userName) {
Mono<UserDO> userDOMono = userService.getUser(id);
UserDO userDO = userDOMono.block();
if (userDO == null) {
throw new IllegalArgumentException("找不到該用戶");
}
userDO.setUserName(userName);
return userService.updateUser(userDO).map(u -> translate(userDO));
}
服務端和Spring MVC一樣,使用@RequestHeader從請求頭中獲取參數,然後根據參數去查詢用戶。有了服務端的方法,接着使用客戶端進行測試:
private static void updateUserByHeader(WebClient client, Long id,String userName) {
Mono<UserVO> userVOMono = client.put()
.uri("/user/name")
.header("id",id+"")
.header("userName",userName)
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToMono(UserVO.class);
UserVO userVO = userVOMono.block();
log.error("update user success:{}", userVO.getUserName());
}
這裏的兩個header方法,它們各自設置了用戶編號id和用戶名userName,這樣就可以以請求頭的形式給服務端傳遞參數了。
5.使用路由函數開發WebFlux
除了上述使用類似Spring MVC的開發方式以外,WebFlux還提供了路由函數開發方式來開發WebFlux。這樣的方式體現了高併發的特性,也符合近期興起的函數式編程的潮流。但是也會引入更多的API和長長的方法鏈。
5.1開發處理器
使用路由函數方法,首先需要開發一個處理器處理各種場景。UserHandler的實現代碼如下:
package com.martin.flux.controller;
import com.martin.flux.pojo.UserDO;
import com.martin.flux.pojo.UserVO;
import com.martin.flux.service.UserService;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: martin
* @date: 2020/2/10
*/
@Service
public class UserHandler {
@Autowired
private UserService userService;
public Mono<ServerResponse> getUser(ServerRequest request) {
//獲取請求參數URI參數
String idStr = request.pathVariable("id");
Long id = Long.valueOf(idStr);
Mono<UserVO> userVOMono = userService.getUser(id).map(u -> translate(u));
return ServerResponse
.ok() //響應成功
.contentType(MediaType.APPLICATION_JSON_UTF8) //響應體類型
.body(userVOMono, UserVO.class);//響應體
}
public Mono<ServerResponse> insertUser(ServerRequest request) {
//把請求頭轉換爲UserDO對象
Mono<UserDO> userDOMono = request.bodyToMono(UserDO.class);
Mono<UserVO> userVOMono = userDOMono
.cache() //將數據流對象緩存起來,無需等待數據接收
.flatMap(user -> userService.insertUser(user))
.map(u -> translate(u));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
}
public Mono<ServerResponse> updateUser(ServerRequest request) {
Mono<UserDO> userDOMono = request.bodyToMono(UserDO.class);
Mono<UserVO> userVOMono = userDOMono.cache().flatMap(userDO -> userService.updateUser(userDO)).map(u -> translate(u));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
//獲取請求參數URI參數
String idStr = request.pathVariable("id");
Long id = Long.valueOf(idStr);
Mono<Void> monoVoid = userService.deleteUser(id);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(monoVoid, Void.class);
}
/**
* 查詢用戶
*
* @param request
* @return
*/
public Mono<ServerResponse> findUsers(ServerRequest request) {
//獲取請求參數URI參數
String userName = request.pathVariable("userName");
String note = request.pathVariable("note");
//使用Flux封裝多個數據單元
Flux<UserVO> userVOFlux = userService.findUsers(userName, note).map(u -> translate(u));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOFlux, UserVO.class);
}
/**
* 修改用戶名
*
* @param request
* @return
*/
public Mono<ServerResponse> updateUserName(ServerRequest request) {
//獲取請求頭數據
String idStr = request.headers().header("id").get(0);
String userName = request.headers().header("userName").get(0);
//獲取原有用戶信息
Mono<UserDO> userDOMono = userService.getUser(Long.valueOf(idStr));
UserDO userDO = userDOMono.block();
//修改用戶名
userDO.setUserName(userName);
Mono<UserVO> userVOMono = userService.insertUser(userDO).map(u -> translate(u));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userVOMono, UserVO.class);
}
private UserVO translate(UserDO userDO) {
UserVO userVO = new UserVO();
userVO.setId(userDO.getId());
userVO.setUserName(userDO.getUserName());
userVO.setSexCode(Integer.valueOf(userDO.getSex()));
userVO.setNote(userDO.getNote());
return userVO;
}
}
用戶處理器各個方法開發完成之後,還不能接收請求,因爲還沒有使它與請求URI對應起來,也沒有設置請求接收和相應的類型等信息。
5.2 開發請求路由
爲了讓HTTP請求能夠映射到方法上,還需要一個路由的功能,因此需要開發路由器,使得請求能夠映射到路由的方法上。
package com.martin.flux.config;
import com.martin.flux.controller.UserHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* @author: martin
* @date: 2020/2/11
*/
@Configuration
public class RouterConfig {
@Autowired
private UserHandler userHandler;
//定義用戶路由
@Bean
public RouterFunction<ServerResponse> userRouter() {
RouterFunction<ServerResponse> router = route(GET("/router/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::getUser)
.andRoute(GET("/router/user/{userName}/{note}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::findUsers)
.andRoute(POST("/router/user").and(contentType(MediaType.APPLICATION_STREAM_JSON)).and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::insertUser)
.andRoute(PUT("/router/user").and(contentType(MediaType.APPLICATION_STREAM_JSON)).and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::updateUser)
.andRoute(DELETE("/router/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::deleteUser)
.andRoute(PUT("/router/user/name").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::updateUserName);
return router;
}
}
5.3使用過濾器
在互聯網的環境中往往還需要保護這些業務請求,以避免網站被攻擊。這時可以採用過濾器的方式攔截請求,通過驗證身份後才處理業務邏輯。下面的實例代碼實現了在請求頭中存放用戶名和密碼,後端驗證後才處理業務邏輯。
package com.martin.flux.config;
import com.martin.flux.controller.UserHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* @author: martin
* @date: 2020/2/11
*/
@Configuration
public class RouterFiltConfig {
@Autowired
private UserHandler userHandler;
public static final String HEADER_NAME = "user";
public static final String HEADER_PWD = "password";
//定義用戶路由
@Bean
public RouterFunction<ServerResponse> userRouter() {
RouterFunction<ServerResponse> router =
route(GET("/security/user/{id}").and(accept(MediaType.APPLICATION_STREAM_JSON)), userHandler::getUser)
.filter(((serverRequest, handlerFunction) -> filterLogic(serverRequest, handlerFunction)));
return router;
}
private Mono<ServerResponse> filterLogic(ServerRequest serverRequest, HandlerFunction<ServerResponse> handlerFunction) {
//取出請求頭
String userName = serverRequest.headers().header(HEADER_NAME).get(0);
String password = serverRequest.headers().header(HEADER_PWD).get(0);
//驗證通過條件
if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password) && !userName.equals(password)) {
//接收請求
return handlerFunction.handle(serverRequest);
}
//驗證不匹配,則不允許請求,返回未簽名錯誤
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
}
}