Spring WebFlux 教程:如何构建反应式 Web 应用程序

反应式系统提供了我们在高数据流世界中所需的无与伦比的响应能力和可扩展性。然而,反应式系统需要经过专门培训的工具和开发人员来实现这些独特的程序架构。Spring WebFlux with Project Reactor 是一个专门为满足现代公司的响应式需求而构建的框架。

今天,我们将通过解释 WebFlux 如何与其他反应式堆栈工具配合、有何不同以及如何制作您的第一个应用程序来帮助您开始使用 WebFlux。

什么是反应式系统?

反应式系统是采用反应式架构模式设计的系统,该模式优先使用松耦合、灵活和可扩展的组件。它们的设计还考虑了故障解决方案,以确保即使出现故障,大部分系统仍能运行。

反应式系统专注于:

  • 反应性:最重要的是,反应性系统应该对任何用户输入做出快速响应。反应式系统倡导者认为,反应式有助于优化系统的所有其他部分,从数据收集到用户体验。

  • 弹性:反应式系统的设计应该能够预测系统故障。反应式系统期望组件最终会失效,并设计松散耦合的系统,即使几个单独的部件停止工作也能保持活动状态。

  • 弹性:反应式系统应该通过扩大或缩小以满足需求来适应工作负载的大小。许多反应式系统还将使用预测性扩展来预测和准备突然变化。实现弹性的关键是消除任何瓶颈并构建可以根据需要分片或复制组件的系统。

  • 消息驱动的通信:反应式系统的所有组件都是松散耦合的,每个组件之间都有硬边界。您的系统应该通过显式消息传递跨越这些边界进行通信。这些消息让不同的组件了解故障,并帮助他们将工作流委派给可以处理它的组件。

什么是反应堆项目?

Project Reactor 是一个由 Pivotal 构建并由 Spring 提供支持的框架。它实现了反应式 API 模式,最著名的是反应式流规范。

如果您熟悉Java 8 Streams,您会很快发现 Stream 和 Flux(或其单元素版本 Mono)之间的许多相似之处。它们之间的主要区别在于 Fluxes 和 Monos 遵循一种publisher-subscriber模式并实现背压,而 Stream API 则没有。

使用 Reactor 的主要优点是您可以完全控制数据流。您可以依靠订阅者在准备好处理信息时询问更多信息的能力,或者在发布者端缓冲一些结果,甚至使用没有背压的全推送方法。

在我们的反应式堆栈中,它位于 Spring Boot 2.0 和 WebFlux 之上:

示例反应式堆栈

什么是 Spring WebFlux?

Spring WebFlux 是一个完全非阻塞、基于注解的 Web 框架,它构建在 Project Reactor 之上,它使得在 HTTP 层上构建响应式应用程序成为可能。WebFlux 使用新的路由器功能特性将函数式编程应用于 Web 层并绕过声明性控制器和请求映射。WebFlux 要求您将 Reactor 作为核心依赖项导入。

WebFlux 作为Spring MVC的响应式替代品在 Spring 5 中添加,并增加了对以下内容的支持:

  • 非阻塞线程:无需等待先前任务完成即可完成指定任务的并发线程。
  • Reactive Stream API:一种标准化工具,包括用于非阻塞背压的异步流处理选项。
  • 异步数据处理:当数据在后台处理并且用户可以不间断地继续使用正常的应用程序功能时。

最终WebFlux摒弃了SpringMVC的多请求线程模型,而是使用多EventLoop非阻塞模型来启用反应式、可扩展的应用程序。由于支持Netty、Undertow 和Servlet 3.1+ 容器等流行服务器,WebFlux 已成为反应式堆栈的关键部分。

Router功能

RouterFunction是标准springmvc中使用的@RequestMapping和@Controller注释样式的一种功能替代。

我们可以使用它将请求路由到处理程序函数:

  • 传统的路由定义
@RestController
public class ProductController {
    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}
  • 函数式定义
@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

你可以使用RouterFunctions.route()来创建路由,而不是编写完整的路由器函数。路由注册为spring的bean,因此可以在任何配置类中创建。 路由器功能避免了由请求映射的多步骤过程引起的潜在副作用,而是将其简化为直接的路由器/处理程序链。这允许函数式编程实现反应式编程。

WebClient 详解

项目中经常用到发送Http请求的客户端,如果你使用webflux那非常简单去创建一个Http请求。WebClient是WebFlux的反应式web客户端,它是从著名的rest模板构建的。它是一个接口,表示web请求的主要入口点,并支持同步和异步操作。WebClient主要用于反应式后端到后端通信。

您可以通过使用Maven导入标准WebFlux依赖项来构建和创建WebClient实例:

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

创建实例


WebClient webClient = WebClient.create();
// 如果是调用特定服务的API,可以在初始化webclient 时使用,baseUrl
WebClient webClient = WebClient.create("https://github.com/1ssqq1lxr");

或者构造器方式初始化

WebClient webClient1 = WebClient.builder()
    .baseUrl("https://github.com/1ssqq1lxr")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
    .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
    .build();
  • Get请求
