In the previous article, we analyzed in detail the annotation parsing and dynamic proxy implementation in Retrofit. This article will continue to study the core source code of Retrofit in depth, focusing on how Retrofit handles network requests and responses.
network request
When using Retrofit to initiate a network request, we can define an interface and use Retrofit annotations to describe the request in this interface, and Retrofit will automatically generate a proxy object that implements the interface. When we call the method of this proxy object, Retrofit will construct a Request object according to the description of the annotation, and use OkHttp to send the Request.
In Retrofit, we Retrofit#execute
can Retrofit#enqueue
send requests through the or method. The difference between these two methods is that execute
the method will block the current thread until the request is completed, while enqueue
the method will add the request to the OkHttp request queue and notify us through the callback when the request is completed.
Let's first look at the implementation of execute
the method :
public <T> T execute(Call<T> call) throws IOException {
Utils.validateServiceInterface(call.request().tag(), call.request().url().toString());
return (T) callAdapter(call, call.request().tag()).adapt(call).execute();
}
In this method, the interface is first verified to ensure that the interface is valid. Then we get the adapter based on the request's Tag and URL callAdapter
, and use the adapter to execute the request.
The role of the adapter is to adapt the parameters of the request to a form that OkHttp can recognize, and to adapt the response of OkHttp to the form we need. Retrofit provides a series of adapters, including Call adapter, RxJava adapter, CompletableFuture adapter, etc.
Let's take a look at the implementation of callAdapter
the method :
private CallAdapter<?, ?> callAdapter(Call<?> call, Object tag) {
Type responseType = call.request().method().equals("HEAD")
? Void.class
: getParameterUpperBound(0, (ParameterizedType) call.request().tag());
return callAdapter(tag, responseType);
}
In this method, we first judge the response type according to the request method. If it is the HEAD method, then the response type is Void; otherwise, we will obtain the response type of the request through reflection, and use this response type to obtain the adapter.
The way to get the adapter is callAdapter
:
public <R, T> CallAdapter<R, T> callAdapter(Object tag, Type returnType) {
// ...
for (CallAdapter.Factory factory : adapterFactories) {
CallAdapter<?, ?> adapter = factory.get(returnType, annotations, this);
if (adapter != null) {
return (CallAdapter<R, T>) adapter;
}
}
// ...
}
In this method, we iterate through all adapter factories, trying to get an adapter. When getting the adapter, we pass the request's response type, annotation, and Retrofit instance as parameters. Each adapter factory will judge whether these parameters meet its own adaptation conditions, and if so, return an adapter instance, otherwise return null. After traversing all adapter factories, if no adapter has been obtained, an exception will be thrown.
After getting the adapter, we can use the adapter to execute the request. In the adapter, we will convert the request parameters into the OkHttp Request object, and convert the OkHttp Response object into the response type we need. For specific implementation, please refer to CallAdapter
the interface .
For enqueue
the method , we can first look at enqueue
the implementation of the method:
public <T> void enqueue(Call<T> call, Callback<T> callback) {
Utils.validateServiceInterface(call.request().tag(), call.request().url().toString());
callAdapter(call, call.request().tag()).adapt(call).enqueue(new CallbackRunnable<>(callback));
}
In this method, we first check the interface, then obtain the adapter according to the requested Tag and URL, and use the adapter to execute the request. The difference is that in enqueue
the method , we pass a Callback object as a parameter enqueue
into the method of the adapter, so that the callback will notify us after the request is completed.
In the adapter, we can see the implementation of enqueue
the method :
public void enqueue(final Callback<T> callback) {
delegate.enqueue(new Callback<Response<T>>() {
@Override public void onResponse(Call<Response<T>> call, Response<Response<T>> response) {
Response<T> body;
try {
body = response.body();
} catch (Throwable t) {
if (response.code() == 204) {
body = null;
} else {
callback.onFailure(call, t);
return;
}
}
if (response.isSuccessful()) {
callback.onResponse(call, Response.success(body, response.raw()));
} else {
callback.onFailure(call, Response.error(response.errorBody(), response.raw()));
}
}
@Override public void onFailure(Call<Response<T>> call, Throwable t) {
callback.onFailure(call, t);
}
});
}
In this method, we will convert the incoming Callback object into an Callback<Response<T>>
object , and use this object to call the enqueue method of OkHttp. After the request is completed, we will convert the Response object of OkHttp into the Response object of Retrofit, and judge the result of the request according to the response code. If the response code indicates that the request is successful, then we call onResponse
the method ; otherwise, we call onFailure
the method of the Callback object.
response processing
In Retrofit, we can describe our desired request format and response format by defining an interface and using annotations. For example, we can use @GET
annotations to describe a GET request, use @Query
annotations to describe request parameters, use @Body
annotations to describe the request body, use @Headers
annotations to describe request headers, and so on.
When executing a request, Retrofit will automatically generate a corresponding request object based on these annotations, and convert the request object into an OkHttp Request object. When receiving the response, Retrofit will convert the OkHttp Response object into a corresponding response object, and convert the data in the response object into the data type we need. These conversions are done through Retrofit's converters. Retrofit provides two converters by default: GsonConverterFactory
and JacksonConverterFactory
. We can also customize a converter to achieve our desired data conversion.
In the construction method of the Retrofit class, we can see that Retrofit uses Platform.get()
the method to obtain the default converter factory of the current running platform and add it converterFactories
to . We can then use addConverterFactory
the method to add custom converter factories.
public Retrofit(Builder builder) {
// ...
if (builder.converterFactories == null) {
converterFactories.add(Platform.get().defaultConverterFactory());
} else {
converterFactories.addAll(builder.converterFactories);
}
// ...
}
public interface Platform {
// ...
Converter.Factory defaultConverterFactory();
}
In execute
the method, we call the adapter's adapt method to execute the request and convert the returned Call object into a response object. During the conversion process, we will select the corresponding converter according to the response type for conversion. For specific conversion implementation, please refer to Converter
the interface and Converter.Factory
interface provided by Retrofit.
public <T> T execute(Call<T> call) throws IOException {
// ...
Response<T> response = call.execute();
if (response.isSuccessful()) {
return response.body();
} else {
Converter<ResponseBody, ErrorResponse> converter = retrofit.responseBodyConverter(
ErrorResponse.class, new Annotation[0]);
throw new ApiException(converter.convert(response.errorBody()));
}
}
@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
public <T> T adapt(Call<T> call) {
return (T) new OkHttpCall<>(requestFactory, callFactory, converter, call);
}
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
return nextResponseBodyConverter(null, type, annotations);
}
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
Objects.requireNonNull(type, "type == null");
Objects.requireNonNull(annotations, "annotations == null");
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
return (Converter<ResponseBody, T>) converter;
}
}
throw new IllegalArgumentException(
"Could not locate ResponseBody converter for " + type + " with annotations " + Arrays.toString(annotations));
}
The above is the core code for processing responses in Retrofit. When we execute a request, Retrofit will first convert the request into an OkHttp Request object and send it out, and then wait for the response to return. When the response is returned, Retrofit will convert the response into a response object and convert the data in the response object into the data type we expect. In this process, we can use the converters provided by Retrofit to customize the data conversion rules.
Here's an example of how to use Retrofit to send a GET request and convert the JSON data in the response into a Java object:
public interface ApiService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiService apiService = retrofit.create(ApiService.class);
Call<List<Repo>> call = apiService.listRepos("smallmarker");
List<Repo> repos = call.execute().body();
In the example above, we first created a Retrofit instance using the Retrofit builder, specifying the base URL for the request and the converter factory. Then, create
we create a ApiService
proxy object of by calling the method. Finally, we call listRepos
the method to send a GET request.
In the example above, we used Retrofit's GsonConverterFactory
to convert the JSON data in the response body into Java objects. For specific implementation, please refer to GsonConverterFactory
the classes .
public final class GsonConverterFactory extends Converter.Factory {
private final Gson gson;
private GsonConverterFactory(Gson gson) {
this.gson = gson;
}
public static GsonConverterFactory create() {
return create(new Gson());
}
public static GsonConverterFactory create(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
return new GsonConverterFactory(gson);
}
@Override
public @Nullable Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
@Override
public @Nullable Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations,
Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);
}
}
It can be seen that GsonConverterFactory
the Retrofit Converter.Factory
class , and responseBodyConverter
the method and requestBodyConverter
method are rewritten. In responseBodyConverter
the method , we convert the JSON data in the response body into a Java object, and requestBodyConverter
in the method, we convert the Java object into the JSON data in the request body.
In addition GsonConverterFactory
to , Retrofit also provides other converters, such as JacksonConverterFactory、MoshiConverterFactory
etc. We can choose the converter that suits us according to our needs.
In general, the core code of network request and response processing in Retrofit is very concise and clear. We only need to describe the request and response by defining the interface, then use Retrofit's dynamic proxy mechanism to convert the interface into an actual implementation class, and specify the request and response converter through the Retrofit configuration. This method greatly simplifies the process of network requests, allowing us to focus more on the processing of business logic.