使用StringBuilder与URLEncoder时出现URL编码异常如何解决?
以下是我的Kotlin代码:
override suspend fun getData(ids: String): SomeData { val itemIds = ids.split(",").iterator() val sb = StringBuilder() sb.append("/v1/data?ids=") itemIds.forEach { val assetsJson = myObjectMapperWrapper.getObjectMapper().writeValueAsString( DataRequest(type = "Picture", id = it) ) sb.append(URLEncoder.encode(assetsJson, Charsets.UTF_8)) if (itemIds.hasNext()) sb.append("&data=") } val uri = sb.toString() }
其中assetsJson示例为:{"type":"Picture","id":" 12345"}(id字段的空格是故意保留的,API要求如此)。
通过在线工具编码该字符串得到:
%7B%22type%22%3A%22Picture%22%2C%22id%22%3A%22%20%20%20%20%20%20%20%20%2012345%22%7D
将其粘贴到Insomnia中调用API可正常工作。
但本地编码结果为:
%7B%22type%22%3A%22Picture%22%2C%22id%22%3A%22+++++++++12345%22%7D
空格被编码为+而非%20。更严重的是,实际发送到服务器的内容出现二次编码:
%257B%2522type%2522%253A%2522Picture%2522%252C%2522id%2522%253A%2522+++++++++12345%2522%257D
%被转为%25,导致API解析失败,返回错误:
Failed to convert value of type 'java.lang.String[]' to required type 'java.util.Set'; nested exception is java.lang.IllegalArgumentException: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('%' (code 37)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false') at [Source: (String)"%7B%22type%22%3A%22Picture%22%2C%22id%22%3A%22 12345%22%7D"; line: 1, column: 2]
1. 避免手动拼接URL参数,改用HTTP客户端的参数构建API
二次编码的核心原因是:手动用URLEncoder.encode编码后,后续的HTTP客户端(比如OkHttp、Retrofit)会自动对URL查询参数进行编码,导致重复转义。
正确的做法是让客户端负责参数编码,只需传入原始的JSON字符串即可。以OkHttp为例:
override suspend fun getData(ids: String): SomeData { val itemIds = ids.split(",") val urlBuilder = HttpUrl.Builder() .scheme("https") // 根据实际情况修改 .host("your-api-host.com") // 根据实际情况修改 .addPathSegments("v1/data") itemIds.forEachIndexed { index, id -> val assetsJson = myObjectMapperWrapper.getObjectMapper().writeValueAsString( DataRequest(type = "Picture", id = id) ) // 第一个参数用ids,后续用data val paramKey = if (index == 0) "ids" else "data" urlBuilder.addQueryParameter(paramKey, assetsJson) } val url = urlBuilder.build() // 后续用url发起请求 }
HttpUrl.Builder会自动按照URL标准编码参数,空格会被转为%20,且不会出现二次编码问题。
如果用Retrofit,可以通过@Query注解传递参数,框架会自动处理编码:
interface DataApi { @GET("/v1/data") suspend fun getData( @Query("ids") firstIdJson: String, @Query("data") otherIdJsons: List<String> ): SomeData } // 调用时 val itemIds = ids.split(",") val firstJson = myObjectMapperWrapper.getObjectMapper().writeValueAsString( DataRequest(type = "Picture", id = itemIds.first()) ) val otherJsons = itemIds.drop(1).map { myObjectMapperWrapper.getObjectMapper().writeValueAsString( DataRequest(type = "Picture", id = it) ) } val result = dataApi.getData(firstJson, otherJsons)
2. 若必须手动编码,处理空格并避免二次编码
如果因特殊原因必须手动拼接URL,需要:
- 将
URLEncoder.encode的结果中的+替换为%20,符合URL标准编码 - 确保后续客户端不会再次编码(比如设置客户端禁用自动编码,或确认直接使用拼接好的完整URL发起请求)
修改后的手动拼接代码:
override suspend fun getData(ids: String): SomeData { val itemIds = ids.split(",").iterator() val sb = StringBuilder() sb.append("/v1/data?ids=") itemIds.forEach { val assetsJson = myObjectMapperWrapper.getObjectMapper().writeValueAsString( DataRequest(type = "Picture", id = it) ) // 编码后替换+为%20 val encodedJson = URLEncoder.encode(assetsJson, Charsets.UTF_8).replace("+", "%20") sb.append(encodedJson) if (itemIds.hasNext()) sb.append("&data=") } val uri = sb.toString() // 确保后续请求时直接使用该uri,不进行额外编码 }
这种方式风险较高,若后续客户端仍会自动编码,还是会出现二次转义,因此优先推荐第一种方法。
内容的提问来源于stack exchange,提问作者hc0re

