阅读 7159

实战Spring Boot 2.0 Reactive编程系列 - WebFlux初体验

前言

上文引入了 反应式编程模型 相关概念,对 Spring Reactor 的核心 API 进行了简单归纳。本文会对 Spring 5 WebFlux 进行相关介绍,包括引入 Servlet 3.1 +,各个功能组件 Router FunctionsWebFluxReactive Streams 等,以及如何在 Spring Boot 2.0 中分别以 全局功能路由MVC 控制器 的方式配置 HTTP 请求处理。

正文

Spring 5 WebFlux介绍

关于 Spring 5WebFlux 响应式编程,下图是传统 Spring Web MVC 结构以及Spring 5 中新增加的基于 Reactive StreamsSpring WebFlux 框架。可以使用 webFlux 模块来构建 异步的非堵塞的事件驱动 的服务,其在 伸缩性方面 表现非常好。

如图所示,WebFlux 模块从上到下依次是 Router FunctionsWebFluxReactive Streams 三个新组件。

Servlet 3.1+ API介绍

WebFlux 模块需要运行在实现了 Servlet 3.1+ 规范 的容器之上。Servlet 3.1 规范中新增了对 异步处理 的支持,在新的 Servlet 规范中,Servlet 线程不需要一直 阻塞等待 到业务处理完成。

Servlet 3.1 中,其请求处理的线程模型大致如下:

  1. Servlet 线程接收到新的请求后,不需要等待业务处理完成再进行结果输出,而是将这个请求委托给另外一个线程(业务线程)来完成。

  2. Servlet 线程将委托完成之后变返回到容器中去接收新的请求。

Servlet 3.1 规范特别适用于那种 业务处理非常耗时 的场景之下,可以减少 服务器资源 的占用,并且提高 并发处理速度 ,而对于那些能 快速响应 的场景收益并不大。

所以 WebFlux 支持的容器有 TomcatJetty同步容器 ,也可以是 NettyUndertow 这类 异步容器。在容器中 Spring WebFlux 会将 输入流 适配成 MonoFlux 格式进行统一处理。

Spring WebFlux的功能模块

下面介绍上图中 WebFlux 各个模块:

1. Router Functions

对标准的 @Controller@RequestMapping等的 Spring MVC 注解,提供一套 函数式风格API,用于创建 RouterHandlerFilter

2. WebFlux

核心组件,协调上下游各个组件提供 响应式编程 支持。

3. Reactive Streams

一种支持 背压 (Backpressure)异步数据流处理标准,主流实现有 RxJavaReactorSpring WebFlux 集成的是Reactor

Flux

FluxMono 属于 事件发布者,类似于 生产者,对消费者 提供订阅接口。当有事件发生的时候,FluxMono 会回调 消费者相应的方法来通知 消费者 相应的事件。

下面这张图是 Flux 的工作流程图:

关于 Flux 的工作模式,可以看出 Flux 可以 触发 (emit) 很多 item,而这些 item 可以经过若干 Operators 然后才被 subscribe,下面是使用 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 的处理流程,可以很直观的看出来 MonoFlux 的区别:

Mono 只能触发 (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);
复制代码

Spring Boot 2.0 Reactive Stack

Spring Boot Webflux 就是基于 Reactor 实现的。Spring Boot 2.0 包括一个新的 spring-webflux 模块。该模块包含对 响应式 HTTPWebSocket 客户端的支持,以及对 RESTHTMLWebSocket 交互等程序 的支持。一般来说,Spring MVC 用于 同步处理Spring Webflux 用于 异步处理

如上图所示,从 Web 表现层到数据访问,再到容器,Spring Boot 2.0 同时提供了 同步阻塞式异步非阻塞式 两套完整的 API Stack

从上而下对比以下两者的区别:

API Stack Sevlet Stack Reactive Stack
Web控制层 Spring MVC Spring WebFlux
安全认证层 Spring Security Spring Security
数据访问层 Spring Data Repositories Spring Data Reactive Repositories
容器API Servlet API Reactive Streams Adapters
内嵌容器 Servlet Containers Netty, Servlet 3.1+ Containers

适用场景

控制层一旦使用 Spring WebFlux,它下面的安全认证层、数据访问层都必须使用 Reactive API。其次,Spring Data Reactive Repositories 目前只支持 MongoDBRedisCouchbase 等几种不支持事务管理的 NOSQL。技术选型时一定要权衡这些弊端和风险,比如:

  1. Spring MVC 能满足场景的,就不需要更改为 Spring WebFlux

  2. 要注意容器的支持,可以看看底层 内嵌容器 的支持。

  3. 微服务 体系结构,Spring WebFluxSpring MVC 可以混合使用。尤其开发 IO 密集型 服务的时候,可以选择 Spring WebFlux 去实现。

编程模型

Spring 5 Web 模块包含了 Spring WebFluxHTTP 抽象。类似 Servlet APIWebFlux 提供了 WebHandler API 去定义 非阻塞 API 抽象接口。可以选择以下两种编程模型实现:

  1. 注解控制层:MVC 保持一致,WebFlux 也支持 响应性 @RequestBody 注解。

  2. 功能性端点: 基于 lambda 轻量级编程模型,用来 建立路由处理请求 的工具。和上面最大的区别就是,这种模型,全程 控制了 请求 - 响应 的生命流程

内嵌容器

