WebFluxLinkBuilder生成含LocalDate路径参数的自链接时日期编码格式异常求助
我最近正把自己的一个项目从Spring MVC迁移到WebFlux,在使用WebFluxLinkBuilder生成指向包含LocalDate路径参数的方法的链接时遇到了问题:生成的链接里日期格式变成了12%2F02%2F2025(也就是斜杠分隔的日期被URL编码后的结果),而不是我期望的2025-02-12格式。
我的非阻塞版本代码
@GetMapping("test/{date}") fun test(@PathVariable date: LocalDate?): Mono<ResponseEntity<EntityModel<*>>> = Mono .fromCallable { date } .subscribeOn(Schedulers.boundedElastic()) .map { EntityModel.of( object { val date = it } ) } .flatMap { linkTo(octopusLinkBuilder.test(date)).withSelfRel().toMono() .map { link -> it.add(link) } } .map { ResponseEntity.ok(it) }
非阻塞版本的输出
调用接口:
$ curl https://localhost:8898/api/test/2025-02-12
返回结果:
{ "date" : "2025-02-12", "_links" : { "self" : { "href" : "https://localhost:8898/api/test/12%2F02%2F2025" } } }
尝试过的阻塞版本(存在缺陷)
我试过改用阻塞调用,虽然能得到正确的日期格式,但生成的链接丢失了协议和主机名:
@GetMapping("test/{date}") fun test(@PathVariable date: LocalDate?): Mono<ResponseEntity<EntityModel<*>>> = Mono .fromCallable { date } .subscribeOn(Schedulers.boundedElastic()) .map { EntityModel.of( object { val date = it } ) } .map { linkTo(octopusLinkBuilder.test(date)).withSelfRel().toMono() .map { link -> it.add(link) }.block()!! } .map { ResponseEntity.ok(it) }
阻塞版本的输出
调用同一个接口后返回:
{ "date" : "2025-02-12", "_links" : { "self" : { "href" : "/api/test/2025-02-12" } } }
我刚接触WebFlux,是不是我对响应式链接生成的逻辑有什么根本性的误解?
问题原因分析
其实这个问题的核心是:WebFlux响应式链接生成流程中,LocalDate参数的格式化默认用了JVM本地的斜杠分隔日期格式,而非Spring默认的ISO-8601(yyyy-MM-dd)格式。而阻塞调用能得到正确日期,是因为同步上下文会用Spring配置的ISO格式,但同时会丢失响应式上下文中保存的请求元数据(协议、主机名),导致链接变成相对路径。
给你几个可行的解决方案
方案1:全局配置LocalDate格式化器
给LocalDate注册一个全局格式化器,让Spring在所有场景(包括路径解析和链接生成)都用ISO格式:
@Configuration class WebConfig : WebFluxConfigurer { override fun addFormatters(registry: FormatterRegistry) { registry.addFormatterForFieldType( LocalDate::class.java, object : Formatter<LocalDate> { override fun parse(text: String, locale: Locale): LocalDate { return LocalDate.parse(text, DateTimeFormatter.ISO_LOCAL_DATE) } override fun print(objectValue: LocalDate, locale: Locale): String { return DateTimeFormatter.ISO_LOCAL_DATE.format(objectValue) } } ) } }
这个配置会把ISO格式全局应用到所有LocalDate的格式化场景,包括WebFluxLinkBuilder的链接生成,不用改业务代码就能解决问题。
方案2:生成链接时显式格式化日期
如果不想全局修改格式,也可以在生成链接前手动把LocalDate转成ISO格式的字符串再传入:
.flatMap { entity -> val formattedDate = date?.format(DateTimeFormatter.ISO_LOCAL_DATE) linkTo(octopusLinkBuilder.test(formattedDate)).withSelfRel().toMono() .map { link -> entity.add(link) } }
注意:这种方式需要你的octopusLinkBuilder.test方法支持接收String类型的日期参数,或者调整链接构建器的方法签名适配格式化后的字符串。
方案3:手动结合请求上下文生成完整链接
如果上面两种方案都不适用,你可以手动利用请求上下文里的协议、主机信息,同时显式格式化日期:
.flatMap { entity -> ServerWebExchangeContextHolder.getExchange()?.let { exchange -> // 从请求上下文获取协议、主机、端口 val uriBuilder = MvcUriComponentsBuilder.fromController(YourController::class.java) .withScheme(exchange.request.scheme) .withHost(exchange.request.headers.host.hostName) .withPort(exchange.request.headers.host.port ?: 80) // 显式格式化日期并构建完整链接 val fullUri = uriBuilder .path("/api/test/{date}") .buildAndExpand(date?.format(DateTimeFormatter.ISO_LOCAL_DATE)) .toUriString() // 生成Link并添加到EntityModel val selfLink = Link.of(fullUri, "self") Mono.just(entity.add(selfLink)) } ?: Mono.error(IllegalStateException("当前没有可用的ServerWebExchange上下文")) }
这种方式完全手动控制链接的生成逻辑,既能保证日期格式正确,也能保留完整的协议和主机信息。
为什么非阻塞版本会出问题?
简单来说,WebFlux响应式链接生成的toMono()方法,在处理参数时没有复用Spring全局配置的ISO日期格式化策略,而是用了JVM默认的格式,导致日期被转成斜杠分隔的形式后被URL编码。而阻塞调用的block()会切换到同步上下文,此时会用Spring的ISO格式,但同时会丢失响应式上下文中的请求元数据,所以链接变成了相对路径。
内容来源于stack exchange

