oynix

于无声处听惊雷,于无色处见繁花

Retrofit使用

上篇文章简单说了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
2
3
4
5
// 描述用户的信息
public class User {
public int id;
public String name;
}

第一步,创建一个Retrofit的实例,

1
2
3
4
5
6
7
8
9
10
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://useinfo.hostname.com/")
.build();

// 也可以手动增加一些配置,这里添加了一个Gson的转换器,和一个RxJava的CallAdapter,后面介绍
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://useinfo.hostname.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();

第二步,创建请求API的接口

1
2
3
4
5
6
7
8
interface UserApi {

@GET("user/{userId}")
Call<UserInfo> getInfo(@Path("userId") int userId);

@POST("user")
Call<String> updateUser(@Body UserInfo user);
}

第三步,生成请求API的实例,因为上面的请求API还只是接口,并不能直接用,要先生成实例

1
UserApi api = retrofit.create(UserApi.class);

最后一步,用实例发送请求

1
2
3
4
5
6
// 获取用户信息
UserInfo user = api.getInfo(1);

// 更改用户名字
user.name = "new Name";
String message = api.updateUser(user);

以上,便是一个简单的调用,可以看到的是,确实都是注解堆起来的,所以,下面看看都有哪些注解,每个注解的用法。

4. 注解

注解可以分为3类:网络请求方法类、标记类和网络请求参数类。

  • 网络请求方法
    可以说Retrofit是RESTful风格请求的封装,注解和请求方式一一对应
    1
    2
    3
    4
    5
    6
    7
    8
    @GET
    @POST
    @PUT
    @DELETE
    @PATCH
    @HEAD
    @OPTIONS
    @HTTP:自定义HTTP请求 @HTTP(method = "DELETE", path = "remove/", hasBody = true)
    使用这些注解,便可以标明一个方法的网络请求方法,注解接收一个参数,表明请求的路径,就像上面例子中的那样。Retrofit将URL分成了两部分,第一部分是主机地址,在创建Retrofit时配置的baseUrl,另一部分就是每个请求的具体路径,最终的请求URL就是base后面接上请求方法里的路径,后面会单独说这里要注意的地方
  • 标记类
    1
    2
    3
    @FormUrlEncoded:表明是Form表单,需要和@Field搭配使用,参数使用@Field修饰
    @Multipart:表明body是multi-part,需要和@Part搭配使用,参数使用@Part修饰
    @Streaming:表示数据以流的形式返回,适用于返回数据较大的场景,如果不标记,默认把数据全部载入内存,取数据是从内存取
    这几个注解是用来标记方法的。
  • 网络请求参数
    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
    @HeaderCall<ResponseBody> foo(@Header("Accept-Language") String lang)
    @Headers:与上面不同,这个注解标记方法
    @Url:使用这个注解将忽略baseUrl配置 Call<ResponseBody> list(@Url String url);
    @Body:是用Converter将参数写入到请求body中
    @Path:请求路径的中的参数 @GET("/image/{id}") Call<ResponseBody> example(@Path("id") int id);
    @Field:form表单的参数 @FormUrlEncoded @POST("/") Call<ResponseBody> example(@Field("name") String name, @Field("occupation") String occupation);
    @FieldMap:同上,多个参数,修饰Map Call<ResponseBody> things(@FieldMap Map<String, String> fields);
    @Part:这个我很少用,看看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., @Part 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., @Part("foo") 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., @Part("foo") Image photo).
    Values may be null which will omit them from the request body.

    @Multipart
    @POST("/")
    Call<ResponseBody> example(
    @Part("description") String description,
    @Part(value = "image", encoding = "8-bit") RequestBody image);

    Part parameters may not be null.
    @PartMap:同上,修饰Map
    @Query:添加GET请求URL中的参数 例如:user?id=23&name=John @GET("/friends") Call<ResponseBody> friends(@Query("page") int page);
    @QueryMap:同上,修饰Map

5. 请求的最终URL

Retrofit会根据请求方法注解(@GET, @POST等)里面的路径参数动态生成最终的URL,对于Retrofit的baseUrl配置项,有两种形式,当以斜杠/结尾时是目录形式,当以非斜杠结尾时是文件形式,这两种形式是不同的,对于请求方法注解中写的路径,以斜杠/开头表示绝对路径和不以斜杠开头表示相对路径,这两种形式也是不同的。

