您的当前位置:首页正文

如何使用Retrofit请求非Restful API

来源:花图问答

前言

HttpClient时代

OkHttp

Api文档方面,我非常喜欢Square公司的设计风格,okHttp首页相当简洁,Overview、Example、Download全在首页展示,详细使用案例、说明,在github上很清晰。

Retrofit

Retrofit底层基于OkHttp·,并且可以加很多Square开发的“周边产品”:converter-gsonadapter-rxjava等。Retrofit抱着gson&rxjava的大腿,这种聪明做法,也是最近大受欢迎的原因之一,所谓“Rxjava火了,Retrofit也火了”。Retrofit·不仅仅支持这两种周边,我们可以自定义converter&call adapter,可以你喜欢的其他第三方库。



何为非Restful Api?

Restful Api

User数据,有uid、name,Restful Api返回数据:

{
    "name": "kkmike999",
    "uid": 1
}

Not Restful Api

但不少后端工程师,并不一定喜欢用Restful Api,他们会自己在json中加入ret、msg这种数据。当User正确返回:

{
    "ret": 0,
    "msg": "成功",
    "data": {
        "uid": 1,
        "name": "kkmike999"
    }
}

错误返回:

{
    "ret": -1,
    "msg": "失败"
}

这样的好处,就是调试api方便,在任意浏览器都可以直观地看到错误码&错误信息。

Retrofit一般用法

本来Retrofitrestful的支持,可以让我们写少很多冤枉代码。但后端这么搞一套,前端怎么玩呀?既然木已成舟,我们做APP的总不能老对后端指手画脚,友谊小船说翻就翻。

先说说retrofit普通用法

public class User {
    int    uid;
    String name;
}

public interface UserService {

    @GET("not_restful/user/{name}.json")
    Call<User> loadUser(@Path("name") String name);
}

BeanService准备好,接下来就是调用Retrofit了:

OkHttpClient client = new OkHttpClient.Builder().build();

Retrofit retrofit = new 
                                          .addConverterFactory(GsonConverterFactory.create())
                                          .client(client)
                                          .build();

UserService userService = retrofit.create(UserService.class);

User user = userService.loadUser("kkmike999")
                       .execute()
                       .body();

此处加入了GsonConverterFactory,没有使用RxJavaCallAdapter。如果是restful api,直接返回Userjson,那调用execute().body()就能获得正确的User了。然而,not restful api,返回一个不正确的User ,也不抛错,挺难堪的。

ResponseConverter

我们留意到GsonConverterFactory,看看源码:

package retrofit2.converter.gson;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;

public final class GsonConverterFactory extends Converter.Factory {

  public static GsonConverterFactory create() {
      return create(new Gson());
  }

  public static GsonConverterFactory create(Gson gson) {
      return new GsonConverterFactory(gson);
  }

  private final Gson gson;

  private GsonConverterFactory(Gson gson) {
      if (gson == null) throw new NullPointerException("gson == null");
      this.gson = gson;
  }

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
      TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
      return new GsonResponseBodyConverter<>(gson, adapter);
  }

  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
      Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      
      TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
      return new GsonRequestBodyConverter<>(gson, adapter);
  }
}

responseBodyConverter方法返回GsonResponseBodyConverter,我们再看看GsonResponseBodyConverter源码:

package retrofit2.converter.gson;

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson           gson;
    private final TypeAdapter<T> adapter;

    GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        JsonReader jsonReader = gson.newJsonReader(value.charStream());
        try {
            return adapter.read(jsonReader);
        } finally {
            value.close();
        }
    }
}

重写GsonResponseConverter

由源码看出,是GsonResponseBodyConverterjson进行解析的,只要重写GsonResponseBodyConverter,自定义解析,就能达到我们目的了。

GsonResponseBodyConverterGsonConverterFactory都是final class,并不能重写。靠~ 不让重写,我就copy代码!

新建retrofit2.converter.gson目录,新建CustomConverterFactory,把GsonConverterFactory源码拷贝过去,同时新建CustomResponseConverter。 把CustomConverterFactoryGsonResponseBodyConverter替换成CustomResponseConverter

public final class CustomConverterFactory extends Converter.Factory {
    ......
    
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new CustomResponseConverter<>(gson, adapter);
    }
    ......
}

CustomResponseConverter

public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {

    private final Gson gson;
    private final TypeAdapter<T> adapter;

    public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            String body = value.string();

            JSONObject json = new JSONObject(body);

            int    ret = json.optInt("ret");
            String msg = json.optString("msg", "");

            if (ret == 0) {
                if (json.has("data")) {
                    Object data = json.get("data");

                    body = data.toString();

                    return adapter.fromJson(body);
                } else {
                    return (T) msg;
                }
            } else {
                throw new RuntimeException(msg);
            }
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            value.close();
        }
    }
}

为什么我们要新建retrofit2.converter.gson目录?因为GsonRequestBodyConverter不是public class,所以CustomConverterFactoryimport GsonRequestBodyConverter就得在同一目录下。当然你喜欢放在自己目录下,可以拷贝源码如法炮制。

接下来,只要 new Retrofit.Builder().addConverterFactory(CustomConverterFactory.create())就大功告成了!


更灵活的写法

上述做法,我们仅仅踏入半条腿进门,为什么?万一后端不喜欢全用"data",而是根据返回数据类型命名,例如返回User"user",返回Student"student"呢?

