拾贝

【赏码会】HTTP Client中的瑞士军刀:Retrofit

    coding     原创·赏码会

写在前面

最近开始在GitHub上找一些优秀的开源项目,跟团队一起阅读源代码,每周一次,每次一个半小时左右,美其名曰“赏码会”(还记得《唐伯虎点秋香》那句“赏花赏月赏秋香”吗?)。为什么要阅读源代码?好处举不胜举,比如学习如何合理的命名,如何写出简洁、清晰的注释,如何编写有效的单元测试,知道良好的编码风格是什么样的。有一些积累之后,可以试试看找一找隐藏在代码里的设计模式,加一些新的单元测试,想一想如果自己实现会如何设计,亦或者尝试提交一个PR,修复一个Issue。阅读源代码可以说是有百利而无一害,属于典型的第二象限的事情(参见《高效能人士的七个习惯》)。

Retrofit简介

第一次“赏码会”我选的是Retrofit项目,为什么选它呢?第一,小巧(核心代码不到5000行),第二,高Star(17+K),第三,平时一直在用。先简单介绍一下Retrofit这个框架。Retrofit是Square公司开源的一个Java实现的轻量级HTTP Client框架,本质上是对Square公司另一个开源框架OkHTTP的一层type-safe的封装。所谓的type-safe,我的理解就是将OkHTTP原生的Request/Response对象通过类型安全的方式转化为其他任意类型的对象,比如String,用户自定义类型等。

面向接口的声明式API定义风格是Retrofit最受欢迎的特性,例如下面的GitHubService接口的listRepos方法定义了GitHub的List user repositories API。

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

无需定义具体的实现类,就可以直接调用,例如:

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> repos = service.listRepos("octocat");

有经验的Java程序员立刻就能看出,相对于其他的HTTP Client框架,比如Apache HttpClient或者Async Http Client,使用Retrofit将使编程效率产生质的提升。

核心类

从GitHub拉取Retrofit的源代码,导入retrofit子工程,核心代码都在retrofit包下。

核心类列举如下:

  • Retrofit: Retrofit框架的门面类,大多数情况下,你的代码中只需要用到它。
  • ServiceMethod: 对应接口类中的一个方法(比如上文中的listRepos),负责解析方法签名中用到的各种注解,生成最终的Request对象。
  • Call: 类似于Java 8里面的CompletableFuture,提供异步支持。
  • CallAdapter: Retrofit默认只接受Call<?>作为方法返回类型,如果需要使用其他类型,就要添加额外的CallAdapter。
  • Converter: Retrofit默认只接受Response和Void作为Call<?>的类型参数,如果需要使用其他类型,就要添加额外的Converter。

解惑

和任何形式的阅读(读书,读人,读心)一样,要读懂源代码,一定要带着问题去读。为了帮助理解上述几个核心类的关系,简单列举几个我阅读代码时思考的问题,

Q1:为什么只有接口?实现类在哪?

A: 答案很简单,因为使用了JDK的动态代理,非常讨巧的设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@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);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}

Q2:我不想用Call,怎样才能使用其他返回类型?

A: 刚才已经提到了,通过Retrofit.Builder#addCallAdapterFactory()添加相应的CallAdapter,例如想返回CompleteableFuture,可以使用Retrofit提供的Java8CallAdapterFactory

Q3:如何打印请求和响应日志?

A: 跟动态设置Headers类似,可以自定义用于打印日志的OkHttp interceptor,然后添加到自己创建的OkHttpClient实例,再绑定Retrofit.Builder#client()。

漫谈

总的来说,Retrofit框架设计精巧,上手简单,开发效率高,但也存在一些不足。第一,跟OkHTTP框架绑定太死,不像Feign那么灵活,支持多种Client。第二,和JDK的动态代理强绑定,对其他AOP方式不友好,比如这个Issue提到的Hystrix集成问题。

今天先写到这里,未来我会不定期放一些“赏码会”的心得,欢迎到GitHub留言交流。

传送门