Skip to content

WebClient Attributes:请求级别的数据传递机制 🎯

什么是 WebClient Attributes?

WebClient Attributes 是 Spring WebFlux 中一个强大而优雅的特性,它允许我们在 HTTP 请求中附加自定义属性,这些属性可以在整个请求处理链路中传递和访问。

NOTE

可以把 Attributes 理解为请求的"行李标签",每个请求都可以携带一些额外的信息,这些信息会伴随请求在整个处理流程中传递。

核心价值与应用场景 💡

解决的痛点

在传统的 HTTP 客户端中,我们经常遇到这样的问题:

kotlin
// 传统方式:需要在多个地方重复传递相同的信息
class UserService {
    fun getUserData(userId: String, requestId: String) {
        // 需要在每个方法中都传递 requestId
        val profile = getUserProfile(userId, requestId)
        val preferences = getUserPreferences(userId, requestId)
        // ... 更多调用
    }
    private fun getUserProfile(userId: String, requestId: String) {
        // 又要传递 requestId 用于日志追踪
        logger.info("[$requestId] Fetching user profile for $userId")
        // HTTP 调用...
    }
}
kotlin
// 使用 Attributes:一次设置,全链路可用
class UserService {
    fun getUserData(userId: String, requestId: String) {
        webClient.get()
            .uri("/api/user/$userId/profile")
            .attribute("requestId", requestId) 
            .attribute("userId", userId)       
            .retrieve()
            .bodyToMono<UserProfile>()
    }
}

主要应用场景

  1. 请求追踪与日志关联 📊
  2. 认证信息传递 🔐
  3. 业务上下文传递 🏢
  4. 性能监控与统计 📈

技术原理深度解析 🔍

IMPORTANT

Attributes 只存在于客户端的请求处理链中,不会作为 HTTP 头部发送到服务器。它们是纯粹的客户端上下文信息。

实战代码示例 💻

基础用法:请求追踪

kotlin
@Service
class OrderService(private val webClient: WebClient) {

    suspend fun processOrder(orderId: String): OrderResult {
        val traceId = UUID.randomUUID().toString()

        return webClient.post()
            .uri("/api/orders/$orderId/process")
            .attribute("traceId", traceId)           
            .attribute("operation", "processOrder")  
            .attribute("startTime", System.currentTimeMillis()) 
            .retrieve()
            .awaitBody<OrderResult>()
    }
}

高级用法:全局过滤器配置

kotlin
@Configuration
class WebClientConfig {
    @Bean
    fun webClient(): WebClient {
        return WebClient.builder()
            .filter(loggingFilter())      
            .filter(metricsFilter())      
            .filter(authenticationFilter()) 
            .build()
    }
    private fun loggingFilter(): ExchangeFilterFunction {
        return ExchangeFilterFunction.ofRequestProcessor { request ->
            // 从属性中获取追踪信息
            val traceId = request.attribute("traceId") as? String 
            val operation = request.attribute("operation") as? String 

            logger.info("[$traceId] Starting $operation - ${request.method()} ${request.url()}")

            Mono.just(request)
        }
    }

    private fun metricsFilter(): ExchangeFilterFunction {
        return ExchangeFilterFunction.ofRequestProcessor { request ->
            val startTime = request.attribute("startTime") as? Long 
            if (startTime != null) {
                val duration = System.currentTimeMillis() - startTime
                // 记录性能指标
                meterRegistry.timer("http.client.duration")
                    .record(duration, TimeUnit.MILLISECONDS)
            }
            Mono.just(request)
        }
    }
}

业务场景:多租户支持

kotlin
@Service
class MultiTenantApiClient(private val webClient: WebClient) {
    suspend fun fetchTenantData(tenantId: String, dataType: String): ApiResponse {
        return webClient.get()
            .uri("/api/data/$dataType")
            .attribute("tenantId", tenantId)        
            .attribute("dataType", dataType)        
            .attribute("requestTime", Instant.now()) 
            .retrieve()
            .awaitBody<ApiResponse>()
    }
}

// 对应的过滤器
@Component
class TenantFilter : ExchangeFilterFunction {

    override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
        val tenantId = request.attribute("tenantId") as? String 

        return if (tenantId != null) {
            // 为请求添加租户相关的头部
            val modifiedRequest = ClientRequest.from(request)
                .header("X-Tenant-ID", tenantId)    
                .build()
            next.exchange(modifiedRequest)
        } else {
            next.exchange(request)
        }
    }
}

