Appearance
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` 过滤器时需要注意以下几点:
- 内存使用:响应体会被完全加载到内存中,对于大文件可能导致内存压力
- 性能影响:响应修改会增加延迟,需要权衡业务需求和性能
- 异常处理:必须妥善处理序列化/反序列化异常,避免服务中断
对于流式响应或大文件下载,不建议使用此过滤器,因为它会缓冲整个响应体。
处理空响应体
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 中处理响应体的强大工具,它为我们提供了在网关层统一处理响应数据的能力。通过合理使用这个过滤器,我们可以实现数据脱敏、格式转换、响应包装等多种业务需求,提高系统的安全性和一致性。
在实际使用中,我们需要注意性能影响和异常处理,确保在提供功能的同时不影响系统的稳定性和响应速度。