tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 探索SpringBoot

  • 常用功能实现

    • SpringBoot常用功能实现_1
    • SpringBoot中拦截器与日志
    • 多环境配置与数据绑定
    • 异步方法(线程池)的实现
    • controller参数接收
      • Controller层的接口
        • Get请求的参数
        • `@RequestParam`
        • `@PathVariable`
        • 直接使用方法参数
        • `ttpServletRequest`
        • `Map` 接收所有参数
        • Object 对象来接收参数
        • Post请求的参数
        • `@RequestParam`
        • `@RequestBody`
        • 实体类直接接收
        • `Map` 接收请求体
        • 文件上传 `@RequestParam + MultipartFile`
        • 文件上传的获取【扩展】
        • `@RequestParam` 的设计目的
        • Spring 如何处理文件上传
        • 多文件上传处理
        • 几个问题
        • Get的Body为什么不能放参数?
        • Post的url可以携带参数吗?
        • 疑问【@RequestParam】
        • @RequestParam的局限
      • Spring中对流数据的处理
        • 收上传的视频流(通过 HTTP 的 `multipart/form-data`)
        • 接收实时流数据(如视频流、传感器数据)
        • 实现步骤
        • 使用分块传输编码(Chunked Transfer Encoding)
        • 实现步骤
        • Chunked Transfer Encoding的作用
        • 使用场景
        • 跟数据流之类的关系
        • Spring WebFlux 的响应式编程模型
        • 以下是它跟传统Web框架的区别:
        • 核心组件对比
    • SpringBoot中关于日志的更好使用
    • 异常捕获的一些细节
    • 时间跟其他数据的序列化
    • 面向切面跟自定义注解的结合
  • Security认证授权

  • 扩展

  • 实战与注意事项

  • 其它

  • 《SpringBoot》笔记
  • 常用功能实现
EffectTang
2025-04-24
目录

controller参数接收

# Controller参数接收

# Controller层的接口

在 SpringBoot 的 @Controller 层中,GET 请求和 POST 请求的参数接收方式有所不同,主要区别在于参数的位置和传递方式。

# Get请求的参数

GET 请求的参数通常通过 URL 的查询参数(Query Parameters) 或 路径变量(Path Variables) 传递。

其中 @RequestParam 和 @PathVariable 最为常用

# @RequestParam

  • 作用:从 URL 的查询参数中获取单个或多个参数。
  • 适用场景:获取 URL 中 ?key=value 格式的参数。
@GetMapping("/query")
public String getParam(
    @RequestParam String name,          // 必填参数
    @RequestParam(required = false) Integer age,  // 可选参数
    @RequestParam(defaultValue = "0") int score   // 默认值
) {
    return "Name: " + name + ", Age: " + age + ", Score: " + score;
}
1
2
3
4
5
6
7
8

# @PathVariable

  • 作用:从 URL 的路径变量中获取参数。
  • 适用场景:RESTful 风格的 URL 路径参数(如 /user/{id})。
@GetMapping("/user/{id}")
public String pathVariable(
    @PathVariable Long id,          // 路径变量
    @PathVariable(name = "id") String userId  // 显式指定变量名
) {
    return "User ID: " + id + ", User String ID: " + userId;
}
1
2
3
4
5
6
7

# 直接使用方法参数

  • 作用:Spring 会自动将 URL 中的查询参数绑定到方法参数。
@GetMapping("/auto-bind")
public String autoBind(String name, Integer age) {
    return "Name: " + name + ", Age: " + age;
}
1
2
3
4
  • 请求示例:/auto-bind?name=Alice&age=25 输出:Name: Alice, Age: 25

# ttpServletRequest

  • 作用:通过 request.getParameter() 获取参数。
@GetMapping("/request")
public String viaRequest(HttpServletRequest request) {
    String name = request.getParameter("name");
    return "Name: " + name;
}
1
2
3
4
5

# Map 接收所有参数

  • 作用:将所有查询参数存入 Map 对象。
