javascript
使用Spring WebFlux进行操作
Spring Boot 2.0最近在GA上線了,所以我決定在相當長一段時間內寫我的第一篇有關Spring的文章。 自發布以來,我已經看到越來越多的提到Spring WebFlux以??及有關如何使用它的教程。 但是,在閱讀完它們并嘗試使它們自己工作之后,我發現很難從我所閱讀的帖子和教程中包含的代碼過渡到編寫代碼,該代碼實際上比返回字符串更有趣從后端。 現在,我希望我不會因為說到您可能對本文中使用的代碼有同樣的批評而無視我的腳步,但這是我嘗試給出一個實際上類似于Spring WebFlux的教程的嘗試。您可能在野外使用的東西。
在繼續之前,以及在提到WebFlux之后,實際上是什么? Spring WebFlux是Spring MVC的完全不阻塞的反應性替代方案。 它允許更好的垂直擴展,而無需增加硬件資源。 現在,它是反應性的,它利用反應性流來異步處理從調用返回到服務器的數據。 這意味著我們將看到更少的List , Collection或什至單個對象,而是它們的反應等效項,例如Flux和Mono (來自Reactor)。 我不會深入探討什么是Reactive Streams,因為說實話,在嘗試向任何人解釋它之前,我需要自己更多地研究它。 相反,讓我們重新關注WebFlux。
我像往常一樣使用Spring Boot在本教程中編寫代碼。
以下是我在本文中使用的依賴項。
<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-cassandra-reactive</artifactId><version>2.0.0.RELEASE</version></dependency></dependencies>盡管我沒有在上面的依賴代碼片段中包含它,但是使用了spring-boot-starter-parent ,最終可以將其2.0.0.RELEASE到2.0.0.RELEASE版本。 作為本教程的主題是有關WebFlux的,包括spring-boot-starter-webflux顯然是一個好主意。 spring-boot-starter-data-cassandra-reactive也已包括在內,因為我們將其用作示例應用程序的數據庫,因為它是(在撰寫本文時)為數不多的具有響應支持的數據庫之一。 通過一起使用這些依賴關系,我們的應用程序可以從前到后完全反應。
WebFlux引入了一種不同的方式來處理請求,而不是使用Spring MVC中使用的@Controller或@RestController編程模型。 但是,它不能替代它。 相反,它已被更新以允許使用反應類型。 這使您可以保持與使用Spring編寫時相同的格式,但是對返回類型進行一些更改,因此可以返回Flux或Mono 。 以下是一個非常人為的示例。
@RestController public class PersonController {private final PersonRepository personRepository;public PersonController(PersonRepository personRepository) {this.personRepository = personRepository;}@GetMapping("/people")public Flux<Person> all() {return personRepository.findAll();}@GetMapping("/people/{id}")Mono<Person> findById(@PathVariable String id) {return personRepository.findOne(id);} }在我看來,這看起來很熟悉,而且乍一看,它與標準的Spring MVC控制器并沒有什么不同,但是在閱讀完這些方法之后,我們可以看到與通常期望的不同的返回類型。 在此示例中, PersonRepository必須是一個反應式存儲庫,因為我們已經能夠直接返回其搜索查詢的結果,以供參考,反應式存儲庫將為集合返回Flux ,對于單個實體返回Mono 。
注解方法不是我在本文中要關注的重點。 這對我們來說還不夠酷和時髦。 沒有足夠的使用lambda來滿足我們以更實用的方式編寫Java的需求。 但是Spring WebFlux有我們的支持。 它提供了一種替代方法來路由和處理到我們服務器的請求,該方法僅使用lambda來編寫路由器功能。 讓我們看一個例子。
@Configuration public class PersonRouter {@Beanpublic RouterFunction<ServerResponse> route(PersonHandler personHandler) {return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get).andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all).andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post).andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put).andRoute(DELETE("/people/{id}"), personHandler::delete).andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);} }這些都是通往PersonHandler中方法的所有路由,我們將在后面介紹。 我們創建了一個將處理路由的bean。 為了設置路由功能,我們使用了命名良好的RouterFunctions類,為我們提供了許多靜態方法,但是現在我們只對它的route方法感興趣。 以下是route方法的簽名。
public static <T extends ServerResponse> RouterFunction<T> route(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {// stuff }該方法顯示,它與HandlerFunction一起接收RequestPredicate并輸出RouterFunction 。
RequestPredicate是我們用來指定路由行為的內容,例如處理程序函數的路徑,請求的類型以及可接受的輸入類型。 由于我使用靜態導入來使所有內容讀起來更加清晰,因此一些重要信息已對您隱藏了。 要創建RequestPredicate我們應該使用RequestPredicates (復數),這是一個靜態幫助器類,為我們提供了所需的所有方法。 我個人確實建議靜態導入RequestPredicates否則由于您可能需要使用RequestPredicates靜態方法的次數而使代碼混亂。 在上面的示例中, GET , POST , PUT , DELETE , accept和contentType都是靜態RequestPredicates方法。
下一個參數是HandlerFunction ,它是一個功能接口。 這里有3條重要信息,它具有<T extends ServerResponse>的通用類型,其handle方法返回Mono<T>并接受ServerRequest 。 使用這些,我們可以確定我們需要傳遞一個返回Mono<ServerResponse> (或其子類型之一)的函數。 顯然,這對我們的處理程序函數返回的內容施加了嚴格的約束,因為它們必須滿足此要求,否則將不適合以這種格式使用。
最后,輸出是RouterFunction 。 然后可以將其返回,并將其用于路由到我們指定的任何函數。 但是通常我們希望將許多不同的請求立即路由到各種處理程序,WebFlux可以滿足這些請求。 由于route返回RouterFunction和事實RouterFunction也有提供自己的路由方法, andRoute ,我們可以鏈通話在一起,不斷增加的所有額外的路線,我們需要。
如果再回顧一下上面的PersonRouter示例,我們可以看到這些方法以REST動詞(例如GET和POST命名,它們定義了處理程序將采用的請求的路徑和類型。 例如,如果我們以第一個GET請求為例,它將使用路徑變量名稱id (由{id}表示的路徑變量)路由到/people ,并且返回內容的類型,特別是APPLICATION_JSON ( MediaType靜態字段)使用accept方法。 如果使用其他路徑,則不會處理。 如果路徑正確,但是Accept標頭不是接受的類型之一,則請求將失敗。
在繼續之前,我想contentType一下accept和contentType方法。 這兩個設置的請求標頭都accept對Accept標頭的匹配,將contentType匹配到Content-Type。 Accept標頭定義了響應可接受的媒體類型,因為我們返回的Person對象的JSON表示將其設置為APPLICATION_JSON (實際標頭中為application/json )。 Content-Type具有相同的想法,但是描述的是發送的請求正文中的媒體類型。 這就是為什么只有POST和PUT動詞包含contentType的原因,其他動詞的主體中沒有任何動詞。 DELETE不包含accept和contentType因此我們可以得出結論,既不希望返回任何內容,也不希望在其請求正文中包含任何內容。
現在我們知道了如何設置路由,讓我們看一下編寫處理傳入請求的處理程序方法。 以下是處理來自先前示例中定義的路由的所有請求的代碼。
@Component public class PersonHandler {private final PersonManager personManager;public PersonHandler(PersonManager personManager) {this.personManager = personManager;}public Mono<ServerResponse> get(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = personManager.findById(id);return person.flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> all(ServerRequest request) {return ok().contentType(APPLICATION_JSON).body(fromPublisher(personManager.findAll(), Person.class));}public Mono<ServerResponse> put(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = request.bodyToMono(Person.class);return personManager.findById(id).flatMap(old ->ok().contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(p -> personManager.update(old, p)),Person.class))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> post(ServerRequest request) {final Mono<Person> person = request.bodyToMono(Person.class);final UUID id = UUID.randomUUID();return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()).contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class));}public Mono<ServerResponse> delete(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));return personManager.findById(id).flatMap(p -> noContent().build(personManager.delete(p))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> getByCountry(ServerRequest serverRequest) {final String country = serverRequest.pathVariable("country");return ok().contentType(APPLICATION_JSON).body(fromPublisher(personManager.findAllByCountry(country), Person.class));} }值得注意的一件事是缺少注釋。 PersonHandler @Component注釋自動創建PersonHandler bean,沒有其他Spring注釋。
我試圖將大多數存儲庫邏輯都排除在此類之外,并通過委派給它所包含的PersonRepository的PersonManager來隱藏對實體對象的任何引用。 如果您對PersonManager中的代碼感興趣,那么可以在我的GitHub上看到它,本文將不再對此進行進一步的解釋,因此我們可以專注于WebFlux本身。
好的,回到手頭的代碼。 讓我們仔細看看get和post方法,以了解發生了什么。
public Mono<ServerResponse> get(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = personManager.findById(id);return person.flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))).switchIfEmpty(notFound().build()); }此方法用于從支持此示例應用程序的數據庫中檢索一條記錄。 由于Cassandra是首選數據庫,因此我決定對每個記錄的主鍵使用UUID ,這具有使測試示例更加煩人的不幸效果,但是有些復制和粘貼無法解決。
請記住,此GET請求的路徑中包含一個路徑變量。 在傳遞給該方法的ServerRequest上使用pathVariable方法,我們可以通過提供變量名稱(在本例中為id來提取其值。 然后將ID轉換為UUID ,如果字符串的格式不正確,則將引發異常,我決定忽略此問題,以使示例代碼不會更混亂。
獲得ID后,我們可以查詢數據庫中是否存在匹配記錄。 返回一個Mono<Person> ,它包含映射到Person的現有記錄,或者保留為空的Mono 。
使用返回的Mono我們可以根據它的存在輸出不同的響應。 這意味著我們可以將有用的狀態代碼與主體內容一起返回給客戶端。 如果記錄存在,則flatMap返回狀態為OK的ServerResponse 。 除了此狀態外,我們還希望輸出記錄,為此,我們指定主體的內容類型(在本例中為APPLICATION_JSON ,然后將記錄添加到其中。 fromPublisher將我們的Mono<Person> (它是Publisher )與Person類一起使用,因此它知道它映射到主體中的內容。 fromPublisher是BodyInserters類中的靜態方法。
如果該記錄不存在,那么該流程將移至switchIfEmpty塊并返回NOT FOUND狀態。 由于找不到任何內容,因此可以將主體保留為空,因此我們只需要創建其中的ServerResponse 。
現在進入post處理程序。
public Mono<ServerResponse> post(ServerRequest request) {final Mono<Person> person = request.bodyToMono(Person.class);final UUID id = UUID.randomUUID();return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()).contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); }即使只是從第一行開始,我們也可以看到它與get方法的工作方式已經不同。 由于這是一個POST請求,因此需要從請求的主體中接受我們要保留的對象。 當我們嘗試插入單個記錄時,我們將使用請求的bodyToMono方法從正文中檢索Person 。 如果要處理多個記錄,則可能要使用bodyToFlux 。
我們將使用created方法返回CREATED狀態,該方法采用URI來確定插入記錄的路徑。 然后,通過使用fromPublisher方法將新記錄添加到響應的正文中,從而遵循與get方法類似的設置。 構成Publisher的代碼略有不同,但是輸出仍然是Mono<Person> ,這很重要。 只是為了進一步說明如何完成插入,使用我們生成的UUID將請求中傳入的Person映射到新的Person ,然后通過調用flatMap save其傳遞到save 。 通過創建一個新的Person我們僅將值插入我們允許的Cassandra中,在這種情況下,我們不希望UUID從請求主體傳入。
因此,關于處理程序,就是這樣。 顯然,還有其他一些我們沒有經歷過的方法。 它們的工作方式不同,但是都遵循相同的概念,即返回ServerResponse ,該ServerResponse包含一個適當的狀態代碼和正文中的記錄(如果需要)。
現在,我們已經編寫了運行基本Spring WebFlux后端所需的所有代碼。 剩下的就是將所有配置捆綁在一起,這對于Spring Boot來說很容易。
@SpringBootApplication public class Application {public static void main(String args[]) {SpringApplication.run(Application.class);} }而不是在這里結束帖子,我們可能應該研究如何實際使用代碼。
Spring提供了WebClient類來處理請求而不會阻塞。 現在,我們可以利用它作為測試應用程序的方法,盡管這里也可以使用WebTestClient 。 創建響應式應用程序時,將使用WebClient而不是阻塞RestTemplate 。
下面的代碼調用了PersonHandler中定義的處理程序。
public class Client {private WebClient client = WebClient.create("http://localhost:8080");public void doStuff() {// POSTfinal Person record = new Person(UUID.randomUUID(), "John", "Doe", "UK", 50);final Mono<ClientResponse> postResponse =client.post().uri("/people").body(Mono.just(record), Person.class).accept(APPLICATION_JSON).exchange();postResponse.map(ClientResponse::statusCode).subscribe(status -> System.out.println("POST: " + status.getReasonPhrase()));// GETclient.get().uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4").accept(APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(Person.class)).subscribe(person -> System.out.println("GET: " + person));// ALLclient.get().uri("/people").accept(APPLICATION_JSON).exchange().flatMapMany(response -> response.bodyToFlux(Person.class)).subscribe(person -> System.out.println("ALL: " + person));// PUTfinal Person updated = new Person(UUID.randomUUID(), "Peter", "Parker", "US", 18);client.put().uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc").body(Mono.just(updated), Person.class).accept(APPLICATION_JSON).exchange().map(ClientResponse::statusCode).subscribe(response -> System.out.println("PUT: " + response.getReasonPhrase()));// DELETEclient.delete().uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc").exchange().map(ClientResponse::statusCode).subscribe(status -> System.out.println("DELETE: " + status));} }不要忘記在某個地方實例化Client ,下面是一種不錯的懶惰方式!
@SpringBootApplication public class Application {public static void main(String args[]) {SpringApplication.run(Application.class);Client client = new Client();client.doStuff();} }首先,我們創建WebClient 。
private final WebClient client = WebClient.create("http://localhost:8080");一旦創建,我們就可以開始使用它做事,因此可以使用doStuff方法。
讓我們分解一下發送到后端的POST請求。
final Mono<ClientResponse> postResponse =client.post().uri("/people").body(Mono.just(record), Person.class).accept(APPLICATION_JSON).exchange(); postResponse.map(ClientResponse::statusCode).subscribe(status -> System.out.println("POST: " + status.getReasonPhrase()));我將此內容寫下的略有不同,因此您可以看到從發送請求返回了Mono<ClientResponse> 。 exchange方法將HTTP請求發送到服務器。 然后,無論何時到達,都會處理該響應。
當然,使用WebClient我們指定我們要使用post方法發送POST請求。 的URI ,然后用所添加的uri的方法(重載的方法,這一個發生在一個String ,但另一個接受一個URI )。 我不敢說此方法執行了該方法所要求的操作,然后將正文內容與Accept標頭一起添加。 最后,我們通過調用exchange發送請求。
請注意, APPLICATION_JSON的媒體類型與POST路由器功能中定義的類型匹配。 如果我們要發送其他類型,請說TEXT_PLAIN ,則將收到404錯誤,因為不存在與請求返回的請求相匹配的處理程序。
使用通過調用exchange返回的Mono<ClientResponse> ,我們可以將其內容映射到所需的輸出。 在上面的示例中,狀態代碼將打印到控制臺。 如果我們回想一下PersonHandler的post方法,請記住它只能返回“已創建”狀態,但是如果發送的請求與之不正確匹配,則將打印出“未找到”。
讓我們看一下其他請求之一。
client.get().uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4").accept(APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(Person.class)).subscribe(person -> System.out.println("GET: " + person));這是我們的典型GET請求。 它看起來與我們剛經歷的POST請求非常相似。 主要區別在于uri將請求和UUID的路徑(在這種情況下為String )都作為參數,它將替換路徑變量{id} ,并且正文保留為空。 響應的處理方式也不同。 在此示例中,它提取響應的正文并將其映射到Mono<Person>并打印出來。 可以使用前面的POST示例完成此操作,但是響應的狀態代碼在該場景中更為有用。
從稍微不同的角度來看,我們可以使用cURL發出請求并查看響應的外觀。
CURL -H "Accept:application/json" -i localhost:8080/peopleHTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/json[{"id": "13c403a2-6770-4174-8b76-7ba7b75ef73d","firstName": "John","lastName": "Doe","country": "UK","age": 50},{"id": "fbd53e55-7313-4759-ad74-6fc1c5df0986","firstName": "Peter","lastName": "Parker","country": "US","age": 50} ]響應看起來像這樣,顯然,它會根據您存儲的數據而有所不同。
注意響應頭。
transfer-encoding: chunked Content-Type: application/json此處的transfer-encoding表示可分塊傳輸的數據,可用于傳輸數據。 這就是我們所需要的,以便客戶端可以對返回的數據做出反應。
我認為這應該是一個停止的好地方。 我們在這里介紹了很多資料,希望可以幫助您更好地理解Spring WebFlux。 我還想介紹WebFlux的其他一些主題,但是我將在單獨的文章中介紹這些主題,因為我認為這足夠長了。
總之,在本文中,我們非常簡要地討論了為什么要在典型的Spring MVC后端上使用Spring WebFlux。 然后,我們研究了如何設置路由和處理程序以處理傳入的請求。 處理程序實現了可以處理大多數REST動詞的方法,并在響應中返回了正確的數據和狀態代碼。 最后,我們研究了兩種向后端發出請求的方法,一種是使用WebClient直接在客戶端上處理輸出,另一種是通過cURL查看返回的JSON的外觀。
如果您有興趣查看我用來創建本文示例應用程序的其余代碼,可以在我的GitHub上找到它。
與往常一樣,如果您發現此帖子有幫助,請分享它;如果您想了解我的最新帖子,則可以通過Twitter @LankyDanDev關注我。
翻譯自: https://www.javacodegeeks.com/2018/03/doing-stuff-with-spring-webflux.html
總結
以上是生活随笔為你收集整理的使用Spring WebFlux进行操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曝一加Pad Go平板电脑即将推出 配超
- 下一篇: c语言构建栈_选择技术栈构建通用平台