4.1 注解路由

Spring WebFlux 注解路由详解:@RestController、@GetMapping/@PostMapping 等请求映射、路径变量与查询参数绑定、请求头抽取、Mono/Flux 返回值类型自动适配与 Content-Type 协商。

最小 WebFlux 应用

Spring WebFlux 需要引入 spring-webflux:

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

最小启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

Spring Boot WebFlux 默认使用 Netty 作为底层服务器(不是 Tomcat)。这意味着第 1–3 章讲的所有 Reactor Netty 概念(EventLoop、背压、写水位线)都在 WebFlux 底层生效。

@RestController 与路由映射

import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public Flux<User> listUsers() {
        return Flux.just(
            new User(1L, "alice"),
            new User(2L, "bob")
        );
    }

    @GetMapping("/{id}")
    public Mono<User> getUser(@PathVariable Long id) {
        return Mono.just(new User(id, "user-" + id));
    }

    @PostMapping
    public Mono<User> createUser(@RequestBody Mono<User> user) {
        return user.map(u -> new User(System.currentTimeMillis(), u.name()));
    }

    @DeleteMapping("/{id}")
    public Mono<Void> deleteUser(@PathVariable Long id) {
        System.out.println("Deleting user " + id);
        return Mono.empty();
    }
}

注意返回值类型:

  • Mono<T> —— 0 或 1 个元素的异步结果
  • Flux<T> —— 0 到 N 个元素的异步流
  • Mono<Void> —— 只有完成信号,没有数据

Spring 会自动把 User 对象序列化为 JSON(需要引入 jackson)。

路径变量与查询参数

@GetMapping("/search")
public Flux<User> search(
    @RequestParam(required = false) String name,
    @RequestParam(defaultValue = "10") int limit,
    @RequestParam(required = false) List<String> tags   // ?tags=java&tags=spring
) {
    return Flux.just(new User(1L, name != null ? name : "default"));
}
注解来源示例
@PathVariableURL 路径段/users/{id} → @PathVariable Long id
@RequestParam查询参数?name=abc → @RequestParam String name
@RequestHeader请求头X-Token: abc → @RequestHeader("X-Token") String token
@CookieValueCookiesession=abc → @CookieValue("session") String session

请求体:Mono vs 直接对象

WebFlux 中 @RequestBody 可以绑定为 Mono<T> 或 Flux<T>:

// 方式 A:Mono(惰性处理)
@PostMapping
public Mono<User> create(@RequestBody Mono<User> userMono) {
    return userMono.flatMap(u -> validate(u))
        .flatMap(u -> saveToDb(u));
}

// 方式 B:直接对象(Spring 先异步解码,再调用方法)
@PostMapping("/sync")
public Mono<User> createSync(@RequestBody User user) {
    // 方法被调用时 user 已经是完整对象;解码过程本身不阻塞线程
    return saveToDb(user);
}

两种方式都不会「阻塞聚合」:WebFlux 用响应式解码器把请求体读完,再调用 handler,期间不占用一个线程等待 I/O。区别在时机和形态——方式 A 把解码推迟到订阅 Mono 时,便于在解码后、业务前插入算子;方式 B 在进入方法前就已经解码完成,写法更直接,但拿不到解码阶段的控制点。都是合法用法,不要因为「响应式」就一律拒绝方式 B。

自定义 MediaType

@GetMapping(value = "/report", produces = MediaType.TEXT_PLAIN_VALUE)
public Mono<String> report() {
    return Mono.just("Sales report content");
}

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<String> upload(@RequestPart("file") FilePart filePart) {
    return filePart.content()
        .map(dataBuffer -> {
            try {
                return dataBuffer.readableByteCount();
            } finally {
                // FilePart 的 DataBuffer 由调用方负责释放,漏掉会泄漏池化内存
                DataBufferUtils.release(dataBuffer);
            }
        })
        .reduce(0, Integer::sum)
        .map(size -> "Uploaded " + size + " bytes");
}

produces 控制响应 Content-Type,consumes 控制接受的请求 Content-Type。

与 Reactor Netty 的关系

WebFlux 注解路由建立在 Reactor Netty 之上:

@RestController @GetMapping
    ↓ Spring DispatcherHandler
        ↓ RequestMappingHandlerMapping(匹配路由)
            ↓ InvocableHandlerMethod(调用方法)
                ↓ Mono/Flux 返回值
                    ↓ ReactorHttpHandlerAdapter
                        ↓ Reactor Netty HttpServer

所以第 1 章的背压、第 2 章的连接池、第 3 章的 WebSocket,在 WebFlux 应用中同样生效。区别只是 Spring 帮你封装了底层的 handle() 回调。

错误处理缺省行为

如果 controller 方法抛出异常,WebFlux 默认返回 500 和一个简单的错误 JSON:

{
  "timestamp": "2026-11-05T06:12:00.000+00:00",
  "path": "/api/users/999",
  "status": 500,
  "error": "Internal Server Error",
  "requestId": "a1b2c3d4-5"
}

默认错误体包含 timestamp、path、status、error 和 requestId。注意 requestId 来自 ServerHttpRequest.getId(),是 WebFlux 为每个请求生成的内部 ID,不是业务层透传的 X-Request-ID;要把它们对齐得自己在过滤器里写入并覆盖错误属性。message 默认不会出现(server.error.include-message=never 的安全默认),只有开启后才会返回异常消息。第 5.3 节会讲如何自定义这个错误体并接入业务请求 ID。

小结

概念注解/用法
REST Controller@RestController + @RequestMapping
路由映射@GetMapping、@PostMapping 等
路径参数@PathVariable
查询参数@RequestParam(支持 List、默认值)
请求头@RequestHeader
请求体@RequestBody Mono<T> 或 @RequestBody Flux<T>
响应类型produces = MediaType...
文件上传@RequestPart FilePart

下一节 4.2 函数式路由 :RouterFunctions、RequestPredicates、与注解路由的对照和选择。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南