上篇文章简单说了OkHttp的使用,这篇来说Retrofit。同样,Retrofit也是Square公司出品。
Retrofit Github地址
1. 简介
在OkHttp的封装下,我们发送网络请求方便了很多,省去了很多重复性的样板代码。而Retrofit的作用,是进一步减少样板代码量,比如发送POST请求时,每次都需要将参数转化成JSON格式,添加到Body中,发送到服务器。接收回应时,每次也是需要先将Response里的Body解析成类,然后从中读取需要的参数。Retrofit通过注解的形式,将这些重复性的工作也省去了。
Retrofit中使用了大量的注解,请求方式、请求参数等均使用注解来标明,记住这些注解,基本就掌握了用法。
2. 使用
是用Retrofit大致分为4个步骤
- 创建Retrofit实例
Retrofit支持配置一些选项,比如CallAdapter、Converter等 - 创建请求API的接口
这里只需要写接口,而不需要写具体实现,Retrofit使用动态代理的方式,来满足调用者 - 生成请求API的实例
通过第一步中生成的Retrofit实例,来生成上一步中接口的实例 - 通过请求API的实例发送请求
使用上一步中的实例,就可以发送网络请求了
3. 示例
看了上面的步骤会觉得很抽象,下面举个简单的例子,就会很具体了。比如,通过GET获取用户信息,通过POST更新用户名称。
这是一个用来描述用户的类,只有两个字段,一个是id,一个用户名
1 | // 描述用户的信息 |
第一步,创建一个Retrofit的实例,
1 | Retrofit retrofit = new Retrofit.Builder() |
第二步,创建请求API的接口
1 | interface UserApi { |
第三步,生成请求API的实例,因为上面的请求API还只是接口,并不能直接用,要先生成实例
1 | UserApi api = retrofit.create(UserApi.class); |
最后一步,用实例发送请求
1 | // 获取用户信息 |
以上,便是一个简单的调用,可以看到的是,确实都是注解堆起来的,所以,下面看看都有哪些注解,每个注解的用法。
4. 注解
注解可以分为3类:网络请求方法类、标记类和网络请求参数类。
- 网络请求方法
可以说Retrofit是RESTful风格请求的封装,注解和请求方式一一对应使用这些注解,便可以标明一个方法的网络请求方法,注解接收一个参数,表明请求的路径,就像上面例子中的那样。Retrofit将URL分成了两部分,第一部分是主机地址,在创建Retrofit时配置的baseUrl,另一部分就是每个请求的具体路径,最终的请求URL就是base后面接上请求方法里的路径,后面会单独说这里要注意的地方1
2
3
4
5
6
7
8
:自定义HTTP请求 - 标记类这几个注解是用来标记方法的。
1
2
3:表明是Form表单,需要和 搭配使用,参数使用 修饰
:表明body是multi-part,需要和 搭配使用,参数使用 修饰
:表示数据以流的形式返回,适用于返回数据较大的场景,如果不标记,默认把数据全部载入内存,取数据是从内存取 - 网络请求参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Call<ResponseBody> foo( String lang) :
@Headers:与上面不同,这个注解标记方法
@Url:使用这个注解将忽略baseUrl配置 Call<ResponseBody> list( String url);
:是用Converter将参数写入到请求body中
Call<ResponseBody> example(int id) ; :请求路径的中的参数
Call<ResponseBody> example( String name, String occupation); :form表单的参数
Map Call<ResponseBody> things( Map<String, String> fields); :同上,多个参数,修饰
:这个我很少用,看看Retrofit自己的解释
Denotes a single part of a multi-part request.
The parameter type on which this annotation exists will be processed in one of three ways:
If the type is okhttp3.MultipartBody.Part the contents will be used directly. Omit the name from the annotation (i.e., . MultipartBody.Part part)
If the type is RequestBody the value will be used directly with its content type. Supply the part name in the annotation (e.g., . RequestBody foo)
Other object types will be converted to an appropriate representation by using a converter. Supply the part name in the annotation (e.g., . Image photo)
Values may be null which will omit them from the request body.
@Multipart
@POST("/")
Call<ResponseBody> example(
String description,
RequestBody image);
Part parameters may not be null.
:同上,修饰Map
23&name=John Call<ResponseBody> friends(int page) ; :添加GET请求URL中的参数 例如:user?id=
:同上,修饰Map
5. 请求的最终URL
Retrofit会根据请求方法注解(@GET, @POST等)里面的路径参数动态生成最终的URL,对于Retrofit的baseUrl配置项,有两种形式,当以斜杠/
结尾时是目录形式,当以非斜杠结尾时是文件形式,这两种形式是不同的,对于请求方法注解中写的路径,以斜杠/
开头表示绝对路径和不以斜杠开头表示相对路径,这两种形式也是不同的。
所以在生成最终的请求URL时就会有多种情况:
- 情况一,请求方法中写的是https/http开头的完整URL,这时Retrofit不会使用配置的baseUrl,而是直接使用请求方法注解里的值,当所有请求都写的完整请求URL,那么这时候可以不配置baseUrl
- 情况二,路径使用的绝对路径,也就是以斜杠
/
开头,那么这时不管baseUrl是哪种形式,这个路径会直接接到主机和端口后,形成最终的URL1
2
3baseUrl = "https://host:port/a/b"
path = "/user"
URL = "https://host:port/user" - 情况三,路径是相对路径,baseUrl是目录形式,那么这时候会直接拼接,形成最终URL
1
2
3baseUrl = "https://host:port/a/" // 这里是斜杠结尾,是目录形式
path = "user"
URL = "https://host:prot/a/user" - 情况四,路径是相对路径,但baseUrl是文件形式,那么这时候路径会接到最后一个文件后,形成最终URL。可以这么理解,因为文件下不能有文件,目录下才能有文件,所以user不能接在a的后面。
1
2
3baseUrl = "https://host:port/a" // 注意这里没有斜杠结尾,所以是文件形式
path = "user"
URL = "https://host:port/user"
按照RESTful的风格来说,一般常用的是,baseUrl是用目录形式,也就是以斜杠/
结尾,路径使用相对路径。
6. CallAdapter
CallAdapter和Converter是Retrofit中最精妙的设计了,这就好比OkHttp中的责任链,所以,接下来分别看看这两个。
先看CallAdapter,那么它是在适配什么呢?顾名思义,请求适配器,在适配请求的返回值类型。先看下这两个请求:
1 | Call<User> getUser(); |
都是网络请求,但是返回类型不同,一个是Call,一个是Response,这两种形式Retrofit都是支持的,那么它是怎么做到的呢?没错,就是通过CallAdapter,每个返回类型,都有一个对应的CallAdapter,来将服务器返回的数据转换的成目标类型。目前支持的有Guava、Java8、RxJava,也可以自定义,在创建Retrofit实例时添加进去,即可。上面的例子中便是添加了一个RxJava的CallAdapter,可以直接将结果转换成Observers,可以方便的进行链式调用。
说完它的作用,那么就要看看Retrofit是如何实现的了。
我们写的是请求API接口,并没有具体的实现,调用Retrofit的create(Class)方法时,它实际上生成并返回了一个动态代理,我们调用API实例的方法时,真实调用的代理里的invoke方法
1 | public <T> T create(final Class<T> service) { |
invoke方法很短,就这么几行,看过platform.isDefaultMethod(method)
这个方法就知道,如果接口里的方法有默认实现,返回true,否则返回false,这是Java8才开始支持的,之前版本接口方法是不允许有方法体的。我们声明的API接口没有方法体,所有最终都会调用
1 | loadServiceMethod(method).invoke(args) |
所以,只需要看这个方法是如何处理返回值,就知道Retrofit是怎么使用CallAdapter的了。
1 | ServiceMethod<?> loadServiceMethod(Method method) { |
Retrofit将接口里的方法,抽象成了一个ServiceMethod类型,这里是做了一个缓存,为接口里的每个方法生成一个ServiceMethod实例,且只生成一个实例,节约资源提高性能。获取到ServiceMethod后,就会调用它的invoke方法,invoke是个抽象方法,它只有在HttpServiceMethod中有实现,而invoke中又调用了另一个抽象方法adopt,adapt里调用的是callAdapter的adapt方法。
总的来说就是,invoke方法调用的是callAdapter的adapt方法,而callAdapter是在构造函数的时候传进去的,意思就是说,在构造好ServiceMethod实例的时候,它所使用的CallAdapter就确定了。那么,看看在构造的时候如何选取的CallAdapter,看上面的代码就知道,ServiceMethod是通过静态方法parseAnnotations创建的,这个方法里又调用了HttpServiceMethod的parseAnnotations方法,也就是说,最终创建的ServiceMethod的是HttpServiceMethod的parseAnnotations方法。
来看HttpServiceMethod的parseAnnotations,这个方法中的CallAdapter,是通过这个方法创建的
1 | private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter( |
也就是调用了retrofit的callAdapter方法,retrofit中最终创建CallAdapter的方法是这个
1 | public CallAdapter<?, ?> nextCallAdapter( |
看到了吧,这里Retrofit就是在遍历自己的CallAdapterFactory,然后调用get方法,将返回值类型returnType和annotations传入,如果返回不为空,则返回。CallAdapterFactory的这个get方法的作用是,如果对应的CallAdapter能把数据转化成returnType类型,那么就生成出一个CallAdapter出来,如果不能则返回null,如果Retrofit中所带的CallAdapter没有一个符合条件的,那么就会来到最后一行,抛出一个Exception。
如果我们在创建Retrofit时,不手动添加CallAdapter,它也能用,是因为Retrofit里有个缺省的CallAdapter,叫DefaultCallAdapterFactory
1 | final class DefaultCallAdapterFactory extends CallAdapter.Factory { |
方法里第一行是这么写的
1 | if (getRawType(returnType) != Call.class) { return null; } |
意思就是,如果returnType不是Call,那么就返回null,换句话说,这个CallAdapter只能处理Call<T>
类型的方法,也就是我们常写的那种,responseType是Call的范型T,方法最后直接new了一个CallAdapter返回了,在adapt方法中,因为Call就是最基本的形式,需要的也是Call类型,所以不需要额外的处理,就直接返回了,ExecutorCallbackCall是切换线程的,就不多说了。
总结一下就是,Retrofit里保存了自带的和我们手动添加的所有CallAdapterFactory,我们调用API实例的方法时,实际上是在调用动态代理的invoke方法,这方法里为每个API接口方法生成了一个ServiceMethod,在生成ServiceMethod时会从Retrofit的CallAdapterFactory中查找,看是否有CallAdapter可以将Call转换成目标类型,有则使用,无则抛异常。这样,在我们调用API实例的方法时,就可以得到结果了。
如果需要自定义CallAdapter,则需要实现CallAdapterFactory和CallAdapter。
7. Converter
说完CallAdapter,再来看看Converter,说实话,我都有点敲累了,本着一鼓作气的精神,还是坚持敲完吧。
有了上面的经验,这个就容易一些了。同样,Retrofit中也是保存了所有的ConverterFactory,所有需要Converter的地方,都是从这里提供的。ConvertFactory是提供Converter的工厂,它里面主要有3个方法,也就是可以提供三个维度的Converter
1 | public interface Converter<F, T> { |
这三个维度分别是requestBodyConverter、responseBodyConverter和stringConverter。
下面我们来看看发起请求过程中的一些细节。
我们在声明请求时用注解标记了很多信息,比如请求Method,请求Path,这些最终都需要转换成网络请求认识的形式才可以,转化注解这件事,是在RequestFactory中做的,而RequestFactory是在ServiceMethod创建时一起创建的
1 | // ServiceMethod.parseAnnotations(this, method); |
RequestFactory通过静态方法创建了实例,在创建时,对所有注解进行了解析,在这个解析过程中,便是使用了Retrofit的ConverterFactory的stringConvterer和requestBodyConverter,解析参数时用stringConverter,解析RequestBody时用requestBodyConverter。
Converter的获取方式和CallAdapter类似,同样是传入一个类型,如果能转化,则返回一个Converter,不能就返回null,解析Body时,多是将起转化成JSON格式
1 | public <T> Converter<T, RequestBody> nextRequestBodyConverter( Converter.Factory skipPast, Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations){ |
生成ServiceMethod后,便会调用其invoke方法,这个方法会将请求包装成一个叫做OkHttpCall的类型
1 | // HttpServiceMethod.java |
将刚刚生成的requestFactory作为参数传了进去,同时还传入了responseConveter,用来解析服务器返回的数据,同样,responseConverter也是由Retrofit提供,如果有能处理type的Converter,则返回,无则返回null
1 | public <T> Converter<ResponseBody, T> nextResponseBodyConverter( Converter.Factory skipPast, Type type, Annotation[] annotations){ |
再来看看GsonConverterFactory的实现
1 | public final class GsonConverterFactory extends Converter.Factory { |
可以看到,它只重写了两个维度,requestBody和responseBody的Converter,GsonResponseBodyConverter的作用是把JSON串转化成对象,而GsonRequestBodyConverter的作用是把对象转化成JSON串
1 | // GsonRequestBodyConverter.java |
同样,若自定义Converter,实现Converter.Factory和Converter,即可。
8. 写在最后
直观来看,依赖框架帮助我们省去了不少重复性的工作。但是,省去不等于是不做。从宏观角度来看,发送一个网络请求的需要做的事情有构建请求,连接服务器,发送请求,接收回应。这些必须的步骤,每次请求都是必不可缺的,这里的省去只是简化了我们的工作,但这些事并没有省去,只是这些依赖库在背后用复杂的逻辑默默的帮我们在做。从这个角度看,依赖库确实可以提升开发效率,但也不能过分依赖,框架背后的不少内容,还是需要我们熟知的,就像是网络请求,如果只知道通过这些框架发送请求、处理回应,那么在遇到一些涉及基础知识的问题时,可以就会有些不知所措。