@GetMapping("/params-map")
public String viaMap(@RequestParam Map<String, Object> params) {
    return "Name: " + params.get("name") + ", Age: " + params.get("age");
}
1
2
3
4

# Object 对象来接收参数

当然在实际开发中,还有一种常用的数据接收方式,方法参数中为 自定义的实体类。比如:

@RestController
public class OrderController {

    @GetMapping("/orderInfoList")
    public Result<?> InfoList(FOrderInfo vo) {
        return Result.success(vo);
    }
}
1
2
3
4
5
6
7
8

在 Spring Boot 中,如果你在 @GetMapping 的 Controller 方法中使用一个自定义对象(如 FOrderInfo vo)作为参数,Spring 会自动将请求中的参数按照 字段名称匹配 的方式映射到这个对象的属性上。

public class FOrderInfo {
    private String orderId;
    private Integer status;
    
    // getters and setters
}
1
2
3
4
5
6

如果请求如下:

GET /orderInfoList?orderId=12345&status=1
1

Spring Boot 会自动创建一个 FOrderInfo 的实例,并将:

  • orderId 映射为 "12345"
  • status 映射为 1

注意:这种方式,字段名称必须匹配才行。

  • URL 参数名要与实体类的 getter/setter 对应的属性名一致。
  • 比如:想要映射 userName 字段,URL 参数也应该是 userName=xxx。

使用该方式,还可能存在类型转换问题:

  • Spring 会尝试自动进行基础类型的转换(如 String -> Integer 等),如果失败可能会抛出异常或绑定失败(可通过 @Valid 或 @RequestParam 做额外校验)。

# Post请求的参数

POST 请求的参数通常通过 请求体(Request Body) 或 表单数据(Form Data) 传递。

post类型的请求存在数据的地方可比Get多,所以内容的形式也比Get多,主要有以下几种:

  • url
  • Body

# @RequestParam

  • 作用:接收表单数据(Content-Type: application/x-www-form-urlencoded)。
  • 适用场景:表单提交或简单键值对数据。
@PostMapping("/post-form")
public String postForm(
    @RequestParam String username,
    @RequestParam("pwd") String password
) {
    return "Username: " + username + ", Password: " + password;
}
1
2
3
4
5
6
7

表单提交实例

<form action="/post-form" method="post">
    <input type="text" name="username" />
    <input type="password" name="pwd" />
    <input type="submit" />
</form>
1
2
3
4
5

# @RequestBody

  • 作用:接收 JSON 格式的请求体(Content-Type: application/json)。(也只有它能处理json请求)
  • 适用场景:复杂数据结构(如对象、数组)。
@PostMapping("/post-json")
public String postJson(@RequestBody User user) {
    return "Name: " + user.getName() + ", Age: " + user.getAge();
}
1
2
3
4

请求体

{
    "name": "Bob",
    "age": 30
}
1
2
3
4

# 实体类直接接收

  • 作用:将表单数据绑定到实体类。
@PostMapping("/post-entity")
public String postEntity(User user) {
    return "Name: " + user.getName() + ", Age: " + user.getAge();
}


@PostMapping("/post-entity")
public String postEntity(ModelAttribute User user) {
    return "Name: " + user.getName() + ", Age: " + user.getAge();
}
1
2
3
4
5
6
7
8
9
10

表单提交时,字段名需与实体类属性一致。

如果发现无法绑定,则加上 @ModelAttribute

# Map 接收请求体

  • 作用:将 JSON 或表单数据存入 Map 对象。
@PostMapping("/post-map")
public String postMap(@RequestBody Map<String, Object> params) {
    return "Name: " + params.get("name") + ", Age: " + params.get("age");
}
1
2
3
4

# 文件上传 @RequestParam + MultipartFile

  • 作用:接收文件上传的参数。
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
    return "File name: " + file.getOriginalFilename();
}
1
2
3
4

表单需设置 enctype="multipart/form-data"

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" />
</form>
1
2
3
4

# 文件上传的获取【扩展】

当表单中包含文件字段时,必须设置表单的 enctype="multipart/form-data"。

