Skip to content

ModifyResponseBody GatewayFilter 工厂

概述

ModifyResponseBody 过滤器是 Spring Cloud Gateway 中一个强大的响应处理工具,它允许我们在响应返回给客户端之前修改响应体内容。这个过滤器在需要对下游服务返回的数据进行统一处理、格式转换或敏感信息脱敏等场景中非常有用。

此过滤器只能通过 Java DSL 配置,不支持 YAML 配置方式。

核心功能

解决的业务问题

在微服务架构中,我们经常遇到以下需求:

  • 数据格式统一:将不同服务返回的数据格式统一为前端期望的格式
  • 敏感信息脱敏:对返回给客户端的敏感数据进行脱敏处理
  • 响应数据增强:为响应添加额外的元数据或统计信息
  • 数据转换:将响应数据从一种格式转换为另一种格式

基本使用示例

简单的字符串转换

kotlin
@Configuration
class GatewayConfig {

    @Bean
    fun routes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("rewrite_response_upper") { r ->
                r.host("*.rewriteresponseupper.org")
                    .filters { f ->
                        f.prefixPath("/httpbin")
                            f.modifyResponseBody(String::class.java,String::class.java) { exchange, responseBody ->
                                // 将响应内容转换为大写
                                Mono.just(responseBody?.uppercase() ?: "")
                            }
                    }
                    .uri("http://httpbin.org")
            }
            .build()
    }
}

实际业务场景示例

场景一:用户信息脱敏

kotlin
@Configuration
class UserPrivacyConfig {

    @Bean
    fun userRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("user_privacy") { r ->
                r.path("/api/users/**")
                    .filters { f ->
                        f.modifyResponseBody(String::class.java,String::class.java) { exchange, responseBody ->
                            // 对用户敏感信息进行脱敏处理
                            desensitizeUserInfo(responseBody)
                        }
                    }
                    .uri("http://user-service")
            }
            .build()
    }

    private fun desensitizeUserInfo(responseBody: String?): Mono<String> {
        if (responseBody.isNullOrEmpty()) {
            return Mono.empty()
        }

        return try {
            val objectMapper = ObjectMapper()
            val jsonNode = objectMapper.readTree(responseBody)

            // 脱敏手机号
            if (jsonNode.has("phone")) {
                val phone = jsonNode.get("phone").asText()
                (jsonNode as ObjectNode).put("phone", maskPhone(phone))
            }

            // 脱敏身份证号
            if (jsonNode.has("idCard")) {
                val idCard = jsonNode.get("idCard").asText()
                (jsonNode as ObjectNode).put("idCard", maskIdCard(idCard))
            }

            Mono.just(objectMapper.writeValueAsString(jsonNode))
        } catch (e: Exception) {
            // 处理异常时返回原始内容
            Mono.just(responseBody)
        }
    }

    private fun maskPhone(phone: String): String {
        return if (phone.length >= 11) {
            phone.substring(0, 3) + "****" + phone.substring(7)
        } else phone
    }

    private fun maskIdCard(idCard: String): String {
        return if (idCard.length >= 18) {
            idCard.substring(0, 6) + "********" + idCard.substring(14)
        } else idCard
    }
}

场景二:统一响应格式

kotlin
@Configuration
class ResponseFormatConfig {

    @Bean
    fun formatRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("format_response") { r ->
                r.path("/api/**")
                    .filters { f ->
                        f.modifyResponseBody(String::class.java,String::class.java) { exchange, responseBody ->
                            // 将所有响应包装为统一格式
                            wrapResponse(exchange, responseBody)
                        }
                    }
                    .uri("http://backend-service")
            }
            .build()
    }

    private fun wrapResponse(exchange: ServerWebExchange, responseBody: String?): Mono<String> {
        val response = exchange.response
        val statusCode = response.statusCode?.value() ?: 200

        val wrappedResponse = mapOf(
            "code" to statusCode,
            "message" to if (statusCode == 200) "成功" else "失败",
            "data" to responseBody,
            "timestamp" to System.currentTimeMillis()
        )

        return try {
            val objectMapper = ObjectMapper()
            Mono.just(objectMapper.writeValueAsString(wrappedResponse))
        } catch (e: Exception) {
            Mono.just("""{"code":500,"message":"响应处理失败","data":null,"timestamp":${System.currentTimeMillis()}}""")
        }
    }
}

场景三:数据类型转换

kotlin
@Configuration
class DataTransformConfig {

    @Bean
    fun transformRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("xml_to_json") { r ->
                r.path("/api/legacy/**")
                    .filters { f ->
                        f.modifyResponseBody(
                            String::class.java,
                            String::class.java
                        ) { exchange, responseBody ->
                            // 将 XML 响应转换为 JSON
                            convertXmlToJson(responseBody)
                        }
                    }
                    .uri("http://legacy-service")
            }
            .build()
    }

    private fun convertXmlToJson(xmlResponse: String?): Mono<String> {
        if (xmlResponse.isNullOrEmpty()) {
            return Mono.empty()
        }

        return try {
            val xmlMapper = XmlMapper()
            val jsonMapper = ObjectMapper()

            // 将 XML 转换为 JsonNode
            val jsonNode = xmlMapper.readTree(xmlResponse.toByteArray())

            // 转换为 JSON 字符串
            val jsonResponse = jsonMapper.writeValueAsString(jsonNode)

            Mono.just(jsonResponse)
        } catch (e: Exception) {
            // 转换失败时返回错误响应
            Mono.just("""{"error":"XML转JSON失败","originalData":"$xmlResponse"}""")
        }
    }
}

