Files
2026-03-01 01:43:46 +08:00

9.1 KiB
Raw Permalink Blame History

差别

Spring WebFlux 和传统的 Spring MVC (使用@Controller 和@RequestMapping 等注解定义的控制器)之间主要的区别在于它们对于处理请求的方式不同,这主要体现在响应式编程范式和阻塞式编程范式的差异上。下面我将通过几个方面来进行通俗说明。

编程模型

Spring MVC

  • 阻塞式 I/O 操作:每个 HTTP 请求通常在一个单独的线程中被处理,如果请求处理涉及到长时间的 I/O 操作(如数据库查询),那么该线程会在操作完成之前处于阻塞状态。
  • 注解驱动:使用 @Controller@RequestMapping 之类的注解来定义路由和处理方法,这些注解通常与同步操作一起使用。

Spring WebFlux

  • 非阻塞式 I/O 操作:使用响应式编程模型,即使操作是长时间运行的,也不会阻塞线程。而是在操作可以继续进行时,以回调的方式恢复执行。
  • 函数式端点定义:虽然 WebFlux 也支持与 Spring MVC 相同的注解方式,但它引入了一个新的函数式编程模型,可以使用路由器函数(RouterFunction)和处理器函数(HandlerFunction)来声明路由和处理逻辑。

底层运行库

  • Spring MVC:通常运行在阻塞式的 Web 服务器上,如 Tomcat。
  • Spring WebFlux:可以运行在支持非阻塞运行环境的服务器上,如 Netty、Undertow。

性能和资源利用

  • Spring MVC:在高负载时,每个请求占用一个线程,可能会导致线程资源耗尽。
  • Spring WebFlux:响应式编程使得可以用较少的线程来处理大量并发请求,从而提高了线程的利用率。

应用场景

  • Spring MVC:适用于传统的、请求 - 响应式的 Web 应用程序,特别是当你有很多的同步操作时。
  • Spring WebFlux:适用于需要处理大量并发连接,以及要求低延迟 & 高吞吐量的实时性应用,例如实时数据处理、在线游戏后端等。

影响

  • 开发风格:WebFlux 的函数式风格可能需要开发者学习新的编程范式,而 Spring MVC 的注解风格更符合大多数 Java 开发者的习惯。
  • 可维护性:WebFlux 的函数式风格可以带来更好的可测试性和可维护性,但这取决于开发者对响应式编程的熟悉程度。
  • 响应性:WebFlux 更适合做到背压和流式数据处理,可以更好地支持响应式系统的需求。

总结来说,Spring WebFlux 和 Spring MVC 提供了不同的编程模型来适应不同的应用场景。WebFlux 响应式编程模型适合处理大量并发请求和实时交互,而传统的 Spring MVC 更适合标准的、阻塞式的 Web 应用开发。选择使用哪个,取决于你的具体需求和开发团队的技术偏好。

非阻塞式 I/O 操作

非阻塞式 I/O 操作:使用响应式编程模型,即使操作是长时间运行的,也不会阻塞线程。而是在操作可以继续进行时,以回调的方式恢复执行。

这个特点。

下面是一个简单的 Spring WebFlux 的示例,展示了如何创建一个非阻塞的 RESTful 接口,该接口模拟了一个长时间运行的操作,比如查询数据库,但在这个示例中我们将使用 Mono.delay 来模拟这个耗时操作。

请注意,此代码需要在 Spring WebFlux 项目中运行,且 Spring Boot 版本应支持 WebFlux。

首先,我们需要一个模型,这里我们使用一个非常简单的 Greeting 类:

public class Greeting {
    private String message;

    public Greeting(String message) {
        this.message = message;
    }

    // getters and setters
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

接着,我们创建一个处理器(handler)来处理 HTTP 请求:

@Component
public class GreetingHandler {

    public Mono<ServerResponse> hello(ServerRequest request) {
        // Mono.delay(Duration.ofSeconds(3)) 模拟一个长时间运行的操作
        Mono<Greeting> greetingMono = Mono.delay(Duration.ofSeconds(3))
                                           .map(l -> new Greeting("Hello, Spring WebFlux!"));

        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
                             .body(greetingMono, Greeting.class);
    }
}

现在,我们需要定义路由来关联 URL 路径和处理器:

@Configuration
public class GreetingRouter {