这种格式的请求体被分割为多个 part(部分),每个 part 可以是表单字段或文件内容。

请求示例

-----------------------------3141592653589793238
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

(文件内容)
-----------------------------3141592653589793238
Content-Disposition: form-data; name="description"

这是一个测试文件
-----------------------------3141592653589793238--
1
2
3
4
5
6
7
8
9
10

每个 part 通过边界符(boundary)分隔,包含元数据(如文件名、类型)和实际内容。

# @RequestParam 的设计目的
  • @RequestParam 的核心作用:
    • 用于绑定 表单字段 或 文件 到方法参数。
    • 它可以处理 简单键值对(如 application/x-www-form-urlencoded)或 multipart 请求中的文件/字段。
    • 对于文件上传,@RequestParam 会绑定到 MultipartFile 对象,该对象封装了文件的元数据和内容。
  • 为什么不用 @RequestBody?
    • @RequestBody 期望请求体是一个 完整的对象(如 JSON 或 XML),并需要将其反序列化为 Java 对象。
    • multipart/form-data 的结构不符合 @RequestBody 的预期:
      • 请求体是多个独立的 part,而非一个整体对象。
      • Spring 没有内置的机制将 multipart/form-data 反序列化为对象,因此无法使用 @RequestBody。

# Spring 如何处理文件上传

(1) 核心组件:MultipartResolver

  • 作用:
    • Spring 通过 MultipartResolver 解析 multipart/form-data 请求。
    • 将请求体中的各个 part 解析为 MultipartFile 对象,并封装到 MultipartHttpServletRequest 中。
  • 默认实现:
    • Spring Boot 内置了 StandardServletMultipartResolver,基于 Servlet 3.0 的 API 实现。

(2) 方法参数绑定

  • 通过 @RequestParam 绑定文件:
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) {
    // 处理文件
}
1
2
3
4

@RequestParam("file") 指定绑定到表单字段名为 file 的 part。

Spring 自动将对应的 MultipartFile 对象注入到方法参数中。

  • 根本原因:@RequestBody 期望请求体是一个 结构化的数据对象(如 JSON),而 multipart/form-data 是一种 分片数据格式,无法直接映射为对象。
# 多文件上传处理

通过 MultipartFile[] files 获取文件

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

@RestController
public class FileUploadController {

    // 从配置文件中读取文件保存路径
    @Value("${file.upload-dir}")
    private String uploadDir;

    @PostMapping("/upload-multiple")
    public String handleMultipleFiles(
            @RequestParam("files") MultipartFile[] files) { // 接收文件数组
        if (files == null || files.length == 0) {
            return "上传失败:没有选择文件!";
        }

        StringBuilder successMessage = new StringBuilder();

        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                continue; // 跳过空文件
            }

            try {
                // 生成唯一文件名(避免重复)
                String originalFilename = file.getOriginalFilename();
                String fileName = System.currentTimeMillis() + "_" + originalFilename;

                // 保存路径
                Path uploadPath = Path.of(uploadDir).toAbsolutePath().normalize();
                Path targetLocation = uploadPath.resolve(fileName);

                // 创建目录(如果不存在)
                if (!Files.exists(uploadPath)) {
                    Files.createDirectories(uploadPath);
                }

                // 保存文件
                Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
                successMessage.append("文件 ").append(fileName).append(" 上传成功!\n");
            } catch (IOException e) {
                return "上传失败:" + e.getMessage();
            }
        }

        return "所有文件上传成功!\n" + successMessage.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

在处理文件上传时,请记得在 application.properties 中配置文件上传目录和大小限制:

# 文件保存路径(绝对路径或相对路径)
file.upload-dir=uploads

# 配置文件上传大小限制(例如:每个文件最大10MB,总请求最大50MB)
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
1
2
3
4
5
6

注意:关于,@RequestParam("files") MultipartFile[] files,参数,有下面2点要注意

  • name="files" 必须与前端表单的 input 标签的 name 属性一致。
  • Spring 会自动将多个文件封装为 MultipartFile 数组。

# 几个问题