Mono<String> resp = WebClient.create()
      .method(HttpMethod.GET)
      .uri("https://github.com/1ssqq1lxr")
      .cookie("token","xxxx")
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .retrieve().bodyToMono(String.class);
  • Post请求(表单)
 MultiValueMap<String, String> formData = new LinkedMultiValueMap();
 formData.add("name1","value1");
 formData.add("name2","value2");
 Mono<String> resp = WebClient.create().post()
       .uri("http://www.w3school.com.cn/test/demo_form.asp")
       .contentType(MediaType.APPLICATION_FORM_URLENCODED)
       .body(BodyInserters.fromFormData(formData))
       .retrieve().bodyToMono(String.class);
  • Post请求(Body)
Book book = new Book();
book.setName("name");
book.setTitle("this is title");
Mono<String> resp = WebClient.create().post()
        .uri("https://github.com/1ssqq1lxr")
        .contentType(MediaType.APPLICATION_JSON_UTF8)
        .body(Mono.just(book),Book.class)
        .retrieve().bodyToMono(String.class);
  • 文件上传
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity<ClassPathResource> entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
arts.add("file", entity);
Mono<String> resp = WebClient.create().post()
        .uri("http://localhost:8080/upload")
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body(BodyInserters.fromMultipartData(parts))
        .retrieve().bodyToMono(String.class);

Reactive Steam API

Reactive Stream API是一个的函数集合,允许更智能的流数据流。它内置了对背压和异步处理的支持,确保应用程序最有效地利用计算机和组件资源。

反应流API有四个主要接口:

  • Publisher:根据链接订阅者的需求向他们发布事件。充当订户可以监视事件的中心链接点。

  • Subscriber:接收和处理发布服务器发出的事件。多个订阅服务器可以链接到单个发布服务器,并对同一事件做出不同的响应。订户可以设置为反应:

    • onNext,当它接收到下一个事件时。

    • onSubscribe,添加新订阅时

    • onError,当另一个订阅服务器发生错误时

    • onComplete,当一个订阅完成时

Server容器

WebFlux在Tomcat、Jetty、servlet3.1+容器以及Netty和Undertow等非Servlet运行时上都受支持。Netty最常用于异步和非阻塞设计,因此WebFlux将默认使用它。只需对Maven或Gradle构建软件进行简单的更改,就可以轻松地在这些服务器选项之间切换。

这使得WebFlux在它可以使用的技术方面具有高度的通用性,并允许您使用现有的基础设施轻松地实现它。

并发模型

WebFlux是以无阻塞的思想构建的,因此使用了与springmvc不同的并发编程模型。

springmvc假设线程将被阻塞,并在阻塞实例期间使用一个大的线程池来保持移动。这个更大的线程池使得MVC资源更密集,因为计算机硬件必须同时保持更多的线

WebFlux使用了一个小的线程池,因为它假设您永远不需要通过工作来避免阻塞。这些线程称为事件循环工作线程,数量固定,在传入请求中的循环速度比MVC线程快。这意味着WebFlux更有效地使用计算机资源,因为活动线程总是在工作。

Spring WebFlux Security

WebFlux使用Spring安全性来实现身份验证和授权协议。springsecurity使用WebFilter根据经过身份验证的用户列表认证请求。

@EnableWebFluxSecurity
public class HelloWebFluxSecurityConfig {
    @Bean
    public MapReactiveUserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("user")
            .roles("USER")
            .build();
        return new MapReactiveUserDetailsService(user);
    }
}

在这里,我们可以看到用户有一个用户名、一个密码和一个或多个roles标签,这些标签允许自定义定访问。类似于SpringBoot Security的 UserDetailsService接口

开始使用 Spring WebFlux

生成项目

spring代码生成器

参考配置

生成后的pom如下

<?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.5.1</version>
  <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>com.github.webflux.learn</groupId>
 <artifactId>demo</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>demo</name>
 <description>Demo project for Spring Boot</description>
 <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.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
  </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>
    <configuration>
     <excludes>
      <exclude>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
      </exclude>
     </excludes>
    </configuration>
   </plugin>
  </plugins>
 </build>

</project>

开发接口

自定义一个函数路由:将请求path中的占位参数获取作为返回值

/**
 * @author coding途中
 */
@Configuration
public class TestRouter {

    @Bean
    public RouterFunction<ServerResponse> routeExample() {
        return RouterFunctions
                .route(RequestPredicates.GET("/hello/{path}").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), serverRequest -> {
                    String str = serverRequest.pathVariable("path");
                    return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).bodyValue(str)
                            .switchIfEmpty(ServerResponse.notFound().build());
                });
    }
}

浏览器请求 http://localhost:4990/hello/haha

haha

添加认证


/**
 * @author coding途中
 */

@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig  {

    @Bean
    public MapReactiveUserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("user")
                .roles("USER")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        // @formatter:off
        return http.authorizeExchange()
                .pathMatchers( "/hello/**").authenticated()
                .pathMatchers("/hello/login").permitAll()
                .anyExchange().authenticated()
                .and()
                .formLogin().and()
                .logout().and()
                .httpBasic().and()
                .csrf().disable()
                .build();
    }
}
  • 再次请求接口 浏览器请求 http://localhost:4990/hello/haha 此时浏览重定向到 http://localhost:4990/login

登陆页面

输入user/user 用户名密码后完成登陆。

再次浏览器请求 http://localhost:4990/hello/authenticate

authenticate

结束

点赞是认可,在看是支持

06-25 09:34