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();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值