You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 3.3中替代RestControllerEndpoint实现代理Actuator端点

在Spring Boot 3.3.0中替代@RestControllerEndpoint实现Actuator代理端点

之前在Spring Boot 3.2.x环境下,我用WebFlux实现了一个基于@RestControllerEndpoint的自定义Actuator端点,用于将hal-browser的请求转发到实际API,通过这个代理实现从Actuator入口访问API,该方案运行多年正常。但Spring Boot 3.3.0中@RestControllerEndpoint被弃用,改用@WebEndpoint后出现「Missing parameters: exchange」错误。

原(Spring Boot 3.2.x可用)代码

@Component
@RestControllerEndpoint(id = "api", enableByDefault = true)
class ApiProxyEndpoint {

    @Value("\${management.endpoints.web.base-path}")
    lateinit var managementPath: String

    @Autowired
    lateinit var webHandler: WebHandler

    @RequestMapping("**") // 匹配<management-path>/api/下的所有路径片段
    fun apiProxyEndpoint(exchange: ServerWebExchange): Mono<Void> {
        val originalPath = exchange.request.path.toString()
        val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
        val updatedRequest = exchange.request.mutate().contextPath("/").path(apiPathWithoutManagementPath).header("X-Api-RootPath", managementPath).build()
        val updatedExchange = exchange.mutate().request(updatedRequest).build()

        return webHandler.handle(updatedExchange)
    }
}

尝试的Spring Boot 3.3.0代码

@Component
@WebEndpoint(id = "api", enableByDefault = true)
class ApiProxyEndpoint {

    @Value("\${management.endpoints.web.base-path}")
    lateinit var managementPath: String

    @Autowired
    lateinit var webHandler: WebHandler

    @WriteOperation
    @ReadOperation
    @DeleteOperation
    fun apiProxyEndpoint(exchange: ServerWebExchange): Mono<Void> {
        val originalPath = exchange.request.path.toString()
        val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
        val updatedRequest = exchange.request.mutate().contextPath("/").path(apiPathWithoutManagementPath).header("X-Api-RootPath", managementPath).build()
        val updatedExchange = exchange.mutate().request(updatedRequest).build()

        return webHandler.handle(updatedExchange)
    }
}

报错信息

{
  "timestamp": "2024-06-12T10:23:17.366+00:00",
  "status": 400,
  "error": "Bad Request",
  "requestId": "efc7c7b9-4",
  "message": "Missing parameters: exchange"
}

解决方案

问题根源

@WebEndpoint是用于构建标准化的Actuator端点(支持JMX/HTTP),它的操作方法(@ReadOperation等)依赖请求参数、路径变量的绑定注入,不能直接接收ServerWebExchange对象,这就是报错的原因。而原@RestControllerEndpoint本质是特殊的@RestController,天然支持Web请求上下文的处理。

替代方案1:使用@RestController配合Actuator路径

直接用普通的@RestController映射到Actuator基础路径下的/api/**,保留原有的代理逻辑:

@Component
@RestController
@RequestMapping("\${management.endpoints.web.base-path}/api/**")
class ApiProxyEndpoint {

    @Value("\${management.endpoints.web.base-path}")
    lateinit var managementPath: String

    @Autowired
    lateinit var webHandler: WebHandler

    @RequestMapping
    fun apiProxyEndpoint(exchange: ServerWebExchange): Mono<Void> {
        val originalPath = exchange.request.path.toString()
        val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
        val updatedRequest = exchange.request.mutate()
            .contextPath("/")
            .path(apiPathWithoutManagementPath)
            .header("X-Api-RootPath", managementPath)
            .build()
        val updatedExchange = exchange.mutate().request(updatedRequest).build()

        return webHandler.handle(updatedExchange)
    }
}

若项目启用了Spring Security,需确保Actuator的安全配置允许访问/api/**路径。

替代方案2:使用RouterFunction实现路由转发

在WebFlux环境下,也可以用函数式路由实现代理逻辑:

@Component
class ApiProxyRouter(
    @Value("\${management.endpoints.web.base-path}") private val managementPath: String,
    private val webHandler: WebHandler
) {

    @Bean
    fun apiProxyRoute(): RouterFunction<ServerResponse> {
        return RouterFunctions.route(RequestPredicates.path("\${management.endpoints.web.base-path}/api/**")) { exchange ->
            val originalPath = exchange.request.path.toString()
            val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
            val updatedRequest = exchange.request.mutate()
                .contextPath("/")
                .path(apiPathWithoutManagementPath)
                .header("X-Api-RootPath", managementPath)
                .build()
            val updatedExchange = exchange.mutate().request(updatedRequest).build()
            webHandler.handle(updatedExchange).then(Mono.empty())
        }
    }
}

内容的提问来源于stack exchange,提问作者Johan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 12:02:18