Hi, I’m Halliday, and I’m going to take a look at some of the technical frameworks that go from tree trunk to twig. This article will introduce you to the open source project Retrofit.
The passage is about 2800 words and takes about 8 minutes to read.
Retrofit source code is based on the latest version 2.9.0
prepare
Retrofit makes web calls as simple as a RESTful design style, such as:
interface WanApi {
// Use annotations to mark the network request mode get, POST, parameter path, query, etc
@GET("article/list/{page}/json")
Call<WanArticleBean> articleList(@Path("page") int page);
}
Copy the code
For another example, the Spring Boot framework on the back end eliminates a lot of configuration by using the convention over configuration idea. It also applies this style to the RestController network interface.
@RestController
public class ActivityController {
@Autowired
private ActivityService activityService;
// Use annotations to mark the network request mode and incoming parameters
@GetMapping("/goods")
public ResultEntity queryGoods(@RequestParam("page") int page) {
returnactivityService.queryGoods(page); }}Copy the code
Retrofit’s underlying network implementation is based on OKHTTP, and it doesn’t have many classes of its own. The core point is the dynamic proxy. The proxy pattern simply provides a proxy for an object that enhances or controls access to it. Let’s first look at static and dynamic proxies
Static agent
The agent is completed at compile time
- Source level: manually write proxy class, APT generation proxy class
- Bytecode level: bytecode generation at compile time,
Chestnuts, for example,
interfaceTo make money{
void makeMoney(int income);
}
classSmall meatimplementsTo make money{ / / the delegate class
@Override
public void makeMoney(int income) {
System.out.println("Let's do it. Make a profit."+ income); }}classagentimplementsTo make money{ / / the proxy classMake money XXR;publicAgent (to make money XXR) {this.xxr = xxr;
}
@Override
public void makeMoney(int income) {
if (income < 1000 _0000) { // Control access
System.out.println("Only" + income + "Go back and wait for the announcement.");
} else{ xxr.makeMoney(income); }}}public static void main(String[] args) {make money XXR =newSmall meat (); Make money JJR =newAgent (XXR); jjr.makeMoney(100 _0000); // Output: only 1000000, go back and wait for notification
jjr.makeMoney(1000 _0000); // Output: start shooting, earn 10000000
}
Copy the code
Why do the proxy and delegate classes implement the same interface? To ensure that the internal structure of the proxy class is as consistent as possible with that of the delegate class, so that all operations on the proxy class can be transferred to the delegate class, and the proxy class only focuses on enhancement and control.
A dynamic proxy
Bytecode is generated at runtime, such as proxy. newProxyInstance and CGLIB
Proxy.newProxyInstance is provided by Java and can only be used as a Proxy for an interface.
CGLIB is more powerful and can also proxy for ordinary classes. The underlying layer is based on ASM (ASM uses a saX-like parser for progressive scanning to improve performance).
Chestnuts, for example,
classWorking standardimplements InvocationHandler {XXR to make money;publicCooperation criteria (making money XXR) {this.xxr = xxr;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int income = (int) args[0];
if (income < 1000 _0000) { // Control access
System.out.println("Only" + income + "Go back and wait for the announcement.");
return null;
} else {
returnmethod.invoke(xxr, args); }}}public static void main(String[] args) {make money XXR =newSmall meat (); Cooperation standard =newCooperation Standards (XXR);$Proxy0 extends Proxy implements money. $Proxy0 extends Proxy implements money
// Then reflect to create an example bd, that is, a temporary business expansionMake money bd = (make money) proxy.newProxyInstance (make money.class.getClassLoader (),newClass[]{make money. Class}, standard);// Call makeMoney and internally forward to the invoke cooperative standard
bd.makeMoney(100 _0000);
bd.makeMoney(1000 _0000);
}
Copy the code
Instead of creating a specific agent class (such as a broker or brokerage firm) in advance to implement the money-making interface, dynamic proxies first draw up a cooperation standard (InvocationHandler), then create the agent class $Proxy0 (bytecode) at runtime, and then reflect to create the example business extension. It’s more flexible.
Once you’ve learned about dynamic proxies, you’re ready to start your Retrofit journey
The trunk
Simple to use
Introducing dependencies,
implementation 'com. Squareup. Okhttp3: okhttp: 3.14.9'
implementation 'com. Squareup. Retrofit2: retrofit: 2.9.0'
implementation 'com. Squareup. Retrofit2: converter - gson: 2.9.0'
implementation 'com. Google. Code. Gson: gson: 2.8.6'
Copy the code
Define the interface WanApi,
interface WanApi {
// Use annotations to mark the type of the network request get with the parameter PATH
@GET("article/list/{page}/json")
Call<WanArticleBean> articleList(@Path("page") int page);
}
Copy the code
Make a request,
class RetrofitActivity extends AppCompatActivity {
final String SERVER = "https://www.xxx.com/";
@Override
protected void onCreate(Bundle savedInstanceState) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SERVER) // Specify the server address
.addConverterFactory(GsonConverterFactory.create()) // Deserialize the data into entities with gson
.build();
// The runtime generates a class (bytecode) that implements the WanApi interface, and reflects the creation examples
WanApi wanApi = retrofit.create(WanApi.class);
// Get the Retrofit call, which encapsulates the OKHTTP call
Call<WanArticleBean> call = wanApi.articleList(0);
// Ask to join the team
call.enqueue(new Callback<WanArticleBean>() {
@Override
public void onResponse(Call<WanArticleBean> call, Response<WanArticleBean> response) {
// Get the data entity
WanArticleBean bean = response.body();
// Unlike okHTTP, Retrofit already uses a Handler to help us switch back to the main thread
mBinding.tvResult.setText("" + bean.getData().getDatas().size());
}
@Override
public void onFailure(Call<WanArticleBean> call, Throwable t) {}}); }}Copy the code
Realize the principle of
Since Retrofit is based on OKHTTP, hardy has done an analysis of the network process in the okHTTP series, so this article ignores the network implementation and only focuses on some processing of Retrofit itself. The construction of Retrofit objects is a simple Builder mode, we directly look at create.
//Retrofit.java
public <T> T create(final Class<T> service) {
/ / verification
validateServiceInterface(service);
return (T)
// Dynamic proxy
Proxy.newProxyInstance(
service.getClassLoader(), // Class loader
newClass<? >[] {service},// A group of interfaces
new InvocationHandler() {
// Determine the Android and JVM platforms and their versions
private final Platform platform = Platform.get();
@Override
public Object invoke(Object proxy, Method method, Object[] args){
// If the method is an Object method, execute it directly
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
//isDefaultMethod: check whether it is the default method supported by Java8 from the beginning
return platform.isDefaultMethod(method)
? platform.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(method).invoke(args); // Let's focus here}}); }Copy the code
Proxy.newProxyInstance dynamic Proxy, run time will generate a class (bytecode) such as $ProxyN, implement the incoming interface WanApi, override the interface method and forward to the InvocationHandler invoke, as follows (pseudo code),
class $ProxyN extends Proxy implements WanApi{
Call<WanArticleBean> articleList(@Path("page") int page){
// Forward to invocationHandler
invocationHandler.invoke(this,method,args); }}Copy the code
Let’s look at the validateServiceInterface validation logic,
//Retrofit.java
private void validateServiceInterface(Class
service) {
// Check: WanApi is not an interface, throw an exception...
// Check: WanApi cannot have generic parameters and cannot implement other interfaces...
if (validateEagerly) { // This function is disabled by default
Platform platform = Platform.get();
for (Method method : service.getDeclaredMethods()) { // Iterate over the WanApi method
// Not a default method, and not a static method
if(! platform.isDefaultMethod(method) && ! Modifier.isStatic(method.getModifiers())) {// Load the method ahead of time (check if it works)loadServiceMethod(method); }}}}Copy the code
If validate15, it will check all methods of WanApi at once and load them in. It can be opened in debug mode to detect errors in advance. For example, if @body is set in @get request, such errors will raise an exception:
java.lang.IllegalArgumentException: Non-body HTTP method cannot contain @Body.
loadServiceMethod
LoadServiceMethod (method).invoke(args)
//Retrofit.java
// Cache, using thread-safe ConcurrentHashMap
finalMap<Method, ServiceMethod<? >> serviceMethodCache =newConcurrentHashMap<>(); ServiceMethod<? > loadServiceMethod(Method method) { ServiceMethod<? > result = serviceMethodCache.get(method);//WanApi's articleList method is cached and returns directly
if(result ! =null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
// Parse the articleList annotation, create a ServiceMethod and cache it
result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); }}return result;
}
Copy the code
Follow up ServiceMethod parseAnnotations,
//ServiceMethod.java
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
/ / 1.
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
// Check: the articleList method return type cannot use wildcards and void...
/ / 2.
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
Copy the code
Look at 1. RequestFactory. ParseAnnotations,
//RequestFactory.java
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}
class Builder {
RequestFactory build(a) {
// Parse method annotations such as GET
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
// Omit all tests...
// Parse parameter annotations such as Path
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = newParameterHandler<? >[parameterCount];for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
parameterHandlers[p] =
parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}
// Omit all tests...
return new RequestFactory(this); }}Copy the code
After get RequestFactory, 2. HttpServiceMethod. ParseAnnotations, HttpServiceMethod responsible for adaptation and conversion process, adjust the call of the interface methods for HTTP calls,
//HttpServiceMethod.java
//ResponseT response type such as WanArticleBean, ReturnT return type such as Call
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {
// Omit kotlin coroutine logic...
Annotation[] annotations = method.getAnnotations();
// Iterate to find the appropriate adapter
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
// Get the response type, such as WanArticleBean
Type responseType = callAdapter.responseType();
// Iterate to find the appropriate converter
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
}
Copy the code
So we finally return a CallAdapted, and we see the CallAdapted,
//CallAdapted extends HttpServiceMethod extends ServiceMethod
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) {
super(requestFactory, callFactory, responseConverter);
this.callAdapter = callAdapter;
}
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
/ / adapter
returncallAdapter.adapt(call); }}Copy the code
So who is this CallAdapter instance? Let’s go back to retrofit.Builder,
//Retrofit.Builder.java
public Retrofit build(a) {
Executor callbackExecutor = this.callbackExecutor;
// If the thread pool is not set, set the default MainThreadExecutor for the Android platform (with Handler to switch callback back to the main thread).
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor();
}
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
Add the default DefaultCallAdapterFactory / /
callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));
}
Copy the code
To create specific instances of CallAdapter DefaultCallAdapterFactory this factory,
//DefaultCallAdapterFactory.java
publicCallAdapter<? ,? > get(Type returnType, Annotation[] annotations, Retrofit retrofit) {final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);
// If the SkipCallbackExecutor annotation is specified, there is no need to switch back to the main thread
final Executor executor =
Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
? null
: callbackExecutor;
return newCallAdapter<Object, Call<? > > () {@Override
public Type responseType(a) {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
// By default, returns a Call wrapped in the main pool, and its enqueue will use the main pool's execute
return executor == null ? call : newExecutorCallbackCall<>(executor, call); }}; }Copy the code
invoke
Invoke (HttpServiceMethod) (HttpServiceMethod);
//HttpServiceMethod.java
final ReturnT invoke(Object[] args) {
// Finally see okhttp!
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}
class CallAdapted<ResponseT.ReturnT> extends HttpServiceMethod<ResponseT.ReturnT> {
private final CallAdapter<ResponseT, ReturnT> callAdapter;
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
// Package OkHttpCall as an ExecutorCallbackCall with the previous adapter
returncallAdapter.adapt(call); }}Copy the code
Then the request team, ExecutorCallbackCall. The enqueue – > OkHttpCall. The enqueue,
//ExecutorCallbackCall.java
void enqueue(final Callback<T> callback) {
delegate.enqueue(
new Callback<T>() {
@Override
public void onResponse(Call<T> call, final Response<T> response) {
// Switch the callback back to the main thread
callbackExecutor.execute(
() -> {
callback.onResponse(ExecutorCallbackCall.this, response);
});
/ /...
}
@Override
public void onFailure(Call<T> call, final Throwable t) {}}); }//OkHttpCall.java
void enqueue(final Callback<T> callback) {
/ / okhttp logic
okhttp3.Call call;
call.enqueue(new okhttp3.Callback() {
void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
callback.onResponse(OkHttpCall.this, response); }})}Copy the code
You finally get through the process and you go back and you look at the flow chart again, and you see
twigs
CallAdapter
The CallAdapter adapter is used to accommodate the return type. For example, it also supports the use of Rxjava, coroutines,
interface WanApi {
//Call
@GET("article/list/{page}/json")
Call<WanArticleBean> articleList(@Path("page") int page);
/ / Rxjava, need addCallAdapterFactory (RxJavaCallAdapterFactory. The create ())
@GET("article/list/{page}/json")
Observable<WanArticleBean> articleListRx(@Path("page") int page);
}
Copy the code
Converter
The Converter Converter is used to convert parameter types, such as formatting a Long timestamp into a string and passing it to the back end.
interface WanApi {
//Long cur Current time
@GET("article/list/{page}/json")
Call<WanArticleBean> articleList(@Path("page") int page, @Query("cur") Long cur);
}
class TimeConverter implements Converter<Long.String> {
private SimpleDateFormat mFormat = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
@Override
public String convert(Long value) throws IOException {
if (value > 1_000_000_000_000L) {// milliseconds is not very precise
return mFormat.format(new Date(value));
}
returnString.valueOf(value); }}class TimeConverterFactory extends Converter.Factory {
@Override
publicConverter<? , String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {if (type == Long.class) {
// Use a custom TimeConverter
return new TimeConverter();
}
return super.stringConverter(type, annotations, retrofit);
}
public static Converter.Factory create(a) {
return newTimeConverterFactory(); }}/ / then set up line, addConverterFactory (TimeConverterFactory. The create ())
Copy the code
Dynamic url replacement
We pass in an HttpUrl object when we build Retrofit, and the instance is always there, so we can reflect and modify its fields such as host to dynamically replace the server address.
String SERVER = "https://www.xxx.com/";
HttpUrl httpUrl = HttpUrl.get(SERVER);
Retrofit retrofit = new Retrofit.Builder()
//.baseUrl(SERVER)
.baseUrl(httpUrl) / / use HttpUrl
.build();
Copy the code
The end of the
See you next time ~😆
Series of articles:
- Okhttp for the Series
- Glide from the Series “Never Forget”
The resources
- GitHub & Documentation & API
- Imooc – Hack Retrofit
- Brief – An architectural perspective on Retrofit’s role, principles, and implications
- Brief – JAVA dynamic proxy
- Csdn-cglib (Code Generation Library) details
- Zhihu – What is the function of Java dynamic proxy?
Welcome to pay attention to the original technology public account: Halliday EI