Spring Boot 大框架一样启动应用,但 Spring WebFlux 默认是通过 Netty 启动,并且自动设置了 默认端口8080。另外还提供了对 JettyUndertow 等容器的支持。开发者自行在添加对应的容器 Starter 组件依赖,即可配置并使用对应 内嵌容器实例

注意: 必须是 Servlet 3.1+ 容器,如 Tomcat、Jetty;或者非 Servlet 容器,如 Netty 和 Undertow。

Starter 组件

Spring Boot 大框架一样,Spring Boot Webflux 提供了很多 开箱即用Starter 组件。添加 spring-boot-starter-webflux 依赖,就可用于构建 响应式 API 服务,其包含了 WebFluxTomcat 内嵌容器 等。

快速开始

Spring Initializr构建项目骨架

利用 Spring Initializer 快速生成 Spring Boot 应用,配置项目信息并设置依赖。

配置Maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </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>
复制代码

Spring Boot启动类

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
复制代码

配置实体类

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    String body;
}
复制代码

1. MVC控制器方式

1.1. 编写控制器

@RestController
@RequestMapping
public class MessageController {
    @GetMapping
    public Flux<Message> allMessages(){
        return Flux.just(
            Message.builder().body("hello Spring 5").build(),
            Message.builder().body("hello Spring Boot 2").build()
        );
    }  
}
复制代码

1.2. 编写测试类

@RunWith(SpringRunner.class)
@WebFluxTest(controllers = MessageController.class)
public class DemoApplicationTests {
    @Autowired
    WebTestClient client;

    @Test
    public void getAllMessagesShouldBeOk() {
        client.get().uri("/").exchange().expectStatus().isOk();
    }
}
复制代码

1.3. 查看启动日志

2018-05-27 17:37:23.550  INFO 67749 --- [           main] s.w.r.r.m.a.RequestMappingHandlerMapping : Mapped "{[],methods=[GET]}" onto reactor.core.publisher.Flux<com.example.demo.Message> com.example.demo.MessageController.allMessages()
2018-05-27 17:37:23.998  INFO 67749 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-27 17:37:23.999  INFO 67749 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-27 17:37:24.003  INFO 67749 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.6 seconds (JVM running for 2.274)
复制代码

从日志里可以看出:

  1. 启动时 WebFlux 利用 MVC 原生的 RequestMappingHandlerMapping 将控制器里的 请求路径MVC 中的 处理器 进行绑定。
  2. Spring WebFlux 默认采用 Netty 作为 内嵌容器,且启动端口默认为 8080

访问 http://localhost:8080,返回结果如下:

2. 全局Router API方式

2.1. 配置全局Router Bean

@Configuration
public class DemoRouterConfig {
    @Bean
    public RouterFunction<ServerResponse> routes() {
        return route(GET("/"), (ServerRequest req)-> ok()
                .body(
                    BodyInserters.fromObject(
                        Arrays.asList(
                            Message.builder().body("hello Spring 5").build(),
                            Message.builder().body("hello Spring Boot 2").build()
                        )
                    )
                )
        );
    }
}
复制代码

2.2. 编写测试类

@RunWith(SpringRunner.class)
@WebFluxTest
public class DemoApplicationTests {    
    @Autowired
    WebTestClient client;
    
    @Test
    public void getAllMessagesShouldBeOk() {
        client.get().uri("/").exchange().expectStatus().isOk();
    }
}
复制代码

2.3. 查看启动日志

运行 Spring Boot 启动入口类,启动日志如下(不重要的省略):

2018-05-27 17:20:28.870  INFO 67696 --- [           main] o.s.w.r.f.s.s.RouterFunctionMapping      : Mapped (GET && /) -> com.example.demo.DemoRouterConfig$$Lambda$213/1561745898@3381b4fc
2018-05-27 17:20:28.931  INFO 67696 --- [           main] o.s.w.r.r.m.a.ControllerMethodResolver   : Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@1460a8c0: startup date [Sun May 27 17:20:27 CST 2018]; root of context hierarchy
2018-05-27 17:20:29.311  INFO 67696 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-27 17:20:29.312  INFO 67696 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-27 17:20:29.316  INFO 67696 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.137 seconds (JVM running for 3.169)
复制代码

从日志里可以看出:启动时 WebFlux 利用 RouterFunctionMappingRouterFunction 里的 全局路径请求处理 进行了映射和绑定。

访问 http://localhost:8080,返回结果如下:

可以看出,无论是使用 Fucntional Router 还是 MVC Controller,都可以产生相同的效果!

开发运行环境

  • JDK 1.8 + : Spring Boot 2.x 要求 JDK 1.8 环境及以上版本。另外,Spring Boot 2.x 只兼容 Spring Framework 5.0 及以上版本。

  • Maven 3.2 + : 为 Spring Boot 2.x 提供了相关依赖构建工具是 Maven,版本需要 3.2 及以上版本。使用 Gradle 则需要 1.12 及以上版本。MavenGradle 大家各自挑选下喜欢的就好。

小结

本文首先对 Spring 5 WebFlux 进行了单独介绍,包括引入 Servlet 3.1 +,各个功能组件 Router FunctionsWebFluxReactive Streams 等。然后在 Spring Boot 2.0 详细地介绍了 Reactive StackServlet Stack 的组成区别,并分别给出了 WebFlux 基于 全局功能路由控制器 的配置和使用案例。


欢迎关注技术公众号: 零壹技术栈

零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

关注下面的标签,发现更多相似文章
评论