Spring WebFlux 入门
1. WebFlux介绍
Spring WebFlux 是 Spring Framework 5.0中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。
Spring WebFlux 用于创建基于事件循环执行模型的完全异步且非阻塞的应用程序。
(PS:所谓异步非阻塞是针对服务端而言的,是说服务端可以充分利用CPU资源去做更多事情,这与客户端无关,客户端该怎么请求还是怎么请求。)
Reactive Streams是一套用于构建高吞吐量、低延迟应用的规范。而Reactor项目是基于这套规范的实现,它是一个完全非阻塞的基础,且支持背压。Spring WebFlux基于Reactor实现了完全异步非阻塞的一套web框架,是一套响应式堆栈。
【spring-webmvc + Servlet + Tomcat】响应式的、异步非阻塞的
【spring-webflux + Reactor + Netty】命令式的、同步阻塞的
2. Spring WebFlux Framework
Spring WebFlux有两种风格:功能性和基于注释的。基于注释的与Spring MVC非常相近。例如:
@RestController @RequestMapping("/users") public class MyRestController { @GetMapping("/{user}") public Mono<User> getUser(@PathVariable Long user) { // ... } @GetMapping("/{user}/customers") public Flux<Customer> getUserCustomers(@PathVariable Long user) { // ... } @DeleteMapping("/{user}") public Mono<User> deleteUser(@PathVariable Long user) { // ... } }
与之等价,也可以这样写:
@Configuration public class RoutingConfiguration { @Bean public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) { return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser) .andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers) .andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser); } } @Component public class UserHandler { public Mono<ServerResponse> getUser(ServerRequest request) { // ... } public Mono<ServerResponse> getUserCustomers(ServerRequest request) { // ... } public Mono<ServerResponse> deleteUser(ServerRequest request) { // ... } }
如果你同时添加了spring-boot-starter-web和spring-boot-starter-webflux依赖,那么Spring Boot会自动配置Spring MVC,而不是WebFlux。你当然可以强制指定应用类型,通过SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)
3. Hello WebFlux
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.example</groupId> <artifactId>cjs-reactive-rest-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cjs-reactive-rest-service</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
GreetingHandler.java
package com.cjs.example.restservice.hello; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; import java.util.concurrent.atomic.AtomicLong; /** * @author ChengJianSheng * @date 2020-03-25 */ @Component public class GreetingHandler { private final AtomicLong counter = new AtomicLong(); /** * A handler to handle the request and create a response */ public Mono<ServerResponse> hello(ServerRequest request) { return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) .body(BodyInserters.fromValue("Hello, Spring!")); } }
GreetingRouter.java
package com.cjs.example.restservice.hello; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.*; /** * @author ChengJianSheng * @date 2020-03-25 */ @Configuration public class GreetingRouter { /** * The router listens for traffic on the /hello path and returns the value provided by our reactive handler class. */ @Bean public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) { return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello); } }
GreetingWebClient.java
package com.cjs.example.restservice.hello; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; /** * @author ChengJianSheng * @date 2020-03-25 */ public class GreetingWebClient { /** * For reactive applications, Spring offers the WebClient class, which is non-blocking. * * WebClient can be used to communicate with non-reactive, blocking services, too. */ private WebClient client = WebClient.create("http://localhost:8080"); private Mono<ClientResponse> result = client.get() .uri("/hello") .accept(MediaType.TEXT_PLAIN) .exchange(); public String getResult() { return ">> result = " + result.flatMap(res -> res.bodyToMono(String.class)).block(); } }
Application.java
package com.cjs.example.restservice; import com.cjs.example.restservice.hello.GreetingWebClient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author ChengJianSheng * @date 2020-03-25 */ @SpringBootApplication public class CjsReactiveRestServiceApplication { public static void main(String[] args) { SpringApplication.run(CjsReactiveRestServiceApplication.class, args); GreetingWebClient gwc = new GreetingWebClient(); System.out.println(gwc.getResult()); } }
可以直接在浏览器中访问 http://localhost:8080/hello
GreetingRouterTest.java
package com.cjs.example.restservice; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class GreetingRouterTest { @Autowired private WebTestClient webTestClient; /** * Create a GET request to test an endpoint */ @Test public void testHello() { webTestClient.get() .uri("/hello") .accept(MediaType.TEXT_PLAIN) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello, Spring!"); } }
4. Reactor 核心特性
Mono: implements Publisher and returns 0 or 1 elements
Flux: implements Publisher and returns N elements
5. Spring Data Redis
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.example</groupId> <artifactId>cjs-webflux-hello</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cjs-webflux-hello</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.67</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
UserController.java
package com.cjs.example.webflux.controller; import com.alibaba.fastjson.JSON; import com.cjs.example.webflux.domain.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.ReactiveHashOperations; import org.springframework.data.redis.core.ReactiveStringRedisTemplate; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; /** * @author ChengJianSheng * @date 2020-03-27 */ @RestController @RequestMapping("/users") public class UserController { @Autowired private ReactiveStringRedisTemplate reactiveStringRedisTemplate; @GetMapping("/hello") public Mono<String> hello() { return Mono.just("Hello, Reactive"); } @PostMapping("/save") public Mono<Boolean> saveUser(@RequestBody User user) { ReactiveHashOperations hashOperations = reactiveStringRedisTemplate.opsForHash(); return hashOperations.put("USER_HS", String.valueOf(user.getId()), JSON.toJSONString(user)); } @GetMapping("/info/{id}") public Mono<User> info(@PathVariable Integer id) { ReactiveHashOperations reactiveHashOperations = reactiveStringRedisTemplate.opsForHash(); Mono<String> hval = reactiveHashOperations.get("USER_HS", String.valueOf(id)); return hval.map(e->JSON.parseObject(e, User.class)); } }
CoffeeController.java
package com.cjs.example.webflux.controller; import com.cjs.example.webflux.domain.Coffee; import org.springframework.data.redis.core.*; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0. * Unlike Spring MVC, it does not require the Servlet API, is fully asynchronous and non-blocking, * and implements the Reactive Streams specification through the Reactor project. * * @author ChengJianSheng * @date 2020-03-27 */ @RestController @RequestMapping("/coffees") public class CoffeeController { private final ReactiveRedisOperations<String, Coffee> coffeeOps; public CoffeeController(ReactiveRedisOperations<String, Coffee> coffeeOps) { this.coffeeOps = coffeeOps; } @GetMapping("/getAll") public Flux<Coffee> getAll() { return coffeeOps.keys("*").flatMap(coffeeOps.opsForValue()::get); } @GetMapping("/info/{id}") public Mono<Coffee> info(@PathVariable String id) { ReactiveValueOperations valueOperations = coffeeOps.opsForValue(); return valueOperations.get(id); } }
最后,也是非常重要的一点:异步非阻塞并不会使程序运行得更快。WebFlux 并不能使接口的响应时间缩短,它仅仅能够提升吞吐量和伸缩性。
Spring WebFlux 是一个异步非阻塞的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。
Reactive and non-blocking generally do not make applications run faster.
6. Docs
https://projectreactor.io/docs/core/release/reference/index.html
https://projectreactor.io/docs/core/release/reference/index.html#core-features
https://docs.spring.io/spring/docs/
https://docs.spring.io/spring/docs/5.1.7.RELEASE/spring-framework-reference/index.html