retrofit之content-type浅析

背景:在做问题反馈的时候,后端是用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> T create(final Class<T> 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<Object, Object> serviceMethod =
              (ServiceMethod<Object, Object>) loadServiceMethod(method);
          OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
          return serviceMethod.callAdapter.adapt(okHttpCall);
        }
      });
}

这里面使用动态代理的方式,得到需要代理的类,就是我们加注解的类,其他就不细看了,我们直接看看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<Object>[] handlers = (ParameterHandler<Object>[]) 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还有其他的注解,有兴趣的同学可以去了解一下。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
retrofit-spring-boot-starter是一个用于整合Retrofit库和Spring Boot的starter项目,它可以简化在Spring Boot中使用Retrofit的配置和使用。 以下是retrofit-spring-boot-starter的使用方法: 1. 在你的Spring Boot项目的pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>com.github.lianjiatech</groupId> <artifactId>retrofit-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> ``` 2. 创建一个接口,用于定义Retrofit的API接口。例如: ```java import retrofit2.http.GET; import retrofit2.http.Path; public interface MyApi { @GET("/users/{username}") User getUser(@Path("username") String username); } ``` 3. 在你的Spring Boot应用程序中,使用`@Autowired`注解将Retrofit的API接口注入到你的代码中。例如: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import retrofit2.Retrofit; @Service public class MyService { private final MyApi myApi; @Autowired public MyService(Retrofit retrofit) { this.myApi = retrofit.create(MyApi.class); } public User getUser(String username) { return myApi.getUser(username); } } ``` 4. 现在你可以在你的代码中使用`MyService`来调用Retrofit的API接口了。例如: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { private final MyService myService; @Autowired public MyController(MyService myService) { this.myService = myService; } @GetMapping("/users/{username}") public User getUser(@PathVariable String username) { return myService.getUser(username); } } ``` 以上是retrofit-spring-boot-starter的基本用法。你可以根据自己的需求进行配置和使用。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值