# Get的Body为什么不能放参数?

根据 HTTP 协议的官方定义(RFC 7231):

  • GET 请求的语义:GET 方法的目的是获取资源,而不是修改或提交数据。协议明确规定,GET 请求的参数应通过 URL 的查询字符串(Query String) 传递,而请求体(Body)通常被忽略。
  • 服务器的处理方式:大多数服务器(如 Apache、Nginx、Tomcat)在接收到 GET 请求时,会忽略请求体中的内容,即使客户端发送了 Body。因此,依赖 Body 传递参数可能导致数据丢失。(规范虽然未明确禁止GET携带Body,但指出Body对GET请求没有语义意义,因此服务器常常选择忽略它。)

技术限制:

  • URL 的长度限制:
    • GET 的参数通过 URL 传递,而 URL 的长度在浏览器和服务器中是有限制的(通常 2KB 到 8KB)。如果参数过多或过大,可能导致 URL 超长,引发错误。
    • Body 没有理论上的长度限制(取决于服务器配置),但 GET 的参数不适合放在 Body 中,因为协议设计不支持(Get的目的是为了获取资源)。
  • 客户端和服务器的兼容性:
    • 大部分客户端(如浏览器、HTTP 客户端库)在发送 GET 请求时,默认不会发送 Body,即使尝试发送也可能被拦截或忽略。
    • 服务器端框架(如 Spring、Express)通常会直接忽略 GET 请求的 Body,或抛出异常(如某些框架明确禁止 GET 的 Body)。

# Post的url可以携带参数吗?

可以的。

POST 请求的 URL 可以携带参数,这些参数通常以 查询参数(Query Parameters) 的形式附加在 URL 的末尾(例如 ?key=value)。同时,POST 请求的参数也可以放在 请求体(Request Body) 中,比如:

POST /api/data?sort=desc&page=2 HTTP/1.1
Content-Type: application/json
1
2
  • 获取 URL 中的查询参数

使用 @RequestParam 注解或直接通过方法参数绑定。

@PostMapping("/api/data")
public String handlePostRequest(
    @RequestParam String sort,          // 必填参数
    @RequestParam(required = false) Integer page,  // 可选参数
    @RequestParam(defaultValue = "0") int limit    // 默认值
) {
    return "Sort: " + sort + ", Page: " + page + ", Limit: " + limit;
}
1
2
3
4
5
6
7
8

请求示例

POST /api/data?sort=desc&page=2&limit=10 HTTP/1.1
Content-Type: application/json
1
2
  • 获取请求体(Body)中的参数

使用 @RequestBody 注解接收 JSON 数据,或通过表单数据绑定。

@PostMapping("/api/data")
public String handlePostRequest(
    @RequestBody User user      // 接收 JSON 数据
) {
    return "Name: " + user.getName() + ", Age: " + user.getAge();
}
1
2
3
4
5
6

请求示例

POST /api/data HTTP/1.1
Content-Type: application/json

{
    "name": "Alice",
    "age": 30
}
1
2
3
4
5
6
7
  • 同时获取 URL 参数和请求体参数
@PostMapping("/api/data")
public String handlePostRequest(
    @RequestParam String sort,    // URL 参数
    @RequestBody User user       // 请求体参数
) {
    return "Sort: " + sort + ", User Name: " + user.getName();
}
1
2
3
4
5
6
7

请求示例

POST /api/data?sort=asc HTTP/1.1
Content-Type: application/json

{
    "name": "Bob",
    "age": 25
}
1
2
3
4
5
6
7

# 疑问【@RequestParam】

简单表单提交不是application/x-www-form-urlencoded吗,它的参数是存放在Body中的呀,不应该是使用@RequestBody获取吗?前文Post关于@RequestParam的参数也说,它是用于获取简单表单提交的参数。

怎么回事呢?

表单数据(x-www-form-urlencoded)的处理方式

  1. 表单数据的 Content-Type:
    • 表单提交的默认 Content-Type 是 application/x-www-form-urlencoded,数据格式为键值对(如 username=admin&password=123),存储在请求体(Body)中。
    • Spring 对这类数据的处理是自动的,即使不使用 @RequestBody,而通过 @RequestParam 或直接方法参数绑定也能获取

