The outline

  • An overview of the
  • Basic usage
  • configuration
  • Summary of the foundation
  • Source article
  • Source code summary
  • thinking
  • The resources

An overview of the

In the process of development, the client often needs to carry out network communication to achieve data interaction with the server. This article starts with the library Retrofit to understand the basic flow of network communication. Turns your HTTP API into a Java interface. This means converting HTTP apis into Java interfaces. Then in practice we can make network requests through this transformed Java interface. Another thing you need to know about Retrofit before you read it is dynamic proxies and annotations (see Resources for this article).

Basic usage

Start with a quick look at the following documentation (the easiest way to learn about a library), the basics. First post the demo above the official document

Public interface GitHubService {@get ("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); } /** * Retrofit will help us to generate the GitHubService implementation class * the developer gets the GitHubService reference and calls the method */ Retrofit Retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .build(); GitHubService service = retrofit.create(GitHubService.class); */ Call<List<Repo>> repos = service. ListRepos ("octocat");Copy the code

From the demo above, you can get a glimpse of the basic use of Retrofit, which converts HTTP requests into our usual interface and declares HTTP requests through annotation declarations, which it supports

  • URL parameter replacement and support for query parameters
  • Objects can be converted to request bodies (e.g., JSON, protocol buffering)
  • Multipart request body and file upload

URL declaration The URL declaration dynamically updates the parts that can be replaced. The parts that can be replaced in annotations need to be wrapped with {and}. The replacement values need to be declared using @path

@get ("group/{id}/users") Call<List<User>> @get ("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId); /** * @query declaration parameters in the GET method will be added to the final requestUrl * form the link requestUrl? */ @get ("group/{id}/users") Call<List<User>> groupList(@path ("id") int groupId, @Query("sort") String sort); /** * @querymap; */ @get ("group/{id}/users") Call<List<User>> groupList(@path ("id") int groupId, @querymap Map<String, String> options);Copy the code

Request Body @Body lets you specify an object as the Body of an HTTP request

RequestBody */ @post ("users/new") Call<User> if there is no converter createUser(@Body User user);Copy the code

Form encoding and partial request Content-Type encoding

  • Form-encoded (Application/X-www-form-urlencoded) is coded using @formurlencoded, and key-value pairs are coded using @field
  • Multipart (multipart/form-data) is represented by @multipart and parts are declared by @part, Retrofit has a default converter to convert this way, and RequestBody can also handle serialization itself
@formurlencoded @post ("user/edit") Call< user > updateUser(@field ("first_name") String first, @Field("last_name") String last); @Multipart @PUT("user/photo") Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);Copy the code

The developer can use @headers to set the Headers. The Headers will not be rewritten. All Headers will be included in the request header. Similarly, request headers can be added dynamically using @Header. If @Header passes in a null value, it is omitted. If you do not want to omit it, use toString to avoid passing in null

@headers (" cache-control: max-age=640000") @get ("widget/list") Call< list < widget >> widgetList(); @Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App" }) @GET("users/{username}") Call<User> getUser(@Path("username") String username); @GET("user") Call<User> getUser(@Header("Authorization" String authorization)) @GET("users") Call<User> getUser(@HeaderMap Map<String, String> headers)Copy the code

Synchronous and asynchronous Call instances can be executed synchronously and asynchronously. Each Call example will only be used once, but using clone() will create a new example that can be used on Android, where network callbacks will be executed on the main thread. In the JVM, the callback will be executed in the same thread

configuration

Retrofit is a class that converts API interfaces into callable objects. By default, Retrofit will provide a reasonable default configuration for the platform used, while supporting customization. By default, Retrofit can only deserialize the HTTP ResponseBody to the ResponseBody type of OkHttp, and can only accept the RequestBody type of @body. But adding Converters supports other types, providing the following Converters (Gson, Jackson, Moshi, Protoful, Wire, Simple XML, JAXB, Scalars)

Retrofit = new retrofit.builder ().baseurl ("https://api.github.com/") .addConverterFactory(GsonConverterFactory.create()) .build(); Service = retrofit.create(githubservice.class); service.listRepos("zak");Copy the code

In addition, you can inherit the Converter.Factory class to customize the Converter to suit the developer’s needs

Summary of the foundation

These are some of the basic things that Retrofit documents tell developers about, and this is by far the most common web request library I use, but how does it encapsulate HTTP requests into an interface that developers are familiar with? How do YOU start a network request? With these two questions ready to start the source code

Source article

The basic way to read the source code, generally start from the call entry tracking, gradually clear up the call with chain, if the information obtained from the call entry method is less, it starts from the construction of the method. Create, configure, invoke, and that’s basically it (where some details are implemented, the understanding phase should be skipped instead of blocking understanding of the whole process). Look at Retrofit’s configuration and pass in the required parameters via the Builder, so skip this one. Using Retrofit, we call create(*.class) and pass in the interface’s class object (the class object is used to record the class members, interfaces, and so on), starting with create

Public <T> T create(final Class<T> service) {public <T> T create(final Class<T> service) {public <T> T create(final Class<T> service) { Since Retrofit is used through interfaces * 1, we need to determine whether the incoming Class object is an interface before using it and throw an exception * 2, we also need to determine whether the incoming interface contains a stereotype and throw an exception * 3, if the interface also contains an interface, 4. If you specify validation ahead of time when configuring Retrofit, the validation of the interface methods will also be validated for specific code. */ validateServiceInterface(service); /** * Returns a proxy instance (i.e. dynamic proxy) * at runtime, Generate the interface implementation Class and call InvocationHandler#invoke() */ return (T) proxy.newProxyInstance (service.getClassLoader(), new Class<? >[] {service}, new InvocationHandler() {private final Platform Platform = platform.get (); 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; /** * teradata * If java8 with default keyword modifier, execute as default * otherwise call loadServiceMethod, Invoke is called from the instance returned by this method and the argument */ return platform.isDefaultMethod(method)? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); }}); } /** * serviceMethodCache is a ConcurrentHashMap. Have return * 2, locked and through ServiceMethod. ParseAnnotations () after the instance is created to cache * so you need to see what was SerivceMethod * / ServiceMethod <? > loadServiceMethod(Method method) { ServiceMethod<? > result = serviceMethodCache.get(method); if (result ! = null) return result; synchronized (serviceMethodCache) { result = serviceMethodCache.get(method); if (result == null) { result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); } } return result; }Copy the code

Here, we trace back to a proxy object that can be retrieved from the interface via Retrofit#create. When the proxy object method is executed, it is the ServiceMethod#invode that is executed and returns the stereotype in the interface method. A ServiceMethod is an abstract class that contains a static method, parseAnnotations, and an abstract invoke() notation.

Abstract class ServiceMethod<T> {** * This method returns an HttpServiceMethod * 1, gets the RequestFactory * 2, determines whether the method return type of the input parameter is supported and whether it is null, Static <T> ServiceMethod<T> parseAnnotations(Retrofit Retrofit, Method method) { RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method); Type returnType = method.getGenericReturnType(); if (Utils.hasUnresolvableType(returnType)) { throw methodError( method, "Method return type must not include a type variable or wildcard: %s", returnType); } if (returnType == void.class) { throw methodError(method, "Service methods cannot return void."); } return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory); } abstract @nullable T invoke(Object[] args); Abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {... /** * The implementation of HttpServiceMethod * 1, constructs an OkHttpCall * 2, ADAPTS the call object (mainly Kotlin's coroutine) * finally returns the call object, The inner classes CallStuck, SuspendForResponse, and SuspendForBody override this method. These three classes are also HttpServiceMethod * to see if they are compatible with coroutines * and then you can see that there are many implementations of Adapt (e.g. RxJava2CallAdapter..) Is used to convert the Call object into the corresponding API, In order to call the Adapter and the source of this Adapter is the OkHttpCall -> HttpServiceMethod that can be passed in when you create Retrofit CallAdapter * Retrofit maintains a callAdapterFactories, Check by the return type of method * In addition, the adapt method always ends up calling.equeue(). */ @override final @nullable ReturnT invoke(Object[] args) {call <ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter); return adapt(call, args); }... }Copy the code