{
    "ret": 0,
    "msg": "成功",
    "user": {
        "uid": 1,
        "name": "小明"
    }
}

{
    "ret": 0,
    "msg": "成功",
    "student": {
        "uid": 1,
        "name": "小红"
    }
}

(此时是否有打死后端工程师的冲动?)

别怒,魔高一尺,道高一丈。

玩转Service注解

既然retrofit能“理解”service方法中的注解,我们为何不试试?GsonConverterFactory的方法responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit),这里有Annotation[],没错,这就是service方法中的注解。

我们写一个@Data注解类:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Data {
    String value() default "data";
}

loadUser(...)添加@Data:

@Data("user")
@GET("not_restful/user/{name}.json")
Call<User> loadUser(@Path("name") String name);

修改CustomResponseConverter

public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {

    private final Gson gson;
    private final TypeAdapter<T> adapter;
    private final String name;

    public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter, String name) {
        this.gson = gson;
        this.adapter = adapter;
        this.name = name;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            ...
            if (ret == 0) {
                if (json.has(name)) {
                    Object data = json.get(name);

                    body = data.toString();

                    return adapter.fromJson(body);
                }
                ...
    }
}

CustomConverterFactoryresponseBodyConverter(...)加上

@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    String name = "data";// 默认"data"

    for (Annotation annotation : annotations) {
        if (annotation instanceof Data) {
            name = ((Data) annotation).value();
            break;
        }
    }
    ...
    
    return new CustomResponseConverter<>(gson, adapter, name);
}

这么写后,后端改什么名称都不怕!


更灵活的Converter

有个需求:APP显示某班级信息&学生信息。后台拍拍脑袋:

{
    "ret": 0,
    "msg": "",
    "users": [
        {
            "name": "鸣人",
            "uid": 1
        },
        {
            "name": "佐助",
            "uid": 2
        }
    ],
    "info": {
        "cid": 7,
        "name": "第七班"
    }
}

哭了吧,灭了后端工程师恐怕也难解心头之恨!

阿尼陀佛, 我不是说了吗?

魔高又一尺,道又高一丈。

我们意识到,CustomResponseConverter责任太重,又是判断retmsg,又是解析json数据并返回bean,如果遇到奇葩json,CustomResponseConverter远远不够强大,而且不灵活。

怎么办,干嘛不自定义converter呢?
问题来了,这个converter应该如何传给CustomConverterFactory?因为在new Retrofit.Builder().addConvertFactory(…)时就要添加ConverterFactory,那时并不知道返回json是怎样,哪个service要用哪个adapter。反正通过构造方法给CustomConverterFactoryConverter肯定行不通。

我们上面不是用过Annotaion吗?同样手段再玩一把如何。写一个@Converter注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Converter {

    Class<? extends AbstractResponseConverter> converter();
}

并且写一个Converter抽象类:

public abstract class AbstractResponseConverter<T> implements Converter<ResponseBody, T>{

    protected Gson gson;

    public AbstractResponseConverter(Gson gson) {
        this.gson = gson;
    }
}

为什么要写一个继承Converter抽象类?让我们自定义的Converter直接继承Converter不行吗?
注意了,@Adapter只能携带Class<?>int``String等基本类型,并不能带converter对象。而我们需要CustomConverterFactoryresponseBodyConverter()方法中,通过反射,new一个converter对象,而CustomConverterFactory并不知道调用Converter哪个构造函数,传什么参数。所以,干脆就写一个AbstractResponseConverter,让子类继承它,实现固定的构造方法。这样CustomConverterFactory就可以获取固定的构造方法,生成Converter对象并传入如gson``typeAdapter参数了。

public class ClazzInfo{
    List<Student> students;
    Info     info;
}

public class ClassConverter implements AbstractResponseConverter<ClazzInfo>{

    public ClassConverter(Gson gson){
        super(gson);
    }

    @Override
    public ClazzInfo convert(ResponseBody value) throws IOException {
        // 这里你想怎么解析json就怎么解析啦
        ClazzInfo clazz = ...
        return clazz;
    }
}
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

        for (Annotation annotation : annotations) {
            if (annotation instanceof Converter) {
                try {
                    Class<? extends AbstractResponseConverter> converterClazz = ((Converter) annotation). converter();
                    // 获取有 以gson参数的 构造函数
                    Constructor<? extends AbstractResponseConverter> constructor = converterClazz .getConstructor(Gson.class);
                    AbstractResponseConverter  converter = constructor.newInstance(gson);

                    return converter;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        ...
        return new CustomResponseConverter<>(gson, adapter, name);
    }

Service方法注解:

@Converter(converter = ClassConverter.class)
@GET("not_restful/class/{cid}.json")
Call<ClazzInfo> loadClass(@Path("cid") String cid);

写到这里,已经快吐血了。怎么会有这么奇葩的后端.... 正常情况下,应该把"users""class"封装在"data"里,这样我们就可以直接把返回结果写成Call<ClassInfo>就可以了。


小结

代码越少,错得越少

“我们可以相信的变革”( CHANGE WE CAN BELIEVE IN ) ——美国总统第44任总统,奥巴马


关于作者

我是键盘男。
在广州生活,在创业公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。希望成为独当一面的工程师。