所以在生成最终的请求URL时就会有多种情况:

  • 情况一,请求方法中写的是https/http开头的完整URL,这时Retrofit不会使用配置的baseUrl,而是直接使用请求方法注解里的值,当所有请求都写的完整请求URL,那么这时候可以不配置baseUrl
  • 情况二,路径使用的绝对路径,也就是以斜杠/开头,那么这时不管baseUrl是哪种形式,这个路径会直接接到主机和端口后,形成最终的URL
    1
    2
    3
    baseUrl = "https://host:port/a/b"
    path = "/user"
    URL = "https://host:port/user"
  • 情况三,路径是相对路径,baseUrl是目录形式,那么这时候会直接拼接,形成最终URL
    1
    2
    3
    baseUrl = "https://host:port/a/" // 这里是斜杠结尾,是目录形式
    path = "user"
    URL = "https://host:prot/a/user"
  • 情况四,路径是相对路径,但baseUrl是文件形式,那么这时候路径会接到最后一个文件后,形成最终URL。可以这么理解,因为文件下不能有文件,目录下才能有文件,所以user不能接在a的后面。
    1
    2
    3
    baseUrl = "https://host:port/a" // 注意这里没有斜杠结尾,所以是文件形式
    path = "user"
    URL = "https://host:port/user"

按照RESTful的风格来说,一般常用的是,baseUrl是用目录形式,也就是以斜杠/结尾,路径使用相对路径。

6. CallAdapter

CallAdapter和Converter是Retrofit中最精妙的设计了,这就好比OkHttp中的责任链,所以,接下来分别看看这两个。

先看CallAdapter,那么它是在适配什么呢?顾名思义,请求适配器,在适配请求的返回值类型。先看下这两个请求:

1
2
3
Call<User> getUser();

Response<User> getUser();

都是网络请求,但是返回类型不同,一个是Call,一个是Response,这两种形式Retrofit都是支持的,那么它是怎么做到的呢?没错,就是通过CallAdapter,每个返回类型,都有一个对应的CallAdapter,来将服务器返回的数据转换的成目标类型。目前支持的有Guava、Java8、RxJava,也可以自定义,在创建Retrofit实例时添加进去,即可。上面的例子中便是添加了一个RxJava的CallAdapter,可以直接将结果转换成Observers,可以方便的进行链式调用。

说完它的作用,那么就要看看Retrofit是如何实现的了。

我们写的是请求API接口,并没有具体的实现,调用Retrofit的create(Class)方法时,它实际上生成并返回了一个动态代理,我们调用API实例的方法时,真实调用的代理里的invoke方法

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

@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable 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);
}
args = args != null ? args : emptyArgs;
return platform.isDefaultMethod(method) ? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args);
}
});
}

invoke方法很短,就这么几行,看过platform.isDefaultMethod(method)这个方法就知道,如果接口里的方法有默认实现,返回true,否则返回false,这是Java8才开始支持的,之前版本接口方法是不允许有方法体的。我们声明的API接口没有方法体,所有最终都会调用

1
loadServiceMethod(method).invoke(args)

所以,只需要看这个方法是如何处理返回值,就知道Retrofit是怎么使用CallAdapter的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;

synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = ServiceMethod.parseAnnotations(this, method);
serviceMethodCache.put(method, result);
}
}
return result;
}

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
2
3
4
5
6
7
8
9
private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
Retrofit retrofit, Method method, Type returnType, Annotation[] annotations) {
try {
//noinspection unchecked
return (CallAdapter<ResponseT, ReturnT>) retrofit.callAdapter(returnType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(method, e, "Unable to create call adapter for %s", returnType);
}
}

也就是调用了retrofit的callAdapter方法,retrofit中最终创建CallAdapter的方法是这个

1
2
3
4
5
6
7
8
9
10
11
12
13
public CallAdapter<?, ?> nextCallAdapter(
@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}

...
throw new IllegalArgumentException(builder.toString());
}

看到了吧,这里Retrofit就是在遍历自己的CallAdapterFactory,然后调用get方法,将返回值类型returnType和annotations传入,如果返回不为空,则返回。CallAdapterFactory的这个get方法的作用是,如果对应的CallAdapter能把数据转化成returnType类型,那么就生成出一个CallAdapter出来,如果不能则返回null,如果Retrofit中所带的CallAdapter没有一个符合条件的,那么就会来到最后一行,抛出一个Exception。

如果我们在创建Retrofit时,不手动添加CallAdapter,它也能用,是因为Retrofit里有个缺省的CallAdapter,叫DefaultCallAdapterFactory

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
26
27
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
private final @Nullable Executor callbackExecutor;

DefaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
this.callbackExecutor = callbackExecutor;
}

@Override
public @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
if (getRawType(returnType) != Call.class) { return null; }

if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException("Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);

final Executor executor = Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class) ? null : callbackExecutor;

return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() { return responseType; }

@Override
public Call<Object> adapt(Call<Object> call) { return executor == null ? call : new ExecutorCallbackCall<>(executor, call); }
};
}
}

