同事拒绝Retrofit,怎么办?

photo-1449452198679-05c7fd30f416_副本.jpg

为什么使用Retrofit?

我在《如何使用Retrofit请求非Restful API》 前言 提到过HttpClient、OkHttp、Retrofit历史。Retrofit无缝衔接RxJava,请求结果解析都不需要手动写,用Gson处理json->Object

总的来说,就是

  • 更清晰的结构
  • 更简洁的代码

拒绝Retrofit的心态

  1. 旧代码不好改
  2. 担心Retrofit不能满足需求
  3. 自己写的代码比Retrofit屌

1)

我猜大部分人都是第一种情况。项目做到一定程度,http层逻辑跟别的类耦合了,换Retrofit要大动干戈,老板不管你代码重构有的没的,项目催得紧......

2)

第二种情况,也是很头疼的问题。很多公司不一定遵循Restful Api准则写接口,新手对如何修改Retrofit代码无从入手。叹息一下,还是用回旧代码吧。

对于这种情况,希望《如何使用Retrofit请求非Restful API》 对你有帮助!

3)

不用犹豫,拿砖头拍死那个同事......


Retrofit官网例子:

http://square.github.io/retrofit/

Retrofit turns your HTTP API into a Java interface.

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

The Retrofit class generates an implementation of the GitHubService interface.

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.

Call<List<Repo>> repos = service.listRepos("octocat");

即使从来未接触Retrofit的人,一看GitHubService listRepos方法,就了解这个api大概意思,url是什么,传什么参数。


思路:代理旧http代码

我们目的是写到好像Retrofit那样。Retrofit本身就运用了代理模式隐藏了请求的细节,因此,我们就模仿它写一个自己的代理层

旧http使用方式

public loadUser(String value, Callback callback) {
        new Thread(new Runnerable(){
            @Override
            public void run(){
                MyHttpClient httpClient = new MyHttpClient();

                Map<String, Object> params = new HashMap<>();
                params.put("key", value);

                String json = httpClient.get("user/kkmike999.json", params);

                User user = new Gson().fromJson(json, User.class);
        
                if (callback != null) {
                    callback.onLoaded(user);
                }
            }
        }).start();
}
MyHttpClient
public class MyHttpClient {

    public String get(String path, Map<String, Object> params) throws IOException {
        OkHttpClient client = new OkHttpClient();

        // "http://kkmike999-file.b0.upaiyun.com/" + path
        HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme("http")
                                                          .host("kkmike999-file.b0.upaiyun.com")
                                                          .addPathSegment(path);

        for (String key : params.keySet()) {
            urlBuilder.addQueryParameter(key, params.get(key)
                                                    .toString());
        }

        HttpUrl httpUrl = urlBuilder.build();

        Request request = new Request.Builder().url(httpUrl)
                                               .build();

        Response response = client.newCall(request)
                                  .execute();
        return response.body()
                       .string();
    }
}

写代理层

大概就是这几个类

Image 2.png
MyRetrofit
public class MyRetrofit {

    public <T> T create(Class<T> clazz) {
        if (!clazz.isInterface()) {
            throw new RuntimeException("Service not interface");
        }

        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ServiceProxy());
    }

}
ServiceProxy
public class ServiceProxy implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this, args);
        }

        ServiceMethod serviceMethod = new ServiceMethod(method);
        Converter     converter     = new Converter(new Gson(), serviceMethod.returnType);

        HttpCall call = new HttpCall(serviceMethod, converter, args);

        return call.request();
    }
}
HttpCall
public class HttpCall<T> {

    MyHttpClient  client;
    Converter     converter;
    ServiceMethod serviceMethod;
    Object[]      args;

    public HttpCall(ServiceMethod serviceMethod, Converter converter, Object[] args) {
        this.client = new MyHttpClient();
        
        this.converter = converter;
        this.serviceMethod = serviceMethod;
        this.args = args;
    }

