背景:在做问题反馈的时候,后端是用Python写的,在post复杂的list的时候,遇到了一个坑。
向后端post结构体数组的时候,android团队版传的content-type是“text/html;charset=UTF-8”,后端同学说无法解析。按照ios那边传过来的content-type,让后端json解析这个string,这样的话,需要传递的string的格式就麻烦了,每个参数就要变成key-value的格式。
后面经过协商,后端将接口content-type统一改成“application/json; charset=utf-8”,也就是我们传过去的是一个json,好了,这下ios就有问题了,当使用json的content-type的时候,团队版后端解析不了,也就是说骑手平台app反馈这个接口需要单独使用“application/json; charset=utf-8”格式的content-type,其他所有接口,都需要使用“text/html;charset=UTF-8”。
那困扰我们android和ios的content-type到底是什么东东?
按照我的理解,content-type就是一个参数而已,我们传的什么参数,然后根据这个参数对数据做不同的处理,就跟我们使用retrofit的时候,返回response中也有content-type,根据content-type类型,决定用什么方式读取数据。
团队版发POST请求的时候,为什么content-type有时候是json,有时候是text呢?
实践发现,其实当我们在接口文件中加上@FormUrlEncoded的时候,content-type是text格式的,没有这个注解的时候,我们会传json,这是因为我们的retrofit会加上一个GsonRequestBodyConverter,这个convert将我们默认的content-type改为json。这样就是说,如果我们接口中,没有加上注解,我们会使用我们自定义好的content-type,如果有注解,我们就使用注解的content-type。
那事实是不是这样咧?我们寻找一下源码看看是不是这么一回事。
入口函数Retrofit中的create函数。
public T create(final Class service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod
这里面使用动态代理的方式,得到需要代理的类,就是我们加注解的类,其他就不细看了,我们直接看看serviceMethod是什么?
ServiceMethod, ?> loadServiceMethod(Method method) {
ServiceMethod, ?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder<>(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}
返回的是result,result又是什么?我们看一下ServiceMethod.Builder做了什么?
Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
是不是一目了然,把有注解的类中的注解、方法、type等拿到,然后在build()中把builder的参数给拿到,
Type responseType;
boolean gotField;
boolean gotPart;
boolean gotBody;
boolean gotPath;
boolean gotQuery;
boolean gotUrl;
String httpMethod;
boolean hasBody;
boolean isFormEncoded;
boolean isMultipart;
String relativeUrl;
Headers headers;
MediaType contentType;
这里就有我们需要找的contentType,具体build()大家可以具体去看一下,这里就不详解了。
到这里了,我们拿到contentType然后再如何网络请求里面咧?
我们看一下OkHttpCall中的enqueue函数,里面有一行代码
call = rawCall = createRawCall();
这里就是retrofit需要使用的okhttp的call,看一下createRawCall函数,
private okhttp3.Call createRawCall() throws IOException {
Request request = serviceMethod.toRequest(args);
okhttp3.Call call = serviceMethod.callFactory.newCall(request);
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}
返回的确也是okhttp的call,这里的request从哪里来的呢?好了,我们又回到之前serviceMethod中的toRequest函数中了,
Request toRequest(Object... args) throws IOException {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
contentType, hasBody, isFormEncoded, isMultipart);
@SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
ParameterHandler[] handlers = (ParameterHandler[]) parameterHandlers;
int argumentCount = args != null ? args.length : 0;
if (argumentCount != handlers.length) {
throw new IllegalArgumentException("Argument count (" + argumentCount
+ ") doesn't match expected count (" + handlers.length + ")");
}
for (int p = 0; p < argumentCount; p++) {
handlers[p].apply(requestBuilder, args[p]);
}
return requestBuilder.build();
}
哈哈,原来RequestBuilder就是最终我们配置的文件,进来一看一目了然,contentType映入眼帘,
RequestBuilder(String method, HttpUrl baseUrl, String relativeUrl, Headers headers,
MediaType contentType, boolean hasBody, boolean isFormEncoded, boolean isMultipart) {
this.method = method;
this.baseUrl = baseUrl;
this.relativeUrl = relativeUrl;
this.requestBuilder = new Request.Builder();
this.contentType = contentType;
this.hasBody = hasBody;
好了,build()函数就是组成okhttp request必备的一个步骤,我们就看我们需要了解contentType在哪里(这里大概原理就是把参数组成body,然后再返回,具体方法)
if (contentType != null) {
if (body != null) {
body = new ContentTypeOverridingRequestBody(body, contentType);
} else {
requestBuilder.addHeader("Content-Type", contentType.toString());
}
}
绕来绕去,终于找到你了!当我们的注解里面有那几个注解的时候,这里就直接把header中的contenttype替换成我们注解里面的。
那如果注解没有contentType咧?
void addHeader(String name, String value) {
if ("Content-Type".equalsIgnoreCase(name)) {
MediaType type = MediaType.parse(value);
if (type == null) {
throw new IllegalArgumentException("Malformed content type: " + value);
}
contentType = type;
} else {
requestBuilder.addHeader(name, value);
}
}
如果没有注解的话,我们再通过addHeader等方式来设置contentType。
contentType在retrofit中的流程就是这个样子,很绕,但是最终还是确定了我们的观点。
如果我们接口中,没有加上注解,我们会使用我们自定义好的content-type,如果有注解,我们就使用注解的content-type。
过程很复杂,结论很简单,以后在跟后端联调的时候,遇到什么content-type不对的问题,我们就可以很快找到问题。
当然retrofit还有其他的注解,有兴趣的同学可以去了解一下。