Okhttp 一个被广泛用于Android and Java applications中作为http请求的基础类库, 具有简单和方便的API接口, 支持同步和异步, 在http2协议下可以允许多个请求共享一个socket, 连接池降低延迟等特性;
问题抛出
今天做一个新项目, 应团队规范, 采用okhttp替换HttpComponents, 然后在做http请求时, 返回的内容中文乱码, 第一次使用okhttp, 遇到这样的问题, 就想先去官方的查找解决方法。。。项目介绍也太简单了吧, 跳转到github上的项目说明, 也没啥有用信息, 那就debug看看吧!
问题分析
- okhttp请求示例(流式api哇)
logger.info("http ask: {}, request:\n {}", url, data);
RequestBody body = RequestBody.create(type, data);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Response response = getInstance().newCall(request).execute();
String content = response.body().string();
logger.info("http ask: {}, response:\n {}", url, content);
- 直接进到最后读取server-response content的部分, 即 body().string();
1). 读取Response的content
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
Util.closeQuietly(source);
}
}
2). 获取编码, 如果contentType中的charset为空, 就默认UTF_8
private Charset charset() {
MediaType contentType = contentType();
return contentType != null ? contentType.charset(UTF_8) : UTF_8;
}
3). 根据Response.Body中的contentTypeString解析资源信息, 包含编码
@Override public MediaType contentType() {
return contentTypeString != null ? MediaType.parse(contentTypeString) : null;
}
4). 解析charset, 如果服务端没返回, 那就为null
public static MediaType get(String string) {
Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
if (!typeSubtype.lookingAt()) {
throw new IllegalArgumentException("No subtype found for: \"" + string + '"');
}
String type = typeSubtype.group(1).toLowerCase(Locale.US);
String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
String charset = null;
Matcher parameter = PARAMETER.matcher(string);
for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
parameter.region(s, string.length());
if (!parameter.lookingAt()) {
throw new IllegalArgumentException("Parameter is not formatted correctly: \""
+ string.substring(s)
+ "\" for: \""
+ string
+ '"');
}
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
String charsetParameter;
String token = parameter.group(2);
if (token != null) {
// If the token is 'single-quoted' it's invalid! But we're lenient and strip the quotes.
charsetParameter = (token.startsWith("'") && token.endsWith("'") && token.length() > 2)
? token.substring(1, token.length() - 1)
: token;
} else {
// Value is "double-quoted". That's valid and our regex group already strips the quotes.
charsetParameter = parameter.group(3);
}
if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
throw new IllegalArgumentException("Multiple charsets defined: \""
+ charset
+ "\" and: \""
+ charsetParameter
+ "\" for: \""
+ string
+ '"');
}
charset = charsetParameter;
}
return new MediaType(string, type, subtype, charset);
}
5). 原因, 服务端给我返了个不包含charset的contentType, 导致客户端默认按照UTF_8去解析, 导致服务端返回的GBK编码的内容就中文乱码咯
text/html
6). 问题找到了, 怎么解决呢?
解决问题
- 既然okhttp是从Response.Body中的contentTypeString解析编码的, 那我们可以在它读取Response.content之前设置客户端想要的contentType嘛
- 用拦截器来做
import com.alibaba.fastjson.JSON;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import okhttp3.Headers;
import okhttp3.Headers.Builder;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* logging and response encoding
* @author Hinsteny
* @version $ID: EncodingInterceptor 2018-10-30 14:16 All rights reserved.$
*/
public class EncodingInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(EncodingInterceptor.class);
/**
* 自定义编码
*/
private String encoding;
public EncodingInterceptor(String encoding) {
this.encoding = encoding;
}
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long start = System.nanoTime();
logger.info("Sending request: {}, headers: {}, request: {}", request.url(), request.headers(), JSON.toJSONString(request.body()));
Response response = chain.proceed(request);
long end = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n %s", response.request().url(), (end - start) / 1e6d, JSON.toJSONString(response.headers())));
settingClientCustomEncoding(response);
return response;
}
/**
* setting client custom encoding when server not return encoding
* @param response
* @throws IOException
*/
private void settingClientCustomEncoding(Response response) throws IOException {
// setHeaderContentType(response);
setBodyContentType(response);
}
/**
* set contentType in headers
* @param response
* @throws IOException
*/
private void setHeaderContentType(Response response) throws IOException {
String contentType = response.header("Content-Type");
if (StringUtils.isNotBlank(contentType) && contentType.contains("charset")) {
return;
}
// build new headers
Headers headers = response.headers();
Builder builder = headers.newBuilder();
builder.removeAll("Content-Type");
builder.add("Content-Type", (StringUtils.isNotBlank(contentType) ? contentType + "; ":"" ) + "charset=" + encoding);
headers = builder.build();
// setting headers using reflect
Class _response = Response.class;
try {
Field field = _response.getDeclaredField("headers");
field.setAccessible(true);
field.set(response, headers);
} catch (NoSuchFieldException e) {
throw new IOException("use reflect to setting header occurred an error", e);
} catch (IllegalAccessException e) {
throw new IOException("use reflect to setting header occurred an error", e);
}
}
/**
* set body contentType
* @param response
* @throws IOException
*/
private void setBodyContentType(Response response) throws IOException {
ResponseBody body = response.body();
// setting body contentTypeString using reflect
Class<? extends ResponseBody> aClass = body.getClass();
try {
Field field = aClass.getDeclaredField("contentTypeString");
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
String contentTypeString = String.valueOf(field.get(body));
if (StringUtils.isNotBlank(contentTypeString) && contentTypeString.contains("charset")) {
return;
}
field.set(body, (StringUtils.isNotBlank(contentTypeString) ? contentTypeString + "; ":"" ) + "charset=" + encoding);
} catch (NoSuchFieldException e) {
throw new IOException("use reflect to setting header occurred an error", e);
} catch (IllegalAccessException e) {
throw new IOException("use reflect to setting header occurred an error", e);
}
}
}
- 由于Response.body没有定义方法去设置contentType, 因此用了反射去设置自定义的内容, 测试了一下, 完美通关, 至此问题解决!