    public T request() {
        try {
            // 参数count与注释count不一致, 抛错
            int argumentCount = args != null ? args.length : 0;
            if (argumentCount != serviceMethod.argumentTypes.length) {
                throw new IllegalArgumentException("Argument count (" + argumentCount + ") doesn't match expected count (" + serviceMethod.argumentTypes.length + ")");
            }

            // 参数
            Map<String, Object> params = new HashMap<>();

            for (int p = 0; p < argumentCount; p++) {
                params.put(serviceMethod.getQueryKey(p), args[p].toString());
            }

            String url = serviceMethod.url();

            String json = client.get(url, params);

            return (T) converter.convert(json);

        } catch (IOException e) {
            e.printStackTrace();

            throw new RuntimeException(e.getMessage());// 请求失败
        }
    }
}
ServiceMethod:
protected Method         method;
    protected Annotation[]   methodAnnotations;
    protected Annotation[][] argumentAnnotations;
    protected Class[]        argumentTypes;
    protected Type           returnType;

    public ServiceMethod(Method method) {
        this.method = method;

        methodAnnotations = method.getDeclaredAnnotations();
        argumentAnnotations = method.getParameterAnnotations();
        argumentTypes = method.getParameterTypes();
        returnType = method.getGenericReturnType();
    }

    /** 
     * 从@Query注释,获取请求参数的key
     */
    public String getQueryKey(int index) {
        for (Annotation annotation : argumentAnnotations[index]) {
            if (annotation instanceof Query) {
                return ((Query) annotation).value();
            }
        }

        return "";
    }

    /**
     * 从@Get注释中,获取url 
     */
    public String url() {
        for (Annotation annotation : methodAnnotations) {
            if (annotation instanceof Get) {
                return ((Get) annotation).value();
            }
        }

        throw new RuntimeException("no GET or POST annotation");
    }
Converter
public class Converter<T> {

    TypeAdapter adapter;

    public Converter(Gson gson, Type type) {
        adapter = gson.getAdapter(TypeToken.get(type));
    }

    T convert(String json) throws IOException {
        return (T) adapter.fromJson(json);
    }
}

最好Converter就抽象成一个接口,你不喜欢用Gson,用fastjson,可以随时切换。

@GET
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {
    String value() default "";
}
@Query
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
    String value() default "";
}

新Api使用方式

json数据:
{
    "uid":1,
    "name":"kkmike999"
}
UserApi
public interface UserApi {

    @Get("user/kkmike999.json")
    User loadUser(@Query("key") String value);
}
public void loadUser(String value, Callback callback) {
        new Thread(new Runnerable(){
            @Override
            public void run(){
                UserApi userApi = new MyRetrofit().create(UserApi.class);

                User user = userApi.loadUser("test");
        
                if (callback != null) {
                    callback.onLoaded(user);
                }
            }
        }).start();
}

调用方式,99%像Retrofit,实际底层http,还是旧代码。


配合RxJava

现在很多开发者都离不开RxJava了。“Retrofit+RxJava简直无敌” 这种想法恐怕不仅仅是我有吧?

你的UserApi应该是这样的:

public interface UserApi {

    @Get("user/kkmike999.json")
    Observable<User> loadUserO(@Query("key") String value);
}

然后,只需要:

新增接口CallAdapter

public interface CallAdapter<T> {

    T adapt(HttpCall call);
}
新增RxCallAdapter
public class RxCallAdapter<T> implements CallAdapter<Observable<T>>{

    @Override
    public Observable<T> adapt(final HttpCall call) {
        return Observable
                    .create(new Observable.OnSubscribe<T>() {
                        @Override
                        public void call(Subscriber<? super T> subscriber) {
                            try {
                                subscriber.onNext((T) call.request());
                                subscriber.onCompleted();
                            } catch (Exception e) {
                                subscriber.onError(e);
                            }
                        }
                    })
                    .subscribeOn(Schedulers.io());
    }
}
修改ServiceProxy
public class ServiceProxy implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this, args);
        }

        ServiceMethod serviceMethod = new ServiceMethod(method);
        Converter     converter     = new Converter(new Gson(), serviceMethod.returnType);
        CallAdapter   adapter       = new RxCallAdapter();
        
        HttpCall call = new HttpCall(serviceMethod, converter, args);

        return adapter.adapt(call);
    }
}
修改ServiceMethod
public class ServiceMethod{