# @RequestParam的局限

使用@RequestParam,也是可以获取请求体中的参数的,但仅限于表单形式

所以总结下,如果请求内容形式为:application/x-www-form-urlencoded时,

@RequestBody和 @RequestParam 可以都可以获取对应参数

如果,请求中url中有参数,且Body中也有参数,并且参数名称都能对上,那**@RequestParam**2个地方的值都会获取到。

【注意】

如果请求body中的content-Type形式为 Application/json,那么无法使用**@RequestParam**

只能使用@RequestBody了。

@RequestBody 的作用:

  • 用于接收 非表单格式 的请求体数据(如 JSON、XML),需要明确指定 Content-Type(如 application/json)。
  • 表单数据(x-www-form-urlencoded)的格式与 JSON 不同,Spring 默认会将其解析为键值对,而非对象。

# Spring中对流数据的处理

使用@RequestBody接收流数据可能会有问题,因为默认情况下它会一次性读取整个请求体,导致内存溢出,特别是处理大文件或持续数据流的时候。如果说用户需要处理持续的数据流,比如视频流,这时候又该怎么办呢?该采取什么办法呢。

以上问题,市面上都有一套成熟的解决方案了,在处理流数据(如视频流、实时数据流等)时,你可以需要根据具体场景选择合适的技术方案。

以下是针对不同场景的实现方法和示例代码:

# 收上传的视频流(通过 HTTP 的 multipart/form-data)

适用于客户端上传视频文件的场景,使用 MultipartFile 处理。

对应后端代码示例:

