这是我参与8月更文挑战的第 2 天,活动详情查看:8月更文挑战
遇到问题了
OkHttp 在使用时对于后台返回的数据调用string()
方法,转成字符串然后进行序列化相关处理即可。最近对接一个很老的系统,后台返回的数据使用的格式不是UTF-8
而是GB2312
,而且在Header
里面没有设置任何Content-Type
字段,导致直接获取数据时,OkHttp 会默认使用UTF-8
来转换数据,导致乱码。具体解析源码如下:
fun string(): String = source().use { source ->
source.readString(charset = source.readBomAsCharset(charset()))
}
private fun charset() = contentType()?.charset(UTF_8) ?: UTF_8
复制代码
最终是在Exchange
中获取ContentType
的,
val contentType = response.header("Content-Type")
复制代码
为了需求,只能在使用过程中就直接通过Response
获取字节数组然后手动转换,这样导致封装的调用方法需要增加一些额外的操作,暂时只能先这样。
寻求解决办法
等到后面翻看代码时,想到是不是可以通过拦截器进行处理,当然事实证明还是太年轻。
首先写一个拦截器,然后在OkHttp
构造方法时设置即可。
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.request().url().encodedPath().toString().equals("/xxx/xxx")) {
return response.newBuilder().header("Content-Type", "GB2312").build();
}
return response;
}
}
复制代码
等设置完成后,觉得这个问题肯定解决了。没想到乱码还是没有任何变化,经过一阵点点点,找找找,似乎看到了一些眉目。
我们调用ResponseBody.string()
方法来获取数据,这里调用source.readString(charset)
方法进行处理,这里面的charset
最终是在Exchange
里面设置进去的,代码如下,
fun openResponseBody(response: Response): ResponseBody {
try {
val contentType = response.header("Content-Type")
val contentLength = codec.reportedContentLength(response)
val rawSource = codec.openResponseBodySource(response)
val source = ResponseBodySource(rawSource, contentLength)
return RealResponseBody(contentType, contentLength, source.buffer())
} catch (e: IOException) {
eventListener.responseFailed(call, e)
trackFailure(e)
throw e
}
}
复制代码
我们的拦截器是生效了的,但是在这里的response
是没有Content-Type
的,只能说明这个方法是在我们拦截器之前调用的。那么是不是这样的呢,最好的解决办法就是去阅读源码了。
首先我们看下构造方法,
这里用了建造者模式来构造对象,
class Builder constructor() {
internal var dispatcher: Dispatcher = Dispatcher()
internal var connectionPool: ConnectionPool = ConnectionPool()
// 传入的拦截器
internal val interceptors: MutableList<Interceptor> = mutableListOf()
internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
......
}
复制代码
构造完成后,我们创建一个Request
通过mOkHttpClient.newCall(request).enqueue()
发起请求。Request
类里面是一些请求相关参数,我们看下newCall
方法,将我们的请求封装成了一个RealCall
对象,并调用了enqueue
方法。我们继续看下这个方法。
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
复制代码
override fun enqueue(responseCallback: Callback) {
check(executed.compareAndSet(false, true)) { "Already Executed" }
callStart()
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
复制代码
先校验下是否运行过,没有运行过才会继续执行。接着回调下方法,第三行里面将我们自己的回调封装成AsyncCall
然后调用dispatcher.enqueue
方法,我们继续跟下去。
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)
// 查找相同 host 的请求
if (!call.call.forWebSocket) {
val existingCall = findExistingCallWithHost(call.host)
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
// 执行请求
promoteAndExecute()
}
// 代码有删减
private fun promoteAndExecute(): Boolean {
synchronized(this) {
while (i.hasNext()) {
// 这里会限制最大请求数和每个 host 的最大请求数
if (runningAsyncCalls.size >= this.maxRequests) break
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
// ......
}
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
// 通过线程池执行
asyncCall.executeOn(executorService)
}
return isRunning
}
复制代码
这里的线程池的核心线程数是0
最大线程数是Int.MAX_VALUE
,继续看下去。
fun executeOn(executorService: ExecutorService) {
var success = false
try {
executorService.execute(this)
success = true
} catch (e: RejectedExecutionException) {
responseCallback.onFailure(this@RealCall, ioException)
}
}
复制代码
这里可以看到对于请求失败调用了我们的回调方法。这里的AsyncCall
就是一个Runnable
,然后交给线程池运行,所以我们找到run
方法继续看下去。
override fun run() {
threadName("OkHttp ${redactedUrl()}") {
try {
// 通过这里进行请求,这里就是开始责任链模式调用了
val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
if (signalledCallback) {
} else {
responseCallback.onFailure(this@RealCall, e)
}
} catch (t: Throwable) {
......
} finally {
}
}
}
internal fun getResponseWithInterceptorChain(): Response {
// 添加上我们的拦截器和系统的拦截器
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
// 构建请求链
val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
var calledNoMoreExchanges = false
try {
// 开始调用
val response = chain.proceed(originalRequest)
} catch (e: IOException) {
} finally {
}
}
复制代码
到这里可以看到先是添加上我们的拦截器和系统的拦截器,然后构建链条,开始调用,我们看看proceded
方法。
override fun proceed(request: Request): Response {
......
// Call the next interceptor in the chain.
val next = copy(index = index + 1, request = request)
val interceptor = interceptors[index]
val response = interceptor.intercept(next) ?: throw NullPointerException(
"interceptor $interceptor returned null")
......
return response
}
复制代码
这里面通过copy
方法获取下一个Chain
然后调用拦截器列表中的下一个拦截器进行请求,下一个拦截器又会依次调用,最终形成一个调用链条。
这里的逻辑比较绕,每一个拦截器都会调用proceed
方法从上到下获取数据,最终会调用到CallServerInterceptor
这个拦截器然后通过网络请求获取数据。获取数据成功后,又会将数据返回回去,这样又按照顺序从下到上回到拦截器的调用方法处,最终将结果返回到我们。
总结
这样整个流程我们大致简单的分析了一遍,当然这其中还是有很多东西我们并没有看到。那么我们回到开始的问题吧,可以发现在CallServerInterceptor
这个拦截器中会调用exchange.createRequestBody().buffer()
对出参进行了封装。这样也就解释通了为什么我们自定义拦截器增加Content-Type
对数据处理没有影响了。虽然这里我们没有很好的解决我们开头的问题,但是通过阅读源码我们还是学习到了很多知识的,看来这波不亏。