1. 程式人生 > >Retrofit2 完全解析 探索與okhttp之間的關係(三)

Retrofit2 完全解析 探索與okhttp之間的關係(三)

五、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);
    }

首先我們一點明確,因為這裡使用了@ 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)));

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

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

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

但是,retrofit2並沒有對檔案做特殊處理,檔案的對應的字串應該是這樣的

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

與鍵值對對應的字串相比,多了個;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\"

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();

該方法接受的是一個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://192.168.1.102:8080/springmvc_users/user/")
//            .addConverterFactory(GsonConverterFactory.create())
            .addConverterFactory(new UserConverterFactory())
            .build();

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);

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();