高级用法

响应体为空的处理

kotlin
@Configuration
class EmptyResponseConfig {

    @Bean
    fun emptyResponseRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("handle_empty_response") { r ->
                r.path("/api/status/**")
                    .filters { f ->
                        f.modifyResponseBody(
                            String::class.java,
                            String::class.java
                        ) { exchange, responseBody ->
                            // 处理空响应体的情况
                            handleEmptyResponse(exchange, responseBody)
                        }
                    }
                    .uri("http://status-service")
            }
            .build()
    }

    private fun handleEmptyResponse(
        exchange: ServerWebExchange,
        responseBody: String?
    ): Mono<String> {
        return if (responseBody == null) {
            // 为空响应提供默认内容
            val defaultResponse = mapOf(
                "status" to "success",
                "message" to "操作完成",
                "data" to null
            )
            val objectMapper = ObjectMapper()
            Mono.just(objectMapper.writeValueAsString(defaultResponse))
        } else {
            Mono.just(responseBody)
        }
    }
}

条件性响应修改

kotlin
@Configuration
class ConditionalModifyConfig {

    @Bean
    fun conditionalRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("conditional_modify") { r ->
                r.path("/api/products/**")
                    .filters { f ->
                        f.modifyResponseBody(
                            String::class.java,
                            String::class.java
                        ) { exchange, responseBody ->
                            // 根据条件决定是否修改响应
                            conditionallyModifyResponse(exchange, responseBody)
                        }
                    }
                    .uri("http://product-service")
            }
            .build()
    }

    private fun conditionallyModifyResponse(
        exchange: ServerWebExchange,
        responseBody: String?
    ): Mono<String> {
        val request = exchange.request
        val userAgent = request.headers.getFirst("User-Agent") ?: ""

        // 只对移动端用户进行响应修改
        return if (userAgent.contains("Mobile", ignoreCase = true)) {
            // 为移动端精简响应内容
            simplifyForMobile(responseBody)
        } else {
            // 桌面端返回完整响应
            Mono.justOrEmpty(responseBody)
        }
    }

    private fun simplifyForMobile(responseBody: String?): Mono<String> {
        if (responseBody.isNullOrEmpty()) {
            return Mono.empty()
        }

        return try {
            val objectMapper = ObjectMapper()
            val jsonNode = objectMapper.readTree(responseBody)

            // 移除不必要的字段以减少数据传输
            if (jsonNode.isObject) {
                val objectNode = jsonNode as ObjectNode
                objectNode.remove("description")
                objectNode.remove("metadata")
                objectNode.remove("createdBy")
                objectNode.remove("updatedBy")
            }

            Mono.just(objectMapper.writeValueAsString(jsonNode))
        } catch (e: Exception) {
            Mono.just(responseBody)
        }
    }
}

工作流程图

最佳实践

以下是使用 `ModifyResponseBody` 过滤器的最佳实践建议:

1. 异常处理

kotlin
private fun safeModifyResponse(responseBody: String?): Mono<String> {
    return try {
        // 响应处理逻辑
        if (responseBody.isNullOrEmpty()) {
            return Mono.empty()
        }

        // 实际的修改逻辑
        val modifiedBody = processResponseBody(responseBody)
        Mono.just(modifiedBody)

    } catch (e: Exception) {
        // 记录错误日志
        logger.error("响应体修改失败", e)

        // 返回原始响应或默认响应
        Mono.justOrEmpty(responseBody)
    }
}

2. 性能优化

kotlin
@Component
class ResponseBodyCache {

    private val cache = ConcurrentHashMap<String, String>()

    fun getCachedTransformation(key: String, transformer: () -> String): String {
        return cache.computeIfAbsent(key) { transformer() }
    }
}

3. 配置化管理

kotlin
@ConfigurationProperties(prefix = "gateway.response.modify")
@Component
data class ResponseModifyProperties(
    var enableDesensitization: Boolean = true,
    var enableFormatWrapper: Boolean = true,
    var enableXmlToJsonConversion: Boolean = false,
    var desensitizationRules: Map<String, String> = emptyMap()
)

注意事项

使用 `ModifyResponseBody` 过滤器时需要注意以下几点:

  1. 内存使用:响应体会被完全加载到内存中,对于大文件可能导致内存压力
  2. 性能影响:响应修改会增加延迟,需要权衡业务需求和性能
  3. 异常处理:必须妥善处理序列化/反序列化异常,避免服务中断

对于流式响应或大文件下载,不建议使用此过滤器,因为它会缓冲整个响应体。

处理空响应体

kotlin
// 正确的空响应体处理方式
private fun handleNullResponse(responseBody: String?): Mono<String> {
    return if (responseBody == null) {
        // 返回 Mono.empty() 表示空响应体
        Mono.empty()
    } else {
        // 处理非空响应体
        Mono.just(processResponse(responseBody))
    }
}

总结

ModifyResponseBody 过滤器是 Spring Cloud Gateway 中处理响应体的强大工具,它为我们提供了在网关层统一处理响应数据的能力。通过合理使用这个过滤器,我们可以实现数据脱敏、格式转换、响应包装等多种业务需求,提高系统的安全性和一致性。

在实际使用中,我们需要注意性能影响和异常处理,确保在提供功能的同时不影响系统的稳定性和响应速度。