上篇文章简单说了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
25:Call<ResponseBody> foo( String lang)
@Headers:与上面不同,这个注解标记方法
@Url:使用这个注解将忽略baseUrl配置 Call<ResponseBody> list( String url);
:是用Converter将参数写入到请求body中
:请求路径的中的参数 Call<ResponseBody> example( int id);
:form表单的参数 Call<ResponseBody> example( String name, String occupation);
:同上,多个参数,修饰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
:添加GET请求URL中的参数 例如:user?id=23&name=John Call<ResponseBody> friends( int page);
:同上,修饰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. 写在最后
直观来看,依赖框架帮助我们省去了不少重复性的工作。但是,省去不等于是不做。从宏观角度来看,发送一个网络请求的需要做的事情有构建请求,连接服务器,发送请求,接收回应。这些必须的步骤,每次请求都是必不可缺的,这里的省去只是简化了我们的工作,但这些事并没有省去,只是这些依赖库在背后用复杂的逻辑默默的帮我们在做。从这个角度看,依赖库确实可以提升开发效率,但也不能过分依赖,框架背后的不少内容,还是需要我们熟知的,就像是网络请求,如果只知道通过这些框架发送请求、处理回应,那么在遇到一些涉及基础知识的问题时,可以就会有些不知所措。