Retrofit2 完全解析 探索與okhttp之間的關系

轉載請標明出處:?
http://blog.csdn.net/lmj623565791/article/details/51304204;?
本文出自:【張鴻洋的博客】

?

之前寫了個okhttputils的工具類,然后有很多同學詢問這個工具類和retrofit什么區別,于是上了下官網,發現其底層對網絡的訪問默認也是基于okhttp,不過retrofit非常適合于restful url格式的請求,更多使用注解的方式提供功能。

既然這樣,我們本篇博文首先研究其所提供的常用的用法:

  • 一般的get、post請求
  • 動態url,動態參數設置,各種注解的使用
  • 上傳文件(單文件,多文件上傳等)
  • 下載文件等(這個不推薦retrofit去做,具體看下文)

此外,由于其內部提供了ConverterFactory用于對返回的requestBody進行轉化和特殊的requestBody的構造,所以本文也包含:

  • 如何自定義ConverterFactory

最后呢,因為其源碼并不復雜,本文將對源碼進行整體的介紹,即

  • retrofit 源碼分析

ok,說這么多,既然需要restful url,我只能撿起我那個半桶水的spring?mvc 搭建一個服務端的小例子~~

最后本文使用版本:

compile 'com.squareup.retrofit2:retrofit:2.0.2'
  • 1
  • 1

主要是源碼解析,自定義Converter.Factory等一些細節的探索。

恩,寫完后,發現本文很長,中途請沒事站起來走兩步。

retrofit2官網地址:https://github.com/square/retrofit/

二、retrofit 用法示例

(1)一般的get請求

retrofit在使用的過程中,需要定義一個接口對象,我們首先演示一個最簡單的get請求,接口如下所示:

public interface IUserBiz
{@GET("users") Call<List<User>> getUsers(); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到有一個getUsers()方法,通過@GET注解標識為get請求,@GET中所填寫的value和baseUrl組成完整的路徑,baseUrl在構造retrofit對象時給出。

下面看如何通過retrofit完成上述的請求:

Retrofit retrofit = new Retrofit.Builder().baseUrl("http://192.168.31.242:8080/springmvc_users/user/").addConverterFactory(GsonConverterFactory.create()).build();
IUserBiz userBiz = retrofit.create(IUserBiz.class);
Call<List<User>> call = userBiz.getUsers();call.enqueue(new Callback<List<User>>(){@Overridepublic void onResponse(Call<List<User>> call, Response<List<User>> response) { Log.e(TAG, "normalGet:" + response.body() + ""); } @Override public void onFailure(Call<List<User>> call, Throwable t) { } });

依然是構造者模式,指定了baseUrlConverter.Factory,該對象通過名稱可以看出是用于對象轉化的,本例因為服務器返回的是json格式的數組,所以這里設置了GsonConverterFactory完成對象的轉化。

ok,這里可以看到很神奇,我們通過Retrofit.create就可以拿到我們定義的IUserBiz的實例,調用其方法即可拿到一個Call對象,通過call.enqueue即可完成異步的請求。

具體retrofit怎么得到我們接口的實例的,以及對象的返回結果是如何轉化的,我們后面具體分析。

這里需要指出的是:

  1. 接口中的方法必須有返回值,且比如是Call<T>類型
  2. .addConverterFactory(GsonConverterFactory.create())這里如果使用gson,需要額外導入:

    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
    

    當然除了gson以外,還提供了以下的選擇:

    Gson: com.squareup.retrofit2:converter-gson
    Jackson: com.squareup.retrofit2:converter-jackson
    Moshi: com.squareup.retrofit2:converter-moshi
    Protobuf: com.squareup.retrofit2:converter-protobuf
    Wire: com.squareup.retrofit2:converter-wire
    Simple XML: com.squareup.retrofit2:converter-simplexml
    Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
    

    當然也支持自定義,你可以選擇自己寫轉化器完成數據的轉化,這個后面將具體介紹。

  3. 既然call.enqueue是異步的訪問數據,那么同步的訪問方式為call.execute,這一點非常類似okhttp的API,實際上默認情況下內部也是通過okhttp3.Call實現。

那么,通過這么一個簡單的例子,應該對retrofit已經有了一個直觀的認識,下面看更多其支持的特性。

(2)動態的url訪問@PATH

文章開頭提過,retrofit非常適用于restful url的格式,那么例如下面這樣的url:

//用于訪問zhy的信息
http://192.168.1.102:8080/springmvc_users/user/zhy
//用于訪問lmj的信息
http://192.168.1.102:8080/springmvc_users/user/lmj
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

即通過不同的username訪問不同用戶的信息,返回數據為json字符串。

那么可以通過retrofit提供的@PATH注解非常方便的完成上述需求。

我們再定義一個方法:

public interface IUserBiz
{@GET("{username}") Call<User> getUser(@Path("username") String username); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到我們定義了一個getUser方法,方法接收一個username參數,并且我們的@GET注解中使用{username}聲明了訪問路徑,這里你可以把{username}當做占位符,而實際運行中會通過@PATH("username")所標注的參數進行替換。

那么訪問的代碼很類似:

//省略了retrofit的構建代碼
Call<User> call = userBiz.getUser("zhy");
//Call<User> call = userBiz.getUser("lmj");
call.enqueue(new Callback<User>()
{@Override public void onResponse(Call<User> call, Response<User> response) { Log.e(TAG, "getUsePath:" + response.body()); } @Override public void onFailure(Call<User> call, Throwable t) { } }); 

?

(3)查詢參數的設置@Query

看下面的url

http://baseurl/users?sortby=username
http://baseurl/users?sortby=id
  • 1
  • 2
  • 1
  • 2

即一般的傳參,我們可以通過@Query注解方便的完成,我們再次在接口中添加一個方法:

public interface IUserBiz
{@GET("users") Call<List<User>> getUsersBySort(@Query("sortby") String sort); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

訪問的代碼,其實沒什么寫的:

//省略retrofit的構建代碼
Call<List<User>> call = userBiz.getUsersBySort("username");
//Call<List<User>> call = userBiz.getUsersBySort("id");
//省略call執行相關代碼
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

ok,這樣我們就完成了參數的指定,當然相同的方式也適用于POST,只需要把注解修改為@POST即可。

對了,我能剛才學了@PATH,那么會不會有這樣嘗試的沖動,對于剛才的需求,我們這么寫:

 @GET("users?sortby={sortby}")Call<List<User>> getUsersBySort(@Path("sortby") String sort);
  • 1
  • 2
  • 1
  • 2

乍一看別說好像有點感覺,哈,實際上運行是不支持的~估計是@ Path的定位就是用于url的路徑而不是參數,對于參數還是選擇通過@Query來設置。

(4)POST請求體的方式向服務器傳入json字符串@Body

大家都清楚,我們app很多時候跟服務器通信,會選擇直接使用POST方式將json字符串作為請求體發送到服務器,那么我們看看這個需求使用retrofit該如何實現。

再次添加一個方法:

public interface IUserBiz
{@POST("add") Call<List<User>> addUser(@Body User user); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

提交的代碼其實基本都是一致的:

//省略retrofit的構建代碼Call<List<User>> call = userBiz.addUser(new User(1001, "jj", "123,", "jj123", "jj@qq.com")); //省略call執行相關代碼
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

ok,可以看到其實就是使用@Body這個注解標識我們的參數對象即可,那么這里需要考慮一個問題,retrofit是如何將user對象轉化為字符串呢?下文將詳細解釋~

下面對應okhttp,還有兩種requestBody,一個是FormBody,一個是MultipartBody,前者以表單的方式傳遞簡單的鍵值對,后者以POST表單的方式上傳文件可以攜帶參數,retrofit也二者也有對應的注解,下面繼續~

(5)表單的方式傳遞鍵值對@FormUrlEncoded

這里我們模擬一個登錄的方法,添加一個方法:

public interface IUserBiz
{@POST("login") @FormUrlEncoded Call<User> login(@Field("username") String username, @Field("password") String password); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

訪問的代碼:

//省略retrofit的構建代碼
Call<User> call = userBiz.login("zhy", "123");
//省略call執行相關代碼
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

ok,看起來也很簡單,通過@POST指明url,添加FormUrlEncoded,然后通過@Field添加參數即可。

(6)單文件上傳@Multipart

下面看一下單文件上傳,依然是再次添加個方法:

public interface IUserBiz
{@Multipart @POST("register") Call<User> registerUser(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password); } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這里@MultiPart的意思就是允許多個@Part了,我們這里使用了3個@Part,第一個我們準備上傳個文件,使用了MultipartBody.Part類型,其余兩個均為簡單的鍵值對。

使用:

File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoRequestBody); Call<User> call = userBiz.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

ok,這里感覺略為麻煩。不過還是蠻好理解~~多個@Part,每個Part對應一個RequestBody。

這里插個實驗過程,其實我最初對于文件,也是嘗試的@Part RequestBody,因為@Part("key"),然后傳入一個代表文件的RequestBody,我覺得更加容易理解,后來發現試驗無法成功,而且查了下issue,給出了一個很奇怪的解決方案,這里可以參考:retrofit#1063。

給出了一個類似如下的方案:

public interface ApiInterface {@Multipart @POST ("/api/Accounts/editaccount") Call<User> editUser (@Header("Authorization") String authorization, @Part("file\"; filename=\"pp.png") RequestBody file , @Part("FirstName") RequestBody fname, @Part("Id") RequestBody id); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到對于文件的那個@Partvalue竟然寫了這么多奇怪的東西,而且filename竟然硬編碼了~~這個不好吧,我上傳的文件名竟然不能動態指定。

為了文件名不會被寫死,所以給出了最上面的上傳單文件的方法,ps:上面這個方案經測試也是可以上傳成功的。

恩,這個奇怪方案,為什么這么做可行,下文會給出非常詳細的解釋。

最后看下多文件上傳~

(7)多文件上傳@PartMap

再添加一個方法~~~