Parse to call.equeue(), which is okHttpCall.equeue ()

@Override public void enqueue(final Callback<T> callback) { Objects.requireNonNull(callback, "callback == null"); okhttp3.Call call; Throwable failure; synchronized (this) { if (executed) throw new IllegalStateException("Already executed."); executed = true; call = rawCall; failure = creationFailure; If (call == null && failure == null) {try {// Create okHttp3. call object Call = rawCall = createRawCall(); } catch (Throwable t) { throwIfFatal(t); failure = creationFailure = t; } } } if (failure ! = null) {// build okHttp3. Call exception returns callback.onFailure(this, failure); return; } if (canceled) {// A method that is a member of okhttp3, go further to call.cancel() when okhttp3 is parsed; Call.enqueue (new okHttp3.callback () {@override public void call.enqueue() onResponse(okhttp3.Call call, okhttp3.Response rawResponse) { Response<T> response; Response = parseResponse(rawResponse); } catch (Throwable e) { throwIfFatal(e); callFailure(e); return; } try {// Callback success result callback.onResponse(okHttpCall. this, response); } catch (Throwable t) { throwIfFatal(t); t.printStackTrace(); // TODO this is not great } } @Override public void onFailure(okhttp3.Call call, IOException e) { callFailure(e); } private void callFailure(Throwable e) {try {callback. OnFailure (okHttpCall.this, e); } catch (Throwable t) { throwIfFatal(t); t.printStackTrace(); // TODO this is not great } } }); }Copy the code

In the enqueue() code, there are two places to continue to explore: 1. Call = rawCall = createRawCall(); Response = parseResponse(rawResponse); The implementation of parsing the response data starts with the creation of the okHttp3.Call object

Private okHttp3. Call createRawCall() throws IOException {/** * Create an okHttp3. Call object with callFactory.newCall * this The callFactory is derived from the OkHttpCall constructor * and the OkHttpCall constructor is created in HttpServiceMethod#invoke() * in invoke when a specific interface method is called CallFactory is a construct passed to * HttpServiceMethod when passing through the HttpServiceMethod construct, Annotations are constructed from the process of parseAnnotations (constructing a subclass that overrides adapt()) SuspendForBody is passed in a callFactory, which is passed in at build time via Retrofit#Builder (if not, The default is OkHttpClient) * concludes that the callFactory is actually an OkHttpClient, Requestfactory.create (args) is then passed in newCall(), This method is to create the Request object (okhttp3. Request) * the requestFactory was ServiceMethod# invoke through requestFactory. ParseAnnotations () create * / okhttp3.Call call = callFactory.newCall(requestFactory.create(args)); if (call == null) { throw new NullPointerException("Call.Factory returned null."); } return call; }Copy the code

For another paragraph, look at the code for the RequestFactory

final class RequestFactory { static RequestFactory parseAnnotations(Retrofit retrofit, Return new Builder(retrofit, Method).build(); }... RequestFactory Build () {parseMethonAnnotation () {parseMethonAnnotation () {parseMethonAnnotation () {parseMethonAnnotation () {parseMethonAnnotation () // parseParameter parseParameter // parseParameterAnnotation parseParameter // parseParameterAnnotation parseParameter // parseParameterAnnotation RequestFactory return Builder.build (); }}Copy the code

RequestFactory has completed parsing in ServiceMethod#invoke, Callfactory.newcall (requestFactory.create(args))) can be assigned to the argument directly and returned by create(). Next look at the OkHttpCall#parseResponse method

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException { ResponseBody rawBody = rawResponse.body(); . ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody); Try {/ * * finally passed responseConverter. Convert () the response of the body to convert * then look at the source of * the same responnseConverter through OkHttpCall - > HttpServiceMethod * in HttpServiceMathod#parseAnnotation via createResponseConverter() -> Retrofit. ResponseBodyConverter () * this method is through the traversal in converterFactories suitable converter return (by callAdapter. ResponseType) * ConverterFactories are assignments in Retrofit#build, Will add the default coverter and receive a custom converter body * / T = responseConverter. Convert (catchingBody); return Response.success(body, rawResponse); } catch (RuntimeException e) { // If the underlying source threw an exception, propagate that rather than indicating it was // a runtime exception. catchingBody.throwIfCaught(); throw e; }}Copy the code

RequestFactory, reponseConverter, OkHttpCall So you get a sense of the subject flow of Retrofit, and one other thing that I found in reading Retrofit was that it made a distinction between Kotlin’s coroutines, To return by isKotlinSuspendFunction CallAdapted or SuspendForResponse/SuspendForBody, finally to get to know the logic conveniently see coroutines way of judgment First you need to clear, The method modified by Kotlin using the suspend keyword ends up producing a static method of a class whose last argument is of type Continuation. (This interface explains that, Annotations(HttpServiceMethod#parseAnnotations(), the part of the coroutine

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory RequestFactory) {/ / whether to obtain as coroutines function Boolean isKotlinSuspendFunction = RequestFactory. IsKotlinSuspendFunction; boolean continuationWantsResponse = false; boolean continuationBodyNullable = false; Annotation[] annotations = method.getAnnotations(); Type adapterType; if (isKotlinSuspendFunction) { Type[] parameterTypes = method.getGenericParameterTypes(); Type responseType = Utils.getParameterLowerBound( 0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]); if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) { // Unwrap the actual body  type from Response<T>. responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType); / / coroutines function will continuationWantsResponse set to true continuationWantsResponse = true; } else { // TODO figure out if type is nullable or not // Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class) // Find the entry for method // Determine if return type is nullable or not } adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType); annotations = SkipCallbackExecutorImpl.ensurePresent(annotations); } else { adapterType = method.getGenericReturnType(); } CallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method, adapterType, annotations); Type responseType = callAdapter.responseType(); if (responseType == okhttp3.Response.class) { throw methodError( method, "'" + getRawType(responseType).getName() + "' is not a valid response body type. Did you mean ResponseBody?" ); } if (responseType == Response.class) { throw methodError(method, "Response must include generic type (e.g., Response<String>)"); } // TODO support Unit for Kotlin? if (requestFactory.httpMethod.equals("HEAD") && ! Void.class.equals(responseType)) { throw methodError(method, "HEAD method must use Void as response type."); } Converter<ResponseBody, ResponseT> responseConverter = createResponseConverter(retrofit, method, responseType); okhttp3.Call.Factory callFactory = retrofit.callFactory; if (! isKotlinSuspendFunction) { return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter); } else if (continuationWantsResponse) { //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object. // Return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter); } else { //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object. return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>( requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable); }}Copy the code

As you can see from the code above, Retrofit uses isKotlinSuspendFunction to determine if a Method passed in is a coroutine function. Let’s see how it does that. ParseParameter in RequestFactory parses the method, first iterating through the annotations of the parseParameter (@path, @query, @field). Let’s take a look at this method

private @Nullable ParameterHandler<? > parseParameter( int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) { ParameterHandler<? > result = null; // Parse annotations if (Annotations! = null) { for (Annotation annotation : annotations) { ParameterHandler<? > annotationAction = parseParameterAnnotation(p, parameterType, annotations, annotation); if (annotationAction == null) { continue; } if (result ! = null) { throw parameterError( method, p, "Multiple Retrofit annotations found, only one allowed."); } result = annotationAction; }} if (result == null) {if (allowContinuation) {if (allowContinuation) {// If (allowContinuation) {// If (allowContinuation) {// If (allowContinuation) {// If (allowContinuation) {// If (allowContinuation) (utils.getrawType (parameterType) == Continuation. Class) {// isKotlinSuspendFunction = true; // isKotlinSuspendFunction = true; return null; } } catch (NoClassDefFoundError ignored) { } } throw parameterError(method, p, "No Retrofit annotation found."); } return result; }Copy the code

Once you know the origin of the isKotlinSuspendFunction, look at the SuspendResponse adapt() method

static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> { private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter; SuspendForResponse( RequestFactory requestFactory, okhttp3.Call.Factory callFactory, Converter<ResponseBody, ResponseT> responseConverter, CallAdapter<ResponseT, Call<ResponseT>> callAdapter) { super(requestFactory, callFactory, responseConverter); this.callAdapter = callAdapter; } @Override protected Object adapt(Call<ResponseT> call, Object[] args) {// Call adapter.adapt () call = callAdapter.adapt(call); Unchecked by Reflection inside RequestFactory. // Discard the last argument and force it to type Continnuation Continuation<Response<ResponseT>> continuation = (Continuation<Response<ResponseT>>) args[args.length - 1]; // See SuspendForBody for explanation about this try/ cat.try {/** * execute the extension function awaitResponse * this extends Call * In conjunction with the way coroutines are called Call#equeue * in onResponse via continuation.resume(response) returns success callback * in onFailure Continuation. ResumeWithExceptionn (t) returns failure callback * / return KotlinExtensions. AwaitResponse (call, continuation); } the catch (Exception e) {/ / problem is throwing an Exception return KotlinExtensions. SuspendAndThrow (e, continuation); }}}Copy the code

Source code summary

The process API for Retrofit is summarized after the reading path above: Use dynamic proxy with Builder to build the corresponding interface object, support to pass RequestFactory, CallAdapter, Converter at the same time will also have the default implementation Platform: HttpServiceMethod: java8 androidservicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod: java8 AndroidServicemethod Http interface methods, derived from ServiceMethod, are used to create and adapt CallAdapter requests that have coroutine compatible methods (the default return callStale, SuspendForResponse/SuspendForBody) these three inherit from HttpServiceMethod and are its internal class CallAdapter: Builder#addCallAdapterFactory add a custom CallAdapter OkHttpCall to determine whether the return value type is appropriate for different requests: Specific request encapsulation, call here okhttp3 actual Converter network request related API: response body Converter, default is ResponseBody, commonly used have GsonResponseBodyConverter CallBack: When an interface method is invoked to make a request, the InvocationHandler#invoke of the interface instance is invoked and Retrofit#serviceMethodCache is checked to see if there is a cached interface instance. Call HttpServiceMethod#invoke to adapt the request (based on the return value type of the interface method). The default is callstuck. The coroutne is SuspendForResponse/SuspendForBody. When these classes are built, the Converter is also built, and when it’s built, adapt() is called, which calls OkhttpCall’s enQueue for the actual network request, When the response is received, the Converter parses the returned response data and calls the CallBack interface

  • Parse before creating the HttpServiceMthod instance
  • Objects are created using the RequestFactory’s static method parseAnnotations() and are parsed before the Builder is built

2. Conversion process of Converter

  • ParseResponse is called in the onResponse callback of OkHttpCall#equeue, where the response data is parsed via responseConverter
  • ResponseConverter matches in converterFactories by returning the value type

3. Adaptation process of CallAdapter

  • The callAdapterFactories return the value type through the interface method for matching

How to support Kotlin coroutine suspend function

  • RequestFactory parses the method’s arguments to determine (the last type of the argument is continuation. class) RequestFactory#isKotlinSuspendFunction is considered to be Kotlin’s suspend function
  • RequestFactory#isKotlinSuspendFunction returns the corresponding SuspendForResponse/SuspendForBody in HttpServiceMethod, Which adapt () by KotlinExtensions. AwaitResponse to complete coroutines execution and through the Callback callbacks

thinking

After looking at Retrofit’s code, I realized that the main skill points involved were All Java-related, such as dynamic proxies, understanding classes, defining and parsing annotations, determining interfaces, and even determining coroutines in Kotlin. These are some of the best features in Java. The library makes good use of these features, so users will find them easy to use. But the web-related processes THAT I wanted to understand were not very obvious in this library. Now you can understand that other articles say Retrofit is a library that wraps okHTTP into a layer and makes it easy for developers to use. The next step is to look at the processes in OkHTTP. See if he can give a more complete picture of the network request process.

The resources

Official documentation AboBack – Be sure to understand Retrofit’s most detailed source code parsing! Start with an interview question about enumeration, dynamic proxy principle