preface
The OkHttp source code has been shared before (and is very detailed and long), but in real projects Retrofit is often used to do network request work. Retrofit is RESTful and essentially a wrapper around OkHttp. Today we’ll take a closer look at Retrofit’s source code and design ideas with a few questions.
1. Use method
Take a look at the official usage.
public final class SimpleService {
public static final String API_URL = "https://api.github.com";
public static class Contributor {
public final String login;
public final int contributions;
public Contributor(String login, int contributions) {
this.login = login;
this.contributions = contributions; }}public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
}
public static void main(String... args) throws IOException {
// Create a very simple REST adapter which points the GitHub API.
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl(API_URL)
.client(new OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS).build())
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create an instance of our GitHub API interface.
GitHub github = retrofit.create(GitHub.class);
// Create a call instance for looking up Retrofit contributors.
Call<List<Contributor>> call = github.contributors("square"."retrofit");
// Fetch and print a list of the contributors to the library.
List<Contributor> contributors = call.execute().body();
for (Contributor contributor : contributors) {
System.out.println(contributor.login + "(" + contributor.contributions + ")"); }}}Copy the code
It can be summarized in three simple steps:
- build
retrofit
Instance. - build
API
Interface instance. - Execute the request and parse the response.
2. Process analysis
Let’s analyze its process according to how it is used.
2.1 Building Retrofit instances
You can see from the usage that the builder pattern is used to build instances.
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl(API_URL)
.client(new OkHttpClient().newBuilder().connectTimeout(10. TimeUnit.SECONDS).build()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();Copy the code
I’m not going to do this, but let’s look at some parameters.
public static final class Builder {
// The actual request invocation, such as okHttp3.okHttpClient
private @Nullable okhttp3.Call.Factory callFactory;
// Basic URL, such as domain name
private @Nullable HttpUrl baseUrl;
// List of data converters
private final List<Converter.Factory> converterFactories = new ArrayList<>();
// Request a list of adapters
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
Copy the code
2.2 Building AN API Instance
According to the official usage description, we will put our API methods in an interface, and then set the request parameters through annotations. When used, this interface is instantiated through the retrofit.create(Class
) method and then its methods are called. Such as:
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
}
// Instantiate the API interface
GitHub github = retrofit.create(GitHub.class);
// Call an API in the interface
Call<List<Contributor>> call = github.contributors("square"."retrofit");
Copy the code
Take a look at the source code
public <T> T create(final Class<T> service) {
// Verify the API service
validateServiceInterface(service);
return (T)
// Dynamic proxy mode is used here, service is the proxy class
// Why did Todo use dynamic proxies and what are the benefits? How about something else?
Proxy.newProxyInstance(
service.getClassLoader(),
newClass<? >[] {service},new InvocationHandler() {
private final Object[] emptyArgs = new Object[0];
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable 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); } args = args ! =null ? args : emptyArgs;
Platform platform = Platform.get();
// If it is not the default method, return a ServiceMethod via the loadServiceMethod() method and call the invoke method
returnplatform.isDefaultMethod(method) ? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); }}); }Copy the code
Did two things:
- Verify our API interface class.
- Use dynamic proxies to instantiate the API interface at run time.
private void validateServiceInterface(Class
service) {
// Service must be interface, otherwise an exception is thrown
if(! service.isInterface()) {throw new IllegalArgumentException("API declarations must be interfaces."); }... Omit code...// Whether to immediately validate all methods in the API interface, set by the user, default is false
if (validateEagerly) {
Platform platform = Platform.get();
// Iterate over all methods defined in the service
for (Method method : service.getDeclaredMethods()) {
// The loadServiceMethod method is executed if the method is not the system default and the method decorator is not static
if(! platform.isDefaultMethod(method) && ! Modifier.isStatic(method.getModifiers())) {// Load the request method.loadServiceMethod(method); }}}}Copy the code
From this we can also see that our API methods must be in the method interface. If you start validating the interface, it iterates through all its declared methods, filtering out the system default and static methods, and then loadServiceMethod(method) is executed.
To expand:
GetMethods (): Returns all public methods declared by a class or interface and inherited from superclasses and superinterfaces. GetDeclaredMethods (): Returns class declared methods, including public, protected, default (package), but not inherited methods. Therefore, getDeclaredMethods is faster than getMethods, especially in complex classes such as the Activity class.
Ultimately, a ServiceMethod is loaded using the loadServiceMethod(method) method.
See HttpServiceMethod. ParseAnnotations () method, which I simplified, as follows:
HttpServiceMethod.java
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {
// Get the annotation information for the method
Annotation[] annotations = method.getAnnotations();
/ / adapter type, is to Retrofit addCallAdapterFactory add the type of ().
Type adapterType;
// Return type of the method
adapterType = method.getGenericReturnType();
// instantiate a CallAdapter object
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
// Check responseType and throw an exception if it fails
Type responseType = callAdapter.responseType();
// Instantiate a Converter object that converts okHttp3. ResponseBody to ResponseT
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
// Instead of suspending the kotlin method, return callStuck, which calls callAdapter.adapter
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
}
Copy the code
After the ServiceMethod is instantiated, the invoke method is called.
HttpServiceMethod.java
@Override
final @Nullable ReturnT invoke(Object[] args) {
// Create an OkHttpCall request
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
// Then call the adapt method, CallAdapted overwrites the adapt method, and calls the callAdapter.adapt(call) method
return adapt(call, args);
}
protected abstract @Nullable ReturnT adapt(Call<ResponseT> call, Object[] args);
Copy the code
As you can see from the above code, the Invoke method instantiates a Call request and then invokes an Adapter method. In this case, Adapter is an abstract method, so the implementation of the method depends on its implementation class, CallAdapter. CallAdapter is through here. AddCallAdapterFactory CallAdapter add () method, and according to the platform provided by default DefaultCallAdapterFactory CallAdapter, The Adapter method is executed, and Call
2.3 Execute the request and parse the response
In the previous step, we instantiated the API interface and adapted the request through a CallAdapter, resulting in a Call
The next step is to execute the Call
For example, as described in the initial usage method:
// You've got the Call
> object, run the Call and get List
List<Contributor> contributors = call.execute().body();
Copy the code
Execute executes the synchronous request to get the Response and then the request body.
OkHttpCall.java
@Override
public Response<T> execute(a) throws IOException {
okhttp3.Call call;
synchronized (this) {
// Determine if the request has been executed and throw an exception if it has been executed
if (executed) throw new IllegalStateException("Already executed.");
executed = true;
// Get the original request and create okHttp3.call with createRawCall()
call = getRawCall();
}
if (canceled) {
call.cancel();
}
// Execute the request and parse the response, converting okHttp3.response to retrofit2.response
return parseResponse(call.execute());
}
private okhttp3.Call createRawCall(a) throws IOException {
// Construct the original request
okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}
/** * parse the response, which translates okhttp3.response to retrofit2.response */
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {... Omit code...try {
// Convert to the desired type using Converter
T body = responseConverter.convert(catchingBody);
return Response.success(body, rawResponse);
} catch(RuntimeException e) { ... Omit code... }Copy the code
As you can see from the source code, the actual work of the request is done through okhttp. Retrofit is responsible for the request and response conversion, converting retroFIT2. Call to okHttp3. Call. Convert okhttp3.response to retrofit2.response.
3. Why introduce CallAdapter and Converter?
If you’re familiar with okHttp, you know that when we make a request, we convert the request to a Call object using the okHttpClient.newCall (Request) method, and then execute the Call object to get the response.
But Retrofit doesn’t just support calls. It can adapt requests to observables, which can be used in conjunction with RxJava2. This is adapted by CallAdapter work, for example by default DefaultCallAdapterFactory transform the request into the Call < Object >, The RxJava2CallAdapter transforms the request into Observable
Going back to okHttp, in most business cases we deserialize the response body as an object for easy invocation. Obviously Retrofit has this in mind, so it provides a GsonConverterFactory by default to help us with this step of deserialization. This is done through The Converter, which also allows users to customize.
4. How does the CallAdapter work?
As a request adapter, we split the CallAdapter workflow into three steps: Add, match, and work.
add
The request adapterFactory class can be added using the addCallAdapterFactory(callAdapterFactory) method, which is then saved in the callAdapterFactories list. In addition, the Retrofit will upon the request of the Platform to add default, the adapter, such as: DefaultCallAdapterFactory, etc., also joined the callAdapterFactories list.
matching
Think about it: All added request adapters are stored in the callAdapterFactories list, so how do you match them in the actual request?
In HttpServiceMethod. ParseAnnotations () method, we have to instantiate a CallAdapter object. (Please refer back to 2.2 Building API Instances for details.)
HttpServiceMethod.java
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {
// instantiate a CallAdapter objectCallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method, adapterType, annotations); ... omit code// Instead of suspending the kotlin method, return callStuck, which calls callAdapter.adapter
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
}
Copy the code
The matching work is done in the createCallAdapter() method, working its way down to retrofit.nextCallAdapter () :
Retrofit.java
publicCallAdapter<? ,? > nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
// Find a matching CallAdapter based on the method return value type and annotation informationCallAdapter<? ,? > adapter = callAdapterFactories.get(i).get(returnType, annotations,this);
if(adapter ! =null) {
returnadapter; }} ·· omit code ···// If no matching CallAdapter is found, an exception is thrown
throw new IllegalArgumentException(builder.toString());
}
Copy the code
If no matching CallAdapter is found, throw an IllegalArgumentException.
work
Once you find the matching CallAdapter, all that’s left is to see how it works.
As described in the previous matching procedure, after a matching callAdapter is found, a CallStuck object is instantiated from it.
static final class CallAdapted<ResponseT.ReturnT> extends HttpServiceMethod<ResponseT.ReturnT> {
private final CallAdapter<ResponseT, ReturnT> callAdapter;
CallAdapted(
RequestFactory requestFactory,
okhttp3.Call.Factory callFactory,
Converter<ResponseBody, ResponseT> responseConverter,
CallAdapter<ResponseT, ReturnT> callAdapter) {
// pass responseConverter to the parent class.
super(requestFactory, callFactory, responseConverter);
this.callAdapter = callAdapter;
}
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
returncallAdapter.adapt(call); }}Copy the code
CallAdapted simply inherits HttpServiceMethod and overwrites the Adapt method. In other words, the adapt method of the CallAdapter object that we matched in the previous step will be executed.
Such as matching to the CallAdapter DefaultCallAdapterFactory, finally is the adapt methods of execution, the specific code details here did not show, interested students, please consult.
In addition, I’m showing a case where the Kotlin suspend function is not supported. Of course, even if the Kotlin suspend function is implemented, the adapt method of its subclass is the same process.
5. How does Converter work?
As data converters, we also divide the Converter workflow into three steps: Add, match, and work.
add
The data ConverterFactory class can be added using the addConverterFactory(ConverterFactory) method, which will be saved in the converterFactories list. In addition, Retrofit adds default data converters based on Platform, such as OptionalConverterFactory, to the converterFactories list.
matching
With the above described 4. CallAdapter works, in the same HttpServiceMethod. ParseAnnotations () method, will instantiate an object of the Converter.
Match the job in createResponseConverter () method, step by step, finally to Retrofit. The nextResponseBodyConverter () method:
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
// Find a matching Converter by converting the type to the annotation informationConverter<ResponseBody, ? > converter = converterFactories.get(i).responseBodyConverter(type, annotations,this);
if(converter ! =null) {
//noinspection unchecked
return(Converter<ResponseBody, T>) converter; }} ·· omit code ···// If no matching Converter is found, an exception is thrown
throw new IllegalArgumentException(builder.toString());
}
Copy the code
To recap, we traverse the converterFactories list to find a matching Converter by converting the type and annotation information, and throw an IllegalArgumentException if none is found.
work
As described above in 4. How the CallAdapter works, we instantiate a Callstuck by finding a matching Converter. The difference is that we pass responseConverter to the parent class, HttpServiceMethod, and when it calls the Invoke method, we instantiate an OkHttpCall object via responseConverter. Finally, the OkHttpCall object is passed to the Adapter method for execution.
When performing the request, eventually OkHttpCall execution parseResponse to parse the response, call responseConverter. Convert () method, transform ResponseBody data type we want.
6. Describe which design patterns are used
Dynamic proxy mode
Retrofit internally uses dynamic proxy + reflection to take the request parameters defined by the user in the interface to build the actual request. I won’t go into details here, but refer back to 2.2 Building an API Instance.
Why use dynamic proxies to get API methods?
I don’t know if you have this question, but why do our API methods need to be defined in interface? Why use dynamic proxy + reflection to retrieve request parameters?
Retrofit is designed in a RESTful style and annotated to define request parameters for API methods and put those API methods into the interface. Since an interface cannot be instantiated, a dynamic proxy is used to instantiate the API interface at run time to get the method’s request parameters.
Further:
Decoupled, isolating the real business from Retrofit. The user only needs to define the request parameters through the annotation method, while the actual request is built internally through Retrofit.
Now, why do I put it in interface? I’m sure you already have the answer.
The strategy pattern
The strategy pattern can be used when there are multiple approaches to the same type of problem, but the specific behavior is different.
For example: request adapters in Retrofit
- Abstract strategy:
CallAdapter
. - Specific implementation of the strategy:
DefaultCallAdapterFactory.get()
,RxJava2CallAdapter
.
Provide the default request adapter, but also support user customization, in line with the open and closed principle, to achieve good scalability.
Adapter mode
Will help us to build the actual request Retrofit, internal to transform the request into the Call by the default DefaultCallAdapterFactory < Object >, Retrofit also support other platforms at the same time, such as in order to fit RxJava features, Transform the request into Observable
- Target:
Call<Object>
.Observable<Object>
. - An object that needs to be adapted:
OkHttpCall
. - Adapter:
DefaultCallAdapterFactory.get()
,RxJava2CallAdapter
.
Factory method pattern
Let’s take the Converter for example.
- Abstract factory:
Converter.Factory
. - Specific factory:
GsonConverterFactory
,BuiltInConverters
And so on. - Abstract products:
Converter
. - Specific products:
GsonResponseBodyConverter
,GsonRequestBodyConverter
,ToStringConverter
And so on.
There is no specific analysis of each class, interested students can consult.
Builder model
The Builder pattern was used when building an instance of Retrofit. The Builder pattern appears quite frequently in open source libraries, and it is common to use the builder pattern because it makes sense to provide a variety of parameters and methods to suit the needs of different users.
7. What pits have you stepped in during use?
One day I changed BaseUrl and found that the server kept returning 404 when I requested the interface. However, when I tried to debug the interface with Postman, I found that the interface was fine, so I guessed that there was something wrong with my code.
The problem turned out to be that the API had been removed when the full URL was concatenated.
Base URL: http://example.com/api/
Endpoint: /foo/bar/
Result: http://example.com/foo/bar/
Copy the code
The correct Endpoint is as follows: The Endpoint does not start with a slash.
Base URL: http://example.com/api/
Endpoint: foo/bar/
Result: http://example.com/api/foo/bar/
Copy the code
conclusion
This article, in the form of a few questions, takes a look at Retrofit’s source code and design ideas, and hopefully you’ll have a better understanding of the source code. Retrofit is essentially just a encapsulation of okHttp. The goal is definitely to make web requests easier to make. It’s amazing how Many design patterns Jake Wharton has used.
That concludes the source code parsing for Retrofit.
This article is a bit different from my previous source code analysis articles. I have deliberately simplified a lot of the code to make it easier for you to read, but I have also commented the code in detail, which you can refer to in my Retrofit detailed code comments.
If you take a closer look at the OkHttp source code analysis, you can check out my other (very detailed and long) OkHttp source code analysis article.
In fact, the biggest purpose of sharing articles is to wait for someone to point out my mistakes. If you find any mistakes, please point them out without reservation and consult with an open mind.
In addition, if you think this article is good and helpful, please give me a like as encouragement, thank you ~ Peace~!