1. 项目概述:多格式网络请求全解析方案
在移动端开发中,网络请求如同应用程序的血管系统,负责着数据在客户端与服务端之间的循环流动。而OkHttp与Retrofit的组合,早已成为Android开发者处理网络请求的黄金搭档。但实际业务中我们常常面临一个痛点:不同接口可能返回JSON、XML、Protobuf等不同格式的数据,甚至同一个接口在不同场景下需要支持多种数据格式的请求与解析。
我曾在一个电商App项目中深有体会:用户中心接口使用JSON、支付网关要求XML、商品推荐服务采用Protobuf,而文件上传又需要表单提交。来回切换不同解析方式不仅代码冗余,还容易产生兼容性问题。这就是为什么我们需要构建一套统一处理多数据格式的解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术选型与配置
2.1 基础框架搭建
首先在build.gradle中添加必要依赖:
gradle复制implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // JSON支持
implementation 'com.squareup.retrofit2:converter-jaxb:2.9.0' // XML支持
implementation 'com.google.protobuf:protobuf-java:3.19.4' // Protobuf支持
2.2 多格式转换器配置
创建Retrofit实例时,关键点在于配置多ConverterFactory的执行顺序:
kotlin复制val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(OkHttpClient.Builder().build())
.addConverterFactory(ProtoConverterFactory.create()) // Protobuf优先
.addConverterFactory(JaxbConverterFactory.create()) // 其次XML
.addConverterFactory(GsonConverterFactory.create()) // 最后JSON
.build()
重要提示:ConverterFactory的添加顺序直接影响数据解析的优先级。当服务器返回的Content-Type不明确时,Retrofit会按添加顺序尝试解析。
3. 各数据格式实战详解
3.1 JSON数据处理方案
3.1.1 基础JSON解析
定义数据模型:
kotlin复制data class UserResponse(
@SerializedName("user_id") val userId: String,
@SerializedName("user_name") val userName: String
)
接口声明:
kotlin复制interface ApiService {
@GET("user/info")
suspend fun getUserInfo(): UserResponse
}
3.1.2 高级JSON特性处理
处理特殊日期格式:
kotlin复制val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer())
.create()
3.2 表单数据处理技巧
3.2.1 普通表单提交
kotlin复制@FormUrlEncoded
@POST("login")
suspend fun login(
@Field("username") username: String,
@Field("password") password: String
): Response<LoginResponse>
3.2.2 多部分表单(文件上传)
kotlin复制@Multipart
@POST("upload")
suspend fun uploadFile(
@Part file: MultipartBody.Part,
@Part("description") description: RequestBody
): Response<UploadResult>
文件封装方法:
kotlin复制fun prepareFilePart(name: String, file: File): MultipartBody.Part {
val requestFile = file.asRequestBody("multipart/form-data".toMediaType())
return MultipartBody.Part.createFormData(name, file.name, requestFile)
}
3.3 XML数据解析方案
3.3.1 模型定义与注解
java复制@XmlRootElement(name = "response")
public class WeatherResponse {
@XmlElement(name = "city")
private String cityName;
@XmlElement(name = "temperature")
private float temp;
// getters & setters
}
3.3.2 接口配置
kotlin复制interface WeatherApi {
@GET("weather")
@Headers("Accept: application/xml")
suspend fun getWeather(@Query("city") city: String): WeatherResponse
}
3.4 Protobuf高效传输方案
3.4.1 定义.proto文件
protobuf复制syntax = "proto3";
message Product {
string id = 1;
string name = 2;
float price = 3;
repeated string tags = 4;
}
3.4.2 接口配置
kotlin复制interface ProductApi {
@GET("products/{id}")
suspend fun getProduct(@Path("id") id: String): Product
}
4. 高级技巧与性能优化
4.1 动态数据格式处理
通过Interceptor实现根据URL自动切换数据格式:
kotlin复制class ContentTypeInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = when {
request.url.encodedPath.contains("/xml/") -> {
request.newBuilder()
.header("Accept", "application/xml")
.build()
}
request.url.encodedPath.contains("/proto/") -> {
request.newBuilder()
.header("Accept", "application/x-protobuf")
.build()
}
else -> request
}
return chain.proceed(newRequest)
}
}
4.2 混合数据格式解析
处理JSON中包含Protobuf二进制数据的情况:
kotlin复制@TypeConverters(ProtoByteConverter::class)
data class HybridResponse(
val meta: MetaData,
val protoData: ByteString
)
class ProtoByteConverter {
@ToJson
fun toJson(byteString: ByteString): String {
return Base64.getEncoder().encodeToString(byteString.toByteArray())
}
@FromJson
fun fromJson(base64: String): ByteString {
return ByteString.copyFrom(Base64.getDecoder().decode(base64))
}
}
5. 实战问题排查指南
5.1 常见Content-Type对照表
| 数据格式 | Content-Type |
|---|---|
| JSON | application/json |
| XML | application/xml |
| Protobuf | application/x-protobuf |
| 表单 | application/x-www-form-urlencoded |
| 多部分表单 | multipart/form-data |
5.2 典型错误解决方案
问题1:Retrofit抛出"Unable to create converter for..."异常
- 检查:是否添加了对应的ConverterFactory
- 检查:服务器返回的Content-Type是否与预期一致
- 解决方案:通过Interceptor强制修改Content-Type头
问题2:表单提交中文乱码
- 解决方案:在OkHttpClient中添加URL编码器:
kotlin复制val client = OkHttpClient.Builder()
.addInterceptor(FormUrlEncodedInterceptor())
.build()
class FormUrlEncodedInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
if (original.body is FormBody) {
val formBody = original.body as FormBody
val newForm = formBody.encodedFields().map {
it.key to URLEncoder.encode(it.value, "UTF-8")
}.toFormBody()
val newRequest = original.newBuilder()
.method(original.method, newForm)
.build()
return chain.proceed(newRequest)
}
return chain.proceed(original)
}
}
6. 性能对比与选型建议
通过基准测试比较不同数据格式在典型Android设备上的表现(数据基于100次请求平均值):
| 格式 | 序列化时间(ms) | 反序列化时间(ms) | 数据大小(KB) |
|---|---|---|---|
| JSON | 12.3 | 15.7 | 28.4 |
| XML | 18.6 | 22.1 | 34.2 |
| Protobuf | 5.2 | 6.8 | 15.7 |
| FormData | 8.9 | - | 19.3 |
选型建议:
- 对性能要求极高的场景(如即时通讯)优先考虑Protobuf
- 需要人类可读或Web兼容的场景使用JSON
- 对接传统系统时可能需要XML
- 文件上传等场景使用FormData
在实际项目中,我通常会建立统一的网络层抽象,对外暴露简洁的API,内部处理各种数据格式的转换。例如:
kotlin复制class NetworkClient {
private val services = mapOf(
"json" to createService(JsonApi::class.java),
"xml" to createService(XmlApi::class.java),
"proto" to createService(ProtoApi::class.java)
)
suspend fun fetchUser(id: String, format: String = "json"): User {
return when(format) {
"xml" -> services["xml"]!!.getUserXml(id).toUser()
"proto" -> services["proto"]!!.getUserProto(id).toUser()
else -> services["json"]!!.getUserJson(id)
}
}
}
这种设计既保持了对外接口的简洁性,又能在内部灵活处理不同数据格式。
