注解路由模板
@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 开发中可以直接复制使用的代码模板。根据项目偏好选择注解路由或函数式路由,混合使用亦可。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。