全局默认属性配置 🌍

Spring 提供了在 WebClient 构建时设置默认属性的能力:

kotlin
@Configuration
class GlobalWebClientConfig {
    @Bean
    fun webClient(): WebClient {
        return WebClient.builder()
            .defaultRequest { requestSpec ->
                requestSpec
                    .attribute("applicationName", "order-service")     
                    .attribute("version", "1.0.0")                   
                    .attribute("environment", activeProfile)          
            }
            .build()
    }

    @Value("${spring.profiles.active:default}")
    private lateinit var activeProfile: String
}

TIP

在 Spring MVC 应用中,你可以利用 ThreadLocal 数据来动态设置默认属性,比如从当前请求的安全上下文中提取用户信息。

与 Spring MVC 集成的实际案例 🔗

kotlin
@RestController
class OrderController(private val orderApiClient: OrderApiClient) {
    @GetMapping("/orders/{orderId}")
    suspend fun getOrder(
        @PathVariable orderId: String,
        request: HttpServletRequest
    ): OrderDto {
        // 从当前 HTTP 请求中提取信息
        val userAgent = request.getHeader("User-Agent")
        val clientIp = request.remoteAddr
        val sessionId = request.session.id
        return orderApiClient.fetchOrder(orderId, userAgent, clientIp, sessionId)
    }
}

@Service
class OrderApiClient(private val webClient: WebClient) {

    suspend fun fetchOrder(
        orderId: String,
        userAgent: String?,
        clientIp: String?,
        sessionId: String?
    ): OrderDto {
        return webClient.get()
            .uri("/external-api/orders/$orderId")
            .attribute("userAgent", userAgent)   
            .attribute("clientIp", clientIp)     
            .attribute("sessionId", sessionId)   
            .attribute("internalOrderId", orderId) 
            .retrieve()
            .awaitBody<OrderDto>()
    }
}

最佳实践与注意事项 ⚡

属性命名约定

kotlin
object AttributeKeys {
    const val TRACE_ID = "traceId"
    const val USER_ID = "userId"
    const val TENANT_ID = "tenantId"
    const val REQUEST_START_TIME = "requestStartTime"
    const val OPERATION_NAME = "operationName"
}

// 使用常量而不是魔法字符串
webClient.get()
    .uri("/api/data")
    .attribute(AttributeKeys.TRACE_ID, traceId)     
    .attribute(AttributeKeys.USER_ID, userId)       
    .retrieve()
    .awaitBody<DataResponse>()

类型安全的属性访问

kotlin
// 创建类型安全的属性访问器
class RequestAttributes(private val request: ClientRequest) {

    fun getTraceId(): String? = request.attribute("traceId") as? String

    fun getUserId(): String? = request.attribute("userId") as? String

    fun getStartTime(): Long? = request.attribute("startTime") as? Long

    // 提供默认值的便捷方法
    fun getTraceIdOrGenerate(): String =
        getTraceId() ?: UUID.randomUUID().toString()
}

// 在过滤器中使用
private fun createLoggingFilter(): ExchangeFilterFunction {
    return ExchangeFilterFunction.ofRequestProcessor { request ->
        val attrs = RequestAttributes(request)  
        val traceId = attrs.getTraceIdOrGenerate()  
        logger.info("[$traceId] Processing request: ${request.url()}")
        Mono.just(request)
    }
}

WARNING

属性值的类型转换需要谨慎处理,建议使用安全的类型转换(as?)并提供合理的默认值。

CAUTION

不要在属性中存储大量数据或敏感信息,属性主要用于传递轻量级的上下文信息。

总结 📝

WebClient Attributes 是一个看似简单但功能强大的特性,它解决了在响应式 HTTP 客户端中传递上下文信息的难题。通过合理使用 Attributes,我们可以:

  • ✅ 实现优雅的请求追踪和日志关联
  • ✅ 简化多层服务调用中的参数传递
  • ✅ 支持复杂的业务场景(如多租户、A/B 测试)
  • ✅ 提供更好的可观测性和监控能力

记住核心理念

Attributes 的设计哲学是"一次设置,全链路可用",它让我们能够在不污染业务逻辑的前提下,优雅地处理横切关注点。

掌握了 WebClient Attributes,你就拥有了构建健壮、可维护的响应式 HTTP 客户端的重要工具! 🚀