    ......

    public ServiceMethod(Method method) {
        ...

        returnType = method.getGenericReturnType();

        if (returnType instanceof ParameterizedType) {
            // Obserable<?> 里面的type
            ParameterizedType parameterizedType = (ParameterizedType) returnType;

            returnType  = parameterizedType.getActualTypeArguments()[0];
        }
    }
    
    ......
}

RxJava Api调用方式

public void loadUser(String value, Callback callback){
    userApi.loadUserO("test")
           .observeOn(AndroidSchedulers.mainThread())
           .subscribe(new Action1<User>() {
                   @Override
                   public void call(User user) {
                        if (callback != null) {
                            callback.onLoaded(user);
                        }
                   }
               });
}

Retrofit使用方式一模一样了!

依赖关系

依赖关系.png

单元测试

使用Retrofit最终目的,就是让我们更好地单元测试!

上面的代码,都没提到UserPresenter,这是担心代码量太多。懂MVP的同学应该一看就明白。

public class UserPresenter {
    UserApi  userApi;
    UserView userView;
    
    public void loadUser(String value){...}
}
public class UserPresenterTest {

    @Mock
    UserApi              userApi;
    @Mock
    UserView             userView;
    @Captor
    ArgumentCaptor<User> captor;

    UserPresenter userPresenter;

    @Before
    public void setUp() throws Exception {
        RxJavaPlugins.getInstance()
                     .registerSchedulersHook(new RxJavaSchedulersHook() {
                         @Override
                         public Scheduler getIOScheduler() {
                             // io scheduler会新建线程,把io scheduler->immediate scheduler, 异步->同步
                             return Schedulers.immediate();
                         }
                     });

        MockitoAnnotations.initMocks(this);

        userPresenter = new UserPresenter(userApi, userView);
    }

    @Test
    public void loadUser() throws InterruptedException {
        User user = new User();
        user.uid = 1;
        user.name = "kkmike999";

        when(userApi.loadUser("test")).thenReturn(Observable.just(user));

        userPresenter.loadUser("test");

        verify(userView).onUserLoaded(captor.capture());

        User result = captor.getValue();

        Assert.assertEquals(result.uid, 1);
        Assert.assertEquals(result.name, "kkmike999");
    }
}

改进

其实ServiceProxy可以写得更解耦,例如ConverterCallAdapter都从外面传进来。

public class MyRetrofit {
    ......

    public Converter converter(Type returnType) {
        return new GsonConverter(new Gson(), returnType);
    }

    public CallAdapter callAdapter() {
        return new RxCallAdapter();
    }
}

可以参考RetrofitMyRetrofitBuilder模式构建会更好,此处就不累赘了。

public class ServiceProxy implements InvocationHandler {
    MyRetrofit retrofit;

    public ServiceProxy(MyRetrofit retrofit) {
        this.retrofit = retrofit;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
        ....

        Converter   converter = retrofit.converter(serviceMethod.actualType);
        CallAdapter adapter   = retrofit.callAdapter();

        HttpCall call = new HttpCall(client, serviceMethod, converter, args);

        return adapter.adapt(call);
    }
}

Java Demo:https://git.oschina.net/kkmike999/ImitationRetrofit.git


结语

无论你的同事出于什么理由,不想使用Retrofit,迁就一下也无妨,除非原来的代码真的一团糟。也许他只是未研究Retrofit,不想冒无谓的风险。

放下砖头,立地成佛

当你自己写一套代理,就可以享受Retrofit的美妙设计模式,同时也不违背原来代码(如果同事连你写代理都不赞同,拿砖头拍死他)。这样我们就能更好地单元测试。(可能你同事还没认识到单元测试重要性)当他看到你行云流水的代码,或许某天他就用你写的这个代理了。

关于MPV、Retrofit、单元测试,请参考《(MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享》


关于作者

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容