 public interface IUserBiz{@Multipart @POST("register") Call<User> registerUser(@PartMap Map<String, RequestBody> params, @Part("password") RequestBody password); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這里使用了一個新的注解@PartMap,這個注解用于標識一個Map,Map的key為String類型,代表上傳的鍵值對的key(與服務器接受的key對應),value即為RequestBody,有點類似@Part的封裝版本。

執行的代碼:

File file = new File(Environment.getExternalStorageDirectory(), "messenger_01.png");RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String,RequestBody> photos = new HashMap<>();
photos.put("photos\"; filename=\"icon.png", photo); photos.put("username", RequestBody.create(null, "abc")); Call<User> call = userBiz.registerUser(photos, RequestBody.create(null, "123")); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以看到,可以在Map中put進一個或多個文件,鍵值對等,當然你也可以分開,單獨的鍵值對也可以使用@Part,這里又看到設置文件的時候,相對應的key很奇怪,例如上例"photos\"; filename=\"icon.png",前面的photos就是與服務器對應的key,后面filename是服務器得到的文件名,ok,參數雖然奇怪,但是也可以動態的設置文件名,不太影響使用~~

(8)下載文件

這個其實我覺得直接使用okhttp就好了,使用retrofit去做這個事情真的有點瞎用的感覺~~

增加一個方法:

@GET("download")
Call<ResponseBody> downloadTest();
  • 1
  • 2
  • 1
  • 2

調用:

Call<ResponseBody> call = userBiz.downloadTest();
call.enqueue(new Callback<ResponseBody>()
{@Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { InputStream is = response.body().byteStream(); //save file } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { } });

?

可以看到可以返回ResponseBody,那么很多事都能干了~~

but,也看出這種方式下載感覺非常雞肋,并且onReponse回調雖然在UI線程,但是你還是要處理io操作,也就是說你在這里還要另外開線程操作,或者你可以考慮同步的方式下載。

最后還是建議使用okhttp去下載,例如使用okhttputils.

有人可能會問,使用okhttp,和使用retrofit會不會造成新建多個OkHttpClient對象呢,其實是可設置的,參考下文。

ok,上面就是一些常用的方法,當然還涉及到一些沒有介紹的注解,但是通過上面這么多方法的介紹,再多一二個注解的使用方式,相信大家能夠解決。

三、配置OkHttpClient

這個需要簡單提一下,很多時候,比如你使用retrofit需要統一的log管理,給每個請求添加統一的header等,這些都應該通過okhttpclient去操作,比如addInterceptor

例如:

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor()//log,統一的header等
{@Overridepublic okhttp3.Response intercept(Chain chain) throws IOException { return null; } }).build();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

或許你需要更多的配置,你可以單獨寫一個OkhttpClient的單例生成類,在這個里面完成你所需的所有的配置,然后將OkhttpClient實例通過方法公布出來,設置給retrofit。

設置方式:

Retrofit retrofit = new Retrofit.Builder().callFactory(OkHttpUtils.getClient()).build();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

callFactory方法接受一個okhttp3.Call.Factory對象,OkHttpClient即為一個實現類。

四、retrofit 源碼解析

ok,接下來我們隊retrofit的源碼做簡單的分析,首先我們看retrofit如何為我們的接口實現實例;然后看整體的執行流程;最后再看詳細的細節;

(1)retrofit如何為我們的接口實現實例

通過上文的學習,我們發現使用retrofit需要去定義一個接口,然后可以通過調用retrofit.create(IUserBiz.class);方法,得到一個接口的實例,最后通過該實例執行我們的操作,那么retrofit如何實現我們指定接口的實例呢?

其實原理是:動態代理。但是不要被動態代理這幾個詞嚇唬到,Java中已經提供了非常簡單的API幫助我們來實現動態代理。

看源碼前先看一個例子:

public interface ITest
{@GET("/heiheihei") public void add(int a, int b); } public static void main(String[] args) { ITest iTest = (ITest) Proxy.newProxyInstance(ITest.class.getClassLoader(), new Class<?>[]{ITest.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Integer a = (Integer) args[0]; Integer b = (Integer) args[1]; System.out.println("方法名:" + method.getName()); System.out.println("參數:" + a + " , " + b); GET get = method.getAnnotation(GET.class); System.out.println("注解:" + get.value()); return null; } }); iTest.add(3, 5); }

?

輸出結果為:

方法名:add
參數:3 , 5
注解:/heiheihei
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

可以看到我們通過Proxy.newProxyInstance產生的代理類,當調用接口的任何方法時,都會調用InvocationHandler#invoke方法,在這個方法中可以拿到傳入的參數,注解等。

試想,retrofit也可以通過同樣的方式,在invoke方法里面,拿到所有的參數,注解信息然后就可以去構造RequestBody,再去構建Request,得到Call對象封裝后返回。

ok,下面看retrofit#create的源碼:

public <T> T create(final Class<T> service) {return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object... args) throws Throwable { }); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

哈,和上面對應。到這里,你應該明白retrofit為我們接口生成實例對象并不神奇,僅僅是使用了Proxy這個類的API而已,然后在invoke方法里面拿到足夠的信息去構建最終返回的Call而已。

哈,其實真正的動態代理一般是有具體的實現類的,只是在這個類調用某個方法的前后去執行一些別的操作,比如開事務,打log等等。當然,本博文并不需要涉及這些詳細的內容,如果你希望詳細去了解,可以搜索關鍵字:Proxy InvocationHandler

(2)retrofit整體實現流程

4.2.1 Retrofit的構建

這里依然是通過構造者模式進行構建retrofit對象,好在其內部的成員變量比較少,我們直接看build()方法。

public Builder() {this(Platform.get());
}public Retrofit build() { if (baseUrl == null) { throw new IllegalStateException("Base URL required."); } okhttp3.Call.Factory callFactory = this.callFactory; if (callFactory == null) { callFactory = new OkHttpClient(); } Executor callbackExecutor = this.callbackExecutor; if (callbackExecutor == null) { callbackExecutor = platform.defaultCallbackExecutor(); } // Make a defensive copy of the adapters and add the default Call adapter. List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories); adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor)); // Make a defensive copy of the converters. List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories); return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories, callbackExecutor, validateEagerly); }

?

  • baseUrl必須指定,這個是理所當然的;
  • 然后可以看到如果不著急設置callFactory,則默認直接new OkHttpClient(),可見如果你需要對okhttpclient進行詳細的設置,需要構建OkHttpClient對象,然后傳入;
  • 接下來是callbackExecutor,這個想一想大概是用來將回調傳遞到UI線程了,當然這里設計的比較巧妙,利用platform對象,對平臺進行判斷,判斷主要是利用Class.forName("")進行查找,具體代碼已經被放到文末,如果是Android平臺,會自定義一個Executor對象,并且利用Looper.getMainLooper()實例化一個handler對象,在Executor內部通過handler.post(runnable),ok,整理憑大腦應該能構思出來,暫不貼代碼了。
  • 接下來是adapterFactories,這個對象主要用于對Call進行轉化,基本上不需要我們自己去自定義。
  • 最后是converterFactories,該對象用于轉化數據,例如將返回的responseBody轉化為對象等;當然不僅僅是針對返回的數據,還能用于一般備注解的參數的轉化例如@Body標識的對象做一些操作,后面遇到源碼詳細再描述。

ok,總體就這幾個對象去構造retrofit,還算比較少的~~

4.2.2 具體Call構建流程

我們構造完成retrofit,就可以利用retrofit.create方法去構建接口的實例了,上面我們已經分析了這個環節利用了動態代理,而且我們也分析了具體的Call的構建流程在invoke方法中,下面看代碼:

public <T> T create(final Class<T> service) {Utils.validateServiceInterface(service);//...return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object... args){ //... ServiceMethod serviceMethod = loadServiceMethod(method); OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); }

主要也就三行代碼,第一行是根據我們的method將其包裝成ServiceMethod,第二行是通過ServiceMethod和方法的參數構造retrofit2.OkHttpCall對象,第三行是通過serviceMethod.callAdapter.adapt()方法,將OkHttpCall進行代理包裝;

下面一個一個介紹:

  • ServiceMethod應該是最復雜的一個類了,包含了將一個method轉化為Call的所有的信息。
#Retrofit class
ServiceMethod loadServiceMethod(Method method) {ServiceMethod result;synchronized (serviceMethodCache) {result = serviceMethodCache.get(method);if (result == null) {result = new ServiceMethod.Builder(this, method).build(); serviceMethodCache.put(method, result); } } return result; } #ServiceMethod public ServiceMethod build() { callAdapter = createCallAdapter(); responseType = callAdapter.responseType(); if (responseType == Response.class || responseType == okhttp3.Response.class) { throw methodError("'" + Utils.getRawType(responseType).getName() + "' is not a valid response body type. Did you mean ResponseBody?"); } responseConverter = createResponseConverter(); for (Annotation annotation : methodAnnotations) { parseMethodAnnotation(annotation); } int parameterCount = parameterAnnotationsArray.length; parameterHandlers = new ParameterHandler<?>[parameterCount]; for (int p = 0; p < parameterCount; p++) { Type parameterType = parameterTypes[p]; if (Utils.hasUnresolvableType(parameterType)) { throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", parameterType); } Annotation[] parameterAnnotations = parameterAnnotationsArray[p]; if (parameterAnnotations == null) { throw parameterError(p, "No Retrofit annotation found."); } parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); } return new ServiceMethod<>(this); }

?

直接看build方法,首先拿到這個callAdapter最終拿到的是我們在構建retrofit里面時adapterFactories時添加的,即為:new ExecutorCallbackCall<>(callbackExecutor, call),該ExecutorCallbackCall唯一做的事情就是將原本call的回調轉發至UI線程。

接下來通過callAdapter.responseType()返回的是我們方法的實際類型,例如:Call<User>,則返回User類型,然后對該類型進行判斷。

接下來是createResponseConverter拿到responseConverter對象,其當然也是根據我們構建retrofit時,addConverterFactory添加的ConverterFactory對象來尋找一個合適的返回,尋找的依據主要看該converter能否處理你編寫方法的返回值類型,默認實現為BuiltInConverters,僅僅支持返回值的實際類型為ResponseBodyVoid,也就說明了默認情況下,是不支持Call<User>這類類型的。

接下來就是對注解進行解析了,主要是對方法上的注解進行解析,那么可以拿到httpMethod以及初步的url(包含占位符)。

后面是對方法中參數中的注解進行解析,這一步會拿到很多的ParameterHandler對象,該對象在toRequest()構造Request的時候調用其apply方法。

ok,這里我們并沒有去一行一行查看代碼,其實意義也不太大,只要知道ServiceMethod主要用于將我們接口中的方法轉化為一個Request對象,于是根據我們的接口返回值確定了responseConverter,解析我們方法上的注解拿到初步的url,解析我們參數上的注解拿到構建RequestBody所需的各種信息,最終調用toRequest的方法完成Request的構建。

  • 接下來看OkHttpCall的構建,構造函數僅僅是簡單的賦值
OkHttpCall(ServiceMethod<T> serviceMethod, Object[] args) {this.serviceMethod = serviceMethod;this.args = args;}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
  • 最后一步是serviceMethod.callAdapter.adapt(okHttpCall)

我們已經確定這個callAdapter是ExecutorCallAdapterFactory.get()對應代碼為:

final class ExecutorCallAdapterFactory extends CallAdapter.Factory {final Executor callbackExecutor;ExecutorCallAdapterFactory(Executor callbackExecutor) {this.callbackExecutor = callbackExecutor;}@Overridepublic CallAdapter<Call<?>> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { if (getRawType(returnType) != Call.class) { return null; } final Type responseType = Utils.getCallResponseType(returnType); return new CallAdapter<Call<?>>() { @Override public Type responseType() { return responseType; } @Override public <R> Call<R> adapt(Call<R> call) { return new ExecutorCallbackCall<>(callbackExecutor, call); } }; }

可以看到adapt返回的是ExecutorCallbackCall對象,繼續往下看:

static final class ExecutorCallbackCall<T> implements Call<T> {final Executor callbackExecutor;final Call<T> delegate;ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {this.callbackExecutor = callbackExecutor; this.delegate = delegate; } @Override public void enqueue(final Callback<T> callback) { if (callback == null) throw new NullPointerException("callback == null"); delegate.enqueue(new Callback<T>() { @Override public void onResponse(Call<T> call, final Response<T> response) { callbackExecutor.execute(new Runnable() { @Override public void run() { if (delegate.isCanceled()) { // Emulate OkHttp's behavior of throwing/delivering an IOException on cancellation. callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled")); } else { callback.onResponse(ExecutorCallbackCall.this, response); } } }); } @Override public void onFailure(Call<T> call, final Throwable t) { callbackExecutor.execute(new Runnable() { @Override public void run() { callback.onFailure(ExecutorCallbackCall.this, t); } }); } }); } @Override public Response<T> execute() throws IOException { return delegate.execute(); } }

可以看出ExecutorCallbackCall僅僅是對Call對象進行封裝,類似裝飾者模式,只不過將其執行時的回調通過callbackExecutor進行回調到UI線程中去了。

4.2.3 執行Call

在4.2.2我們已經拿到了經過封裝的ExecutorCallbackCall類型的call對象,實際上就是我們實際在寫代碼時拿到的call對象,那么我們一般會執行enqueue方法,看看源碼是怎么做的

首先是ExecutorCallbackCall.enqueue方法,代碼在4.2.2,可以看到除了將onResponse和onFailure回調到UI線程,主要的操作還是delegate完成的,這個delegate實際上就是OkHttpCall對象,我們看它的enqueue方法

 @Override
public void enqueue(final Callback<T> callback) { okhttp3.Call call; Throwable failure; synchronized (this) { if (executed) throw new IllegalStateException("Already executed."); executed = true; try { call = rawCall = createRawCall(); } catch (Throwable t) { failure = creationFailure = t; } } if (failure != null) { callback.onFailure(this, failure); return; } if (canceled) { call.cancel(); } call.enqueue(new okhttp3.Callback() { @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) throws IOException { Response<T> response; try { response = parseResponse(rawResponse); } catch (Throwable e) { callFailure(e); return; } callSuccess(response); } @Override public void onFailure(okhttp3.Call call, IOException e) { try { callback.onFailure(OkHttpCall.this, e); } catch (Throwable t) { t.printStackTrace(); } } private void callFailure(Throwable e) { try { callback.onFailure(OkHttpCall.this, e); } catch (Throwable t) { t.printStackTrace(); } } private void callSuccess(Response<T> response) { try { callback.onResponse(OkHttpCall.this, response); } catch (Throwable t) { t.printStackTrace(); } } }); }

沒有任何神奇的地方,內部實際上就是okhttp的Call對象,也是調用okhttp3.Call.enqueue方法。

中間對于okhttp3.Call的創建代碼為:

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; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到,通過serviceMethod.toRequest完成對request的構建,通過request去構造call對象,然后返回.

中間還涉及一個parseResponse方法,如果順利的話,執行的代碼如下:

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException
{ResponseBody rawBody = rawResponse.body();ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);T body = serviceMethod.toResponse(catchingBody);return Response.success(body, rawResponse);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通過serviceMethod對ResponseBody進行轉化,然后返回,轉化實際上就是通過responseConverter的convert方法。

#ServiceMethodT toResponse(ResponseBody body) throws IOException {return responseConverter.convert(body);}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

ok,關于responseConverter后面還會細說,不用擔心。

到這里,我們整個源碼的流程分析就差不多了,目的就掌握一個大體的原理和執行流程,了解下幾個核心的類。

那么總結一下:

  • 首先構造retrofit,幾個核心的參數呢,主要就是baseurl,callFactory(默認okhttpclient),converterFactories,adapterFactories,excallbackExecutor。
  • 然后通過create方法拿到接口的實現類,這里利用Java的Proxy類完成動態代理的相關代理
  • 在invoke方法內部,拿到我們所聲明的注解以及實參等,構造ServiceMethod,ServiceMethod中解析了大量的信息,最痛可以通過toRequest構造出okhttp3.Request對象。有了okhttp3.Request對象就可以很自然的構建出okhttp3.call,最后calladapter對Call進行裝飾返回。
  • 拿到Call就可以執行enqueue或者execute方法了

ok,了解這么多足以。

下面呢,有幾個地方需要注意,一方面是一些特殊的細節;另一方面就是Converter

五、retrofit中的各類細節

(1)上傳文件中使用的奇怪value值

第一個問題涉及到文件上傳,還記得我們在單文件上傳那里所說的嗎?有種類似于hack的寫法,上傳文件是這么做的?

public interface ApiInterface {@Multipart @POST ("/api/Accounts/editaccount") Call<User> editUser (@Part("file_key\"; filename=\"pp.png"),@Part("username") String username); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

首先我們一點明確,因為這里使用了@ Multipart,那么我們認為@Part應當支持普通的key-value,以及文件。

對于普通的key-value是沒問題的,只需要這樣@Part("username") String username

那么對于文件,為什么需要這樣呢?@Part("file_key\"; filename=\"pp.png")

這個value設置的值不用看就會覺得特別奇怪,然而卻可以正常執行,原因是什么呢?

原因是這樣的:

當上傳key-value的時候,實際上對應這樣的代碼:

builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""),RequestBody.create(null, params.get(key)));
  • 1
  • 2
  • 1
  • 2

也就是說,我們的@Part轉化為了

Headers.of("Content-Disposition", "form-data; name=\"" + key + "\"")
  • 1
  • 1

這么一看,很隨意,只要把key放進去就可以了。

但是,retrofit2并沒有對文件做特殊處理,文件的對應的字符串應該是這樣的

 Headers.of("Content-Disposition", "form-data; name="filekey";filename="filename.png");
  • 1
  • 1

與鍵值對對應的字符串相比,多了個;filename="filename.png,就因為retrofit沒有做特殊處理,所以你現在看這些hack的做法

@Part("file_key\"; filename=\"pp.png")
拼接:==>
Content-Disposition", "form-data; name=\"" + key + "\" 結果:==> Content-Disposition", "form-data; name=file_key\"; filename=\"pp.png\"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

ok,到這里我相信你已經理解了,為什么要這么做,而且為什么這么做可以成功!

恩,值得一提的事,因為這種方式文件名寫死了,我們上文使用的的是@Part MultipartBody.Part file,可以滿足文件名動態設置,這個方式貌似也是2.0.1的時候支持的。

上述相關的源碼:

#ServiceMethod
if (annotation instanceof Part) {if (!isMultipart) {throw parameterError(p, "@Part parameters can only be used with multipart encoding."); } Part part = (Part) annotation; gotPart = true; String partName = part.value(); Headers headers = Headers.of("Content-Disposition", "form-data; name=\"" + partName + "\"", "Content-Transfer-Encoding", part.encoding()); }

?

可以看到呢,并沒有對文件做特殊處理,估計下個版本說不定@Part會多個isFile=true|false屬性,甚至修改對應形參,然后在這里做簡單的處理。

ok,最后來到關鍵的ConverterFactory了~

六、自定義Converter.Factory

(1)responseBodyConverter

關于Converter.Factory,肯定是通過addConverterFactory設置的

Retrofit retrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create()).build();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

該方法接受的是一個Converter.Factory factory對象

該對象呢,是一個抽象類,內部包含3個方法:

abstract class Factory {public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,Retrofit retrofit) {return null; } public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { return null; } public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) { return null; } }

可以看到呢,3個方法都是空方法而不是抽象的方法,也就表明了我們可以選擇去實現其中的1個或多個方法,一般只需要關注requestBodyConverterresponseBodyConverter就可以了。

ok,我們先看如何自定義,最后再看GsonConverterFactory.create的源碼。

先來個簡單的,實現responseBodyConverter方法,看這個名字很好理解,就是將responseBody進行轉化就可以了。

ok,這里呢,我們先看一下上述中我們使用的接口:

package com.zhy.retrofittest.userBiz;public interface IUserBiz { @GET("users") Call<List<User>> getUsers(); @POST("users") Call<List<User>> getUsersBySort(@Query("sort") String sort); @GET("{username}") Call<User> getUser(@Path("username") String username); @POST("add") Call<List<User>> addUser(@Body User user); @POST("login") @FormUrlEncoded Call<User> login(@Field("username") String username, @Field("password") String password); @Multipart @POST("register") Call<User> registerUser(@Part("photos") RequestBody photos, @Part("username") RequestBody username, @Part("password") RequestBody password); @Multipart @POST("register") Call<User> registerUser(@PartMap Map<String, RequestBody> params, @Part("password") RequestBody password); @GET("download") Call<ResponseBody> downloadTest(); }

不知不覺,方法還蠻多的,假設哈,我們這里去掉retrofit構造時的GsonConverterFactory.create,自己實現一個Converter.Factory來做數據的轉化工作。

首先我們解決responseBodyConverter,那么代碼很簡單,我們可以這么寫:

public class UserConverterFactory extends Converter.Factory { @Override public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { //根據type判斷是否是自己能處理的類型,不能的話,return null ,交給后面的Converter.Factory return new UserConverter(type); } } public class UserResponseConverter<T> implements Converter<ResponseBody, T> { private Type type; Gson gson = new Gson(); public UserResponseConverter(Type type) { this.type = type; } @Override public T convert(ResponseBody responseBody) throws IOException { String result = responseBody.string(); T users = gson.fromJson(result, type); return users; } } 

?

使用的時候呢,可以

 Retrofit retrofit = new Retrofit.Builder()
.callFactory(new OkHttpClient())               .baseUrl("http://example/springmvc_users/user/")
//.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(new UserConverterFactory()) .build();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

ok,這樣的話,就可以完成我們的ReponseBodyList<User>或者User的轉化了。

可以看出,我們這里用的依然是Gson,那么有些同學肯定不希望使用Gson就能實現,如果不使用Gson的話,一般需要針對具體的返回類型,比如我們針對返回List<User>或者User

你可以這么寫:

package com.zhy.retrofittest.converter;
/*** Created by zhy on 16/4/30.*/
public class UserResponseConverter<T> implements Converter<ResponseBody, T> { private Type type; Gson gson = new Gson(); public UserResponseConverter(Type type) { this.type = type; } @Override public T convert(ResponseBody responseBody) throws IOException { String result = responseBody.string(); if (result.startsWith("[")) { return (T) parseUsers(result); } else { return (T) parseUser(result); } } private User parseUser(String result) { JSONObject jsonObject = null; try { jsonObject = new JSONObject(result); User u = new User(); u.setUsername(jsonObject.getString("username")); return u; } catch (JSONException e) { e.printStackTrace(); } return null; } private List<User> parseUsers(String result) { List<User> users = new ArrayList<>(); try { JSONArray jsonArray = new JSONArray(result); User u = null; for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); u = new User(); u.setUsername(jsonObject.getString("username")); users.add(u); } } catch (JSONException e) { e.printStackTrace(); } return users; } } 

?

這里簡單讀取了一個屬性,大家肯定能看懂,這樣就能滿足返回值是Call<List<User>>或者Call<User>.

這里鄭重提醒:如果你針對特定的類型去寫Converter,一定要在UserConverterFactory#responseBodyConverter中對類型進行檢查,發現不能處理的類型return null,這樣的話,可以交給后面的Converter.Factory處理,比如本例我們可以按照下列方式檢查:

public class UserConverterFactory extends Converter.Factory { @Override public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { //根據type判斷是否是自己能處理的類型,不能的話,return null ,交給后面的Converter.Factory if (type == User.class)//支持返回值是User { return new UserResponseConverter(type); } if (type instanceof ParameterizedType)//支持返回值是List<User> { Type rawType = ((ParameterizedType) type).getRawType(); Type actualType = ((ParameterizedType) type).getActualTypeArguments()[0]; if (rawType == List.class && actualType == User.class) { return new UserResponseConverter(type); } } return null; } }

?

好了,到這呢responseBodyConverter方法告一段落了,謹記就是將reponseBody->返回值返回中的實際類型,例如Call<User>中的User;還有對于該converter不能處理的類型一定要返回null。

(2)requestBodyConverter

ok,上面接口一大串方法呢,使用了我們的Converter之后,有個方法我們現在還是不支持的。

@POST("add")
Call<List<User>> addUser(@Body User user);
  • 1
  • 2
  • 1
  • 2

ok,這個@Body需要用到這個方法,叫做requestBodyConverter,根據參數轉化為RequestBody,下面看下我們如何提供支持。

public class UserRequestBodyConverter<T> implements Converter<T, RequestBody> { private Gson mGson = new Gson(); @Override public RequestBody convert(T value) throws IOException { String string = mGson.toJson(value); return RequestBody.create(MediaType.parse("application/json; charset=UTF-8"),string); } } 

?

然后在UserConverterFactory中復寫requestBodyConverter方法,返回即可:

public class UserConverterFactory extends Converter.Factory { @Override public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { return new UserRequestBodyConverter<>(); } }

?

這里偷了個懶,使用Gson將對象轉化為json字符串了,如果你不喜歡使用框架,你可以選擇拼接字符串,或者反射寫一個支持任何對象的,反正就是對象->json字符串的轉化。最后構造一個RequestBody返回即可。

ok,到這里,我相信如果你看的細致,自定義Converter.Factory是干嘛的,但是我還是要總結下:

  • responseBodyConverter 主要是對應@Body注解,完成ResponseBody到實際的返回類型的轉化,這個類型對應Call<XXX>里面的泛型XXX,其實@Part等注解也會需要responseBodyConverter,只不過我們的參數類型都是RequestBody,由默認的converter處理了。
  • requestBodyConverter 完成對象到RequestBody的構造。
  • 一定要注意,檢查type如果不是自己能處理的類型,記得return null (因為可以添加多個,你不能處理return null ,還會去遍歷后面的converter).

七、值得學習的API

其實一般情況下看源碼呢,可以讓我們更好的去使用這個庫,當然在看的過程中如果發現了一些比較好的處理方式呢,是非常值得記錄的。如果每次看別人的源碼都能吸取一定的精華,比你單純的去理解會好很多,因為你的記憶力再好,源碼解析你也是會忘的,而你記錄下來并能夠使用的優越的代碼,可能用久了就成為你的代碼了。

我舉個例子:比如retrofit2中判斷當前運行的環境代碼如下,如果下次你有這樣的需求,你也可以這么寫,甚至源碼中根據不同的運行環境還提供了不同的Executor都很值得記錄:

class Platform {private static final Platform PLATFORM = findPlatform();static Platform get() {return PLATFORM; } private static Platform findPlatform() { try { Class.forName("android.os.Build"); if (Build.VERSION.SDK_INT != 0) { return new Android(); } } catch (ClassNotFoundException ignored) { } try { Class.forName("java.util.Optional"); return new Java8(); } catch (ClassNotFoundException ignored) { } try { Class.forName("org.robovm.apple.foundation.NSObject"); return new IOS(); } catch (ClassNotFoundException ignored) { } return new Platform(); } 

?

轉載于:https://www.cnblogs.com/laughingQing/p/6397074.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/371526.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/371526.shtml
英文地址,請注明出處:http://en.pswp.cn/news/371526.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

不變性真的意味著線程安全嗎?

我經常閱讀有關“如果對象是不可變的&#xff0c;則它是線程安全的”的文章。 實際上&#xff0c;我從未找到過一篇讓我相信不變的意味著線程安全的文章。 即使是Brian Goetz的Java Concurrency in Practice一書中關于不變性的一本書也沒有完全令我滿意。 在這本書中&#xff0…

c語言設計 數組的知識點,C語言程序設計知識點及示例.pdf

C語言程序設計知識點及示例四川大學錦江學院C語言程序設計知識點及示例知識點1&#xff1a;除了復合語句而外&#xff0c;C語言的語句都以分號結束。示例1&#xff1a;C語言的簡單語句 (非復合語句語句)必須以 結束。參考答案&#xff1a;分號知識點2&#xff1a;目標程序和可執…

移動端知識匯總

參見地址: https://github.com/jtyjty99999/mobileTech 轉載于:https://www.cnblogs.com/duanyue/p/7337789.html

在移動端設置overflow:hidden禁止滾動的解決方法

如果你是將overflow:hidden用在了body上那么不管用&#xff0c;因為移動端是基于touch事件。 兩種解決方法&#xff1a; 1、為html和body同時設置height:100%;overflow:hidden; html, body{height:100%;overflow:hidden; }2、使用touchmove $(document).on(touchmove,function …

單元測試線程代碼的5個技巧

這是一些技巧&#xff0c;說明如何進行代碼的邏輯正確性測試&#xff08;與多線程正確性相對&#xff09;。 我發現本質上有兩種帶有線程代碼的刻板印象模式&#xff1a; 面向任務–許多短期運行的同類任務&#xff0c;通常在Java 5執行程序框架內運行&#xff0c; 面向流程–…

jsp2

D:\Software\Tomcat7\work\Catalina\localhost 是緩存目錄&#xff0c;可以刪掉隱藏域&#xff1a;頁面表單中的一個元素&#xff0c;跟文本框一樣&#xff0c;但是用戶看不到1.建立test1--form表單需要它&#xff0c;而不需要用戶看到&#xff0c;用隱藏域<body><%re…

MongoDB MapReduce 的示例。

// JavaScript source code db.runCommand({mapreduce: "page",map: function Map() {emit(this.title, // how to group{ name: this.name } // associated data point (document));},reduce: function Reduce(key, values) {//reduce用來處理group出來是多條數…

c語言長空格的代碼是什么,c語言中表示空格的是什么代碼?

分析如下&#xff1a;不是所有字符都需要轉義的&#xff0c;空格直接就敲空格&#xff0c;或者使用ASCII碼值賦值為32。空格沒有轉義字符。合法轉義字符如下&#xff1a;\a 響鈴(BEL) 、\b 退格(BS)、\f 換頁(FF)、\n 換行(LF)、\r 回車(CR)、\t 水平制表(HT)、\v 垂直制表(VT)…

使用NoSQL實現實體服務–第1部分:概述

在過去的幾周中&#xff0c;我一直在進行一些研發工作&#xff0c;以了解使用NoSQL數據庫實現實體服務 &#xff08;也稱為數據服務&#xff09;的優勢。 實體服務是托馬斯埃爾&#xff08;Thomas Erl&#xff09;的《服務技術》叢書中提出的服務分類。 它用于描述高度不可知和…

IO注意事項

read()方法返回值為什么是int? 因為字節輸入流可以操作任意類型的文件,比如圖片音頻等,這些文件底層都是以二進制形式的存儲的,如果每次讀取都返回byte,有可能在讀到中間的時候遇到111111111,那么這11111111是byte類型的-1,我們的程序是遇到-1就會停止不讀了,后面的數據就讀不…

c語言用星號輸出沙漏,《算法筆記》學習日記——3.3 圖形輸出

3.3 圖形輸出問題 A: 輸出梯形題目描述輸入一個高度h&#xff0c;輸出一個高為h&#xff0c;上底邊為h的梯形。輸入一個整數h(1<h<1000)。輸出h所對應的梯形。樣例輸入web5樣例輸出數組*********************************************思路這一類的題目都比較簡單&#xf…

JavaOne 2012:101種改進Java的方法-開發人員參與為何如此重要

Bruno Souza &#xff0c; Martijn Verburg和Heather Vancura在希爾頓酒店的大陸宴會廳4中展示了“ 101種改進Java的方法&#xff1a;開發人員參與為何如此重要”。 他們將其分為自己最熟悉的領域。 SouJava的創始人兼協調員 Souza談到了通過用戶組的更大參與。 Verberg也在倫敦…

Java組合實體模式~

組合實體模式用于EJB持久化機制。 組合實體是表示對象圖的EJB實體bean。 當組合實體更新時&#xff0c;內部依賴對象bean將自動更新為由EJB實體bean管理。 以下是組合實體Bean的參與者。 組合實體 - 它是主要的實體bean。 它可以是粗粒度的或可以包含用于持久性目的的粗粒度對象…

python中的一些小知識

在最近學習python中遇到的一些小問題匯總一下&#xff1a; 1.在windows7下安裝python3.5版本時提示安裝不了&#xff0c;缺少ServicePack1. 解決辦法是&#xff0c;打開控制面板\系統和安全\Windows Update&#xff0c;下載和更新計算機安裝&#xff0c;然后卸載以前的python版…

在Java中衡量執行時間– Spring StopWatch示例

有兩種方法可以通過使用System.currentTimeinMillis&#xff08;&#xff09;或通過使用System.nanoTime&#xff08;&#xff09; 來測量Java中經過的執行時間 。 這兩個方法可用于測量 Java中兩個方法調用或事件之間的經過時間或執行時間 。 計算經過的時間是Java程序員要做的…

c語言getch在哪個頭文件,用getch()需要頭文件嗎?

該樓層疑似違規已被系統折疊 隱藏此樓查看此樓#include #include #include "string.h"#includeusing namespace std;struct student{ int num;char name[10];char banji[10];float score[3];struct student *next;};struct student *creat(){struct student *head,*p…

My solution for Git Client Error: Permission denied (publickey)

在使用Git客戶端的過程中遇到的問題以及解決方案分享。 我之前已經安裝Git客戶端并且使用Git開發過公司項目&#xff0c;也已經正確生成PublicKey并且添加到SSH keys on github of my account&#xff0c;但是當我想從github上克隆另一個客戶端push的代碼的時候一直報錯&#x…

OutOfMemoryError:無法創建新的本機線程–問題神秘化

正如您從我以前的教程和案例研究中可能已經看到的那樣&#xff0c;要確定和解決Java Heap Space OutOfMemoryError問題可能很復雜。 我從Java EE生產系統中觀察到的常見問題之一是OutOfMemoryError&#xff1a;無法創建新的本機線程&#xff1b; HotSpot JVM無法進一步創建新的…

求10以內平均數的c語言,求助 給小學生出題,自己選加減乘除 做10題 10以內的數 然后統計分...

該樓層疑似違規已被系統折疊 隱藏此樓查看此樓#include #include #include void Menu(void){printf("1,加法 2,減法 3,乘法 4,除法 5,退出\n");printf("請選擇題目類型:");}int Plus(void){int a, b;a rand() % 10 1;b rand() % 10 1;printf("%-2…

linux常用命令大全(轉)好東西要分享

1、ls命令 就是list的縮寫&#xff0c;通過ls 命令不僅可以查看linux文件夾包含的文件&#xff0c;而且可以查看文件權限(包括目錄、文件夾、文件權限)查看目錄信息等等 常用參數搭配&#xff1a; ls -a 列出目錄所有文件&#xff0c;包含以.開始的隱藏文件 ls -A 列出除.及.…