附录 C:WebFlux 路由模板

Spring WebFlux 注解路由与函数式路由模板对照手册:Controller 方法签名与路径参数绑定、RouterFunctions 构建器 API、WebFilter Ordered 优先级顺序、SSE ServerSentEvent/文件上传 FilePart/全局异常处理代码片段速查。

注解路由模板

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    public Flux<Order> list(
            @RequestParam(required = false) String status,
            @RequestParam(defaultValue = "10") int limit) {
        return service.findByStatus(status, PageRequest.of(0, limit));
    }

    @GetMapping("/{id}")
    public Mono<Order> get(@PathVariable Long id) {
        return service.findById(id)
            .switchIfEmpty(Mono.error(new NotFoundException(id)));
    }

    @PostMapping
    public Mono<ResponseEntity<Order>> create(
            // @Valid 必须写在参数上:WebFlux 从参数注解提取校验提示,
            // 写成 Mono<@Valid OrderRequest> 这种类型注解不会生效。
            // 接收 Mono<OrderRequest> 时,@Valid @RequestBody Mono<OrderRequest>
            // 同样会校验解码出来的元素,只是错误会在读取 body 时抛出。
            @Valid @RequestBody OrderRequest request) {
        return service.create(request)
            .map(order -> ResponseEntity
                .created(URI.create("/api/orders/" + order.id()))
                .body(order));
    }

    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<Void>> delete(@PathVariable Long id) {
        return service.delete(id)
            .then(Mono.just(ResponseEntity.noContent().build()));
    }
}

函数式路由模板

@Bean
public RouterFunction<ServerResponse> orderRoutes(OrderHandler handler) {
    return RouterFunctions.route()
        .nest(path("/api/orders"), builder -> builder
            .GET("", handler::list)
            .GET("/{id}", handler::get)
            .POST("", handler::create)
            .DELETE("/{id}", handler::delete)
        )
        .filter((req, next) -> {
            long start = System.nanoTime();
            return next.handle(req)
                .doFinally(sig -> {
                    long ms = (System.nanoTime() - start) / 1_000_000;
                    log.info("{} {} in {}ms", req.method(), req.path(), ms);
                });
        })
        .build();
}

SSE 模板

@GetMapping(value = "/sse/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> events() {
    return Flux.interval(Duration.ofSeconds(1))
        .map(i -> ServerSentEvent.<String>builder()
            .id(String.valueOf(i))
            .event("tick")
            .data("seq=" + i)
            .build());
}

文件上传模板

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<String> upload(@RequestPart("file") FilePart filePart) {
    // 不要直接拼接客户端传来的文件名:可能包含 ../ 或绝对路径
    String original = filePart.filename() == null ? "" : filePart.filename();
    Path fileName = Path.of(original).getFileName();
    String baseName = fileName == null ? "" : fileName.toString();
    String safeName = baseName.matches("[A-Za-z0-9._-]+") && !baseName.equals("..")
        ? baseName : "upload.bin";
    Path baseDir = Path.of("/tmp/uploads").toAbsolutePath().normalize();
    Path dest = baseDir.resolve(safeName).normalize();
    if (!dest.startsWith(baseDir)) {
        return Mono.error(new IllegalArgumentException("invalid filename"));
    }
    return filePart.transferTo(dest).then(Mono.just("Uploaded"));
}

错误处理模板

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleNotFound(NotFoundException ex) {
        return Mono.just(ResponseEntity.status(404)
            .body(new ErrorResponse("404", ex.getMessage(), 404, now())));
    }

    @ExceptionHandler(Exception.class)
    public Mono<ResponseEntity<ErrorResponse>> handleGeneric(Exception ex) {
        return Mono.just(ResponseEntity.status(500)
            .body(new ErrorResponse("500", "Internal error", 500, now())));
    }
}

WebFilter 顺序

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)   // 最先:生成请求 ID
public class RequestIdFilter implements WebFilter { ... }

@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)  // 尽早:CORS 预检不需要认证
public class CorsFilter implements WebFilter { ... }

@Component
@Order(-100)                          // 其次:访问日志
public class LoggingFilter implements WebFilter { ... }

@Component
@Order(0)                             // 最后:认证/鉴权
public class AuthFilter implements WebFilter { ... }

顺序的原则是「不依赖身份的东西先做」:请求 ID 和 CORS 必须在认证之前,否则浏览器的预检请求会被 401 拦掉。如果使用 Spring Security,CORS 通常配置在 SecurityWebFilterChain 里(http.cors(...)),security 过滤器链自己的 order 是 -100,自定义 WebFilter 的 order 要和它对齐后再调整。

小结

本附录提供 WebFlux 开发中可以直接复制使用的代码模板。根据项目偏好选择注解路由或函数式路由,混合使用亦可。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「java」更多文章

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