    @Bean
    public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/hello"), greetingHandler::hello);
    }
}

最后,这是 SpringWebFluxDemoApplication 的主类,用以启动 Spring Boot 应用:

@SpringBootApplication
public class SpringWebFluxDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringWebFluxDemoApplication.class, args);
    }
}

要测试非阻塞行为,请在浏览器或使用 curl 访问 http://localhost:8080/hello

在我们的 GreetingHandler 中,Mono.delay(Duration.ofSeconds(3)) 模拟了一个长时间运行的 I/O 操作,这个操作在 3 秒后完成。这里的关键是,Mono.delay 创建了一个非阻塞的延迟操作,这意味着在这 3 秒内,执行这个操作的线程不会被阻塞,它可以去做其他的事情。

要测试非阻塞行为,请在浏览器或使用 curl 访问 http://localhost:8080/hello。即便我们在 GreetingHandler 中模拟了一个 3 秒的延迟,你会发现服务仍然可以接收和处理其他的请求。如果你并行地从多个终端或浏览器窗口发送请求,你会看到它们几乎是同时响应的,而不是一个接一个地阻塞等待。

为了完整起见,以下是使用 curl 命令行测试的方式:

curl http://localhost:8080/hello

如果你从多个终端窗口同时发出多个请求,你应该会发现响应时间几乎相同,这说明服务器没有因为模拟的延迟操作而阻塞其他请求。

这个简单的 demo 展示了 WebFlux 如何处理长时间运行的操作。在实际的生产环境中,类似的非阻塞操作可能包括数据库查询、远程服务调用等,这些操作可以利用响应式编程模型完成,而不会导致线程阻塞,从而提高了应用程序的性能和伸缩性。

真实场景

那真实的业务场景一般是怎么设计的?也是用 Mono.delay 吗?可以给我一个简答的真实场景的示例方案吗?比如一个数据查询接口,当收到请求后需要去大数据查询数据。查询大数据是比较耗时的。

当然可以。在许多微服务架构或分布式系统中,经常需要通过 HTTP 请求从其他服务或平台获取数据。如果你想在响应式编程模型中保持非阻塞式调用,你可以使用 Spring WebFlux 的 WebClient,这是一个响应式的 HTTP 客户端,用于执行 HTTP 请求并处理响应。

下面是如何使用 Spring WebFlux 的 WebClient 从另一个服务获取数据的示例:

@Service
public class BigDataService {

    private final WebClient webClient;

    public BigDataService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("http://bigdata-service").build();
    }

    public Mono<BigDataResponse> queryBigData(String queryParam) {
        return webClient.get()
                .uri(uriBuilder -> uriBuilder.path("/query")
                        .queryParam("param", queryParam)
                        .build())
                .retrieve()
                .bodyToMono(BigDataResponse.class);
    }
}

在这个服务中,我们创建了一个 WebClient 实例,它被配置为指向大数据平台的服务。queryBigData 方法通过 HTTP GET 请求发送查询参数,非阻塞地从大数据平台请求数据。返回的对象 Mono<BigDataResponse> 是一个响应式的单值流,它将在异步操作完成时发出一个 BigDataResponse 对象。

然后你可以在控制层调用这个服务:

@RestController
@RequestMapping("/data")
public class DataController {

    private final BigDataService bigDataService;

    @Autowired
    public DataController(BigDataService bigDataService) {
        this.bigDataService = bigDataService;
    }

    @GetMapping("/query")
    public Mono<ResponseEntity<BigDataResponse>> queryData(@RequestParam String param) {
        return bigDataService.queryBigData(param)
                .map(response -> ResponseEntity.ok(response))
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }
}

这里,DataController 调用 BigDataServicequeryBigData 方法,并将返回的 Mono 直接作为响应返回。Spring 框架会处理这个 Mono,当大数据平台返回结果时,将其序列化并发送到客户端。

使用 WebClientSpring WebFlux 保证了整个 HTTP 调用过程是非阻塞的,即使是在等待远程大数据平台的响应时,也不会占用线程资源,这使得应用能够高效处理其他并发请求。