方法里第一行是这么写的

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface Converter<F, T> {
@Nullable
T convert(F value) throws IOException;

/** Creates {@link Converter} instances based on a type and target usage. */
abstract class Factory {
/**
* Returns a {@link Converter} for converting an HTTP response body to {@code type}, or null if
* {@code type} cannot be handled by this factory. This is used to create converters for
* response types such as {@code SimpleResponse} from a {@code Call<SimpleResponse>}
* declaration.
*/
public @Nullable Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}

/**
* Returns a {@link Converter} for converting {@code type} to an HTTP request body, or null if
* {@code type} cannot be handled by this factory. This is used to create converters for types
* specified by {@link Body @Body}, {@link Part @Part}, and {@link PartMap @PartMap} values.
*/
public @Nullable Converter<?, RequestBody> requestBodyConverter(
Type type,
Annotation[] parameterAnnotations,
Annotation[] methodAnnotations,
Retrofit retrofit) {
return null;
}

/**
* Returns a {@link Converter} for converting {@code type} to a {@link String}, or null if
* {@code type} cannot be handled by this factory. This is used to create converters for types
* specified by {@link Field @Field}, {@link FieldMap @FieldMap} values, {@link Header @Header},
* {@link HeaderMap @HeaderMap}, {@link Path @Path}, {@link Query @Query}, and {@link
* QueryMap @QueryMap} values.
*/
public @Nullable Converter<?, String> stringConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}

}
}

这三个维度分别是requestBodyConverter、responseBodyConverter和stringConverter。

下面我们来看看发起请求过程中的一些细节。

我们在声明请求时用注解标记了很多信息,比如请求Method,请求Path,这些最终都需要转换成网络请求认识的形式才可以,转化注解这件事,是在RequestFactory中做的,而RequestFactory是在ServiceMethod创建时一起创建的

1
2
3
4
5
// ServiceMethod.parseAnnotations(this, method);
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
...
}

RequestFactory通过静态方法创建了实例,在创建时,对所有注解进行了解析,在这个解析过程中,便是使用了Retrofit的ConverterFactory的stringConvterer和requestBodyConverter,解析参数时用stringConverter,解析RequestBody时用requestBodyConverter。

Converter的获取方式和CallAdapter类似,同样是传入一个类型,如果能转化,则返回一个Converter,不能就返回null,解析Body时,多是将起转化成JSON格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public <T> Converter<T, RequestBody> nextRequestBodyConverter(@Nullable Converter.Factory skipPast, Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {

int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter.Factory factory = converterFactories.get(i);
Converter<?, RequestBody> converter = factory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<T, RequestBody>) converter;
}
}

...
throw new IllegalArgumentException(builder.toString());
}

生成ServiceMethod后,便会调用其invoke方法,这个方法会将请求包装成一个叫做OkHttpCall的类型

1
2
3
4
5
// HttpServiceMethod.java
final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}

将刚刚生成的requestFactory作为参数传了进去,同时还传入了responseConveter,用来解析服务器返回的数据,同样,responseConverter也是由Retrofit提供,如果有能处理type的Converter,则返回,无则返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter = converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<ResponseBody, T>) converter;
}
}

...
throw new IllegalArgumentException(builder.toString());
}

再来看看GsonConverterFactory的实现

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
26
27
28
29
public final class GsonConverterFactory extends Converter.Factory {

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

public static GsonConverterFactory create(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
return new GsonConverterFactory(gson);
}

private final Gson gson;

private GsonConverterFactory(Gson gson) {
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);
}
}

可以看到,它只重写了两个维度,requestBody和responseBody的Converter,GsonResponseBodyConverter的作用是把JSON串转化成对象,而GsonRequestBodyConverter的作用是把对象转化成JSON串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GsonRequestBodyConverter.java
public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}

// GsonResponseBodyConverter.java
public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
try {
T result = adapter.read(jsonReader);
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
}
return result;
} finally {
value.close();
}
}

同样,若自定义Converter,实现Converter.Factory和Converter,即可。

8. 写在最后

直观来看,依赖框架帮助我们省去了不少重复性的工作。但是,省去不等于是不做。从宏观角度来看,发送一个网络请求的需要做的事情有构建请求,连接服务器,发送请求,接收回应。这些必须的步骤,每次请求都是必不可缺的,这里的省去只是简化了我们的工作,但这些事并没有省去,只是这些依赖库在背后用复杂的逻辑默默的帮我们在做。从这个角度看,依赖库确实可以提升开发效率,但也不能过分依赖,框架背后的不少内容,还是需要我们熟知的,就像是网络请求,如果只知道通过这些框架发送请求、处理回应,那么在遇到一些涉及基础知识的问题时,可以就会有些不知所措。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/05/bc84547e2f8f/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道