@PostMapping("/upload-video")
public String handleVideoUpload(@RequestParam("video") MultipartFile video) {
    if (video.isEmpty()) {
        return "上传失败:视频为空!";
    }
    try {
        // 保存视频到指定路径
        Path targetPath = Paths.get("uploads/" + video.getOriginalFilename());
        video.transferTo(targetPath);
        return "视频上传成功!";
    } catch (IOException e) {
        return "上传失败:" + e.getMessage();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里的就是上文中提到的 @RequestParam和MultipartFile 。

# 接收实时流数据(如视频流、传感器数据)

适用于需要持续接收数据流的场景,如实时监控、日志流等。推荐使用 Spring WebFlux 的响应式编程模型。

# 实现步骤

  1. 使用 Flux 接收流式数据:
    • 通过 ServerSentEvent(SSE)或 WebSocket 实现流式传输。
    • 示例:接收客户端发送的分块数据流。
@RestController
public class StreamController {

    @PostMapping("/stream")
    public Mono<String> handleStreamData(ServerWebExchange exchange) {
        Flux<DataBuffer> flux = exchange.getRequest().getBody();
        return flux
                .map(buffer -> {
                    // 逐块处理数据(如视频流的分片)
                    byte[] bytes = new byte[buffer.readableByteCount()];
                    buffer.read(bytes);
                    // 处理逻辑(如解码、存储)
                    return new String(bytes);
                })
                .doOnNext(data -> System.out.println("Received chunk: " + data))
                .then(Mono.just("数据流处理完成!"));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 使用分块传输编码(Chunked Transfer Encoding)

通过 HTTP 头 Transfer-Encoding: chunked 实现服务器推送流数据。

# 实现步骤

  1. 服务端推送流数据:
@GetMapping("/stream-data")
public Flux<String> streamData() {
    return Flux.interval(Duration.ofSeconds(1))
                .map(i -> "实时数据块 " + i)
                .doOnNext(data -> System.out.println("发送数据:" + data));
}
1
2
3
4
5
6
  1. 客户端接收(如使用 WebClient):
WebClient.create("http://localhost:8080")
         .get()
         .uri("/stream-data")
         .retrieve()
         .bodyToFlux(String.class)
         .subscribe(data -> System.out.println("接收到:" + data));
1
2
3
4
5
6

# Chunked Transfer Encoding的作用

在 HTTP 协议中,Transfer-Encoding: chunked 是一种用于分块传输数据的编码方式,它允许服务器或客户端将数据划分为多个“块”(chunks)逐步传输,而无需预先知道完整数据的长度。以下是其核心含义、工作原理和典型应用场景的详细说明:

  1. 动态生成内容 当服务器无法提前确定响应内容的完整长度(例如实时生成的流数据、动态计算结果或大文件分块传输)时,使用 chunked 编码可以边生成边传输,避免等待所有数据就绪。
  2. 流式传输支持 适用于需要实时推送数据的场景(如视频流、日志实时输出、聊天消息等),客户端可以逐步接收并处理数据,降低延迟。
  3. 替代 Content-Length 头 当无法预先计算 Content-Length(如数据长度未知或计算成本高)时,chunked 编码允许省略该头部,直接分块传输。

传输流程

  1. 服务器按需生成数据块,逐个发送给客户端。
  2. 每个块以十六进制长度值开头,后跟 \r\n,然后是实际数据。
  3. 最后一个块为长度 0 的空块,表示传输结束。
# 使用场景

1.服务器推送流数据

  • 视频/音频流:服务端持续发送媒体数据块,客户端逐步播放。
  • 实时日志:动态生成的日志内容通过分块传输实时展示在客户端。
  • Server-Sent Events (SSE):长连接中服务器持续推送事件数据。
  1. 大文件上传/下载
  • 客户端上传大文件时,可将文件分块发送(请求头中设置 Transfer-Encoding: chunked)。
  • 服务端下载大文件时,分块返回数据(响应头中设置 Transfer-Encoding: chunked)。
  1. 动态内容生成
  • 如实时计算、数据库查询结果流式返回,无需等待所有结果就绪。
特性 Transfer-Encoding: chunked Content-Length
数据长度 动态分块传输,无需提前知道总长度 需预先计算完整数据长度
适用场景 流数据、动态生成内容、大文件传输 静态资源、已知长度的数据
内存占用 低(逐块处理) 高(需缓存完整数据)
协议版本 HTTP/1.1 及以上支持 HTTP/1.0 和 1.1 均支持
# 跟数据流之类的关系

常见的视频流传输确实会利用类似 分块传输 的逻辑,但具体实现方式可能与 Transfer-Encoding: chunked 略有不同。

视频流传输通常依赖以下两种主流技术: 基于 HTTP 的自适应流(HLS、DASH)

当然还有其他技术,欢迎大家自行查阅

传输特点:

  • 视频被预先分片,存储在服务器或 CDN 中。
  • 客户端通过 HTTP 短连接 按需请求分片文件(如 video_1.ts, video_2.ts)。
  • 与 Transfer-Encoding: chunked 的区别:
    • 分片是独立的文件,而非动态分块传输。
    • 每个分片有明确的 Content-Length,不依赖 chunked 编码。

# Spring WebFlux 的响应式编程模型

Spring MVC(传统Web框架)和Spring WebFlux 都是Spring Boot中的Web编程框架,但它们的设计理念、底层架构和适用场景有显著区别。

简单一句话,它也是一种web框架,但它是非阻塞的。

Spring WebFlux 是 Spring 5 引入的 非阻塞、响应式 Web 框架,基于 Reactor(实现了 Reactive Streams 规范)构建。其核心特点包括:

  • 非阻塞 I/O:通过异步事件驱动,避免线程阻塞,提升高并发场景下的性能。
  • 背压支持:消费者可以控制生产者的数据流速度,防止内存溢出。
  • 函数式编程:支持通过 RouterFunction 和 HandlerFunction 定义路由和处理器。

在 Spring WebFlux 中,响应式编程通过以下核心组件实现:

  • Flux<T>:表示 0到N个元素 的异步数据流,适用于批量数据。
  • Mono<T>:表示 0或1个元素 的异步数据流,适用于单一结果。

SSE(Server-Sent Events) 是一种 单向实时通信协议,允许服务器主动向客户端推送数据流。其特点包括:

  • 单向通信:数据仅从服务器流向客户端。
  • 基于 HTTP:使用标准 HTTP 连接,无需额外协议。
  • 自动重连:客户端断开后会自动重连(可配置重试间隔)。
  • 事件流格式:数据以 text/event-stream 格式传输,每条消息以 data: 开头。

以下是一个通过 Spring WebFlux 实现 SSE 的完整示例:

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
1
2
3
4

使用 Flux 和 ServerSentEvent 发送实时数据流:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;

@RestController
public class SSEController {

    // 使用 Sinks 发布实时数据(模拟传感器数据)
    private final Sinks.Many<Integer> sensorDataSink = Sinks.many().multicast().onBackpressureBuffer();

    @GetMapping("/sse")
    public Flux<ServerSentEvent<Integer>> streamSensorData() {
        return sensorDataSink.asFlux()
                .map(data -> ServerSentEvent.<Integer>builder()
                        .id(String.valueOf(System.currentTimeMillis()))
                        .event("sensor-data")
                        .data(data)
                        .build())
                .onBackpressureDrop(); // 处理背压(丢弃旧数据)
    }

    // 模拟传感器数据生成(每秒发送一个随机数)
    public void generateData() {
        Flux.interval(Duration.ofSeconds(1))
            .map(i -> (int) (Math.random() * 100))
            .subscribe(sensorDataSink::tryEmitNext);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

客户端,使用JavaScript实现

使用 EventSource 连接 SSE 端点:

const eventSource = new EventSource("http://localhost:8080/sse");

eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log("Received data:", data);
};

eventSource.onerror = function(error) {
    console.error("SSE 连接错误:", error);
};

eventSource.addEventListener("open", function() {
    console.log("连接已建立");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 以下是它跟传统Web框架的区别:
特性 Spring MVC Spring WebFlux
基础模型 阻塞式、同步模型(基于Servlet规范) 非阻塞、异步、响应式模型(基于Reactive Streams)
线程模型 每个请求分配一个线程,依赖线程池 事件驱动,少量线程处理所有请求(如Netty)
返回类型 同步对象(如String、ResponseEntity) 响应式类型(Mono或Flux)
I/O操作 阻塞式I/O(如传统数据库操作) 非阻塞式I/O(需配合响应式数据源,如R2DBC)

核心设计目标

  • Spring MVC:
    • 适用场景:传统Web应用、轻量级异步需求、与阻塞式组件(如JDBC、传统数据库)集成。
    • 优势:简单易用,生态成熟,对开发者的学习曲线低。
    • 局限:高并发下因线程阻塞可能导致性能瓶颈。
  • Spring WebFlux:
    • 适用场景:高并发、低延迟场景(如实时监控、流式处理、WebSocket)、微服务架构。
    • 优势:非阻塞模型支持更高吞吐量,响应式流处理(如SSE、大文件上传)。
    • 局限:需要熟悉响应式编程,部分传统组件(如JDBC)需适配响应式接口。

注意:

它们两个不能共存:

Spring Boot项目中不能同时使用Spring MVC和WebFlux

,因为它们的核心架构和依赖冲突。

  • 如果引入spring-boot-starter-web,默认启用Spring MVC。
  • 如果引入spring-boot-starter-webflux,则启用WebFlux。

# 核心组件对比

组件 Spring MVC Spring WebFlux
前端控制器 DispatcherServlet(Servlet规范) DispatcherHandler(非Servlet)
请求/响应对象 ServletRequest/ServletResponse ServerWebExchange(包含请求/响应)
处理器 @Controller(基于类) @Controller或函数式HandlerFunction
过滤器 Filter(Servlet过滤器) WebFilter(响应式过滤器)
异常处理 @ControllerAdvice + @ExceptionHandler 统一异常处理链(ErrorWebExceptionHandler)
上次更新: 2025/05/21, 15:29:11
异步方法(线程池)的实现
SpringBoot中关于日志的更好使用

← 异步方法(线程池)的实现 SpringBoot中关于日志的更好使用→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式