Retrofit :A type-safe HTTP client for Android and Java.

retrofit : noun. a component or accessory added to something after it has been manufactured

mocky.io: Mock your HTTP responses to test your REST API


前言

本文默认ConverterFactoryGsonConverterFactory

请求阶段

  • Header的统一处理
  • 访问绝对路径
  • Map的使用避免声明冗余的类
  • RequestBody为String 及 文件上传

返回阶段

  • 后台Json空数据规范
  • 空数据Void声明
  • ResponseBody为String
  • ResponseBody的多次读取
  • 统一的错误处理

Header的统一处理

使用Interceptor可为每一个request添加统一的Header信息

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
public class OkHttpInterceptor implements okhttp3.Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request newRequest = originalRequest.newBuilder()
.header("sdkInt", Integer.toString(Build.VERSION.SDK_INT))
.header("device", Build.DEVICE)
.header("user-agent", "android.sodino")
.header("ticket", Constant.TICKET)
.header("os", "android")
.header("version", "2.6")
.header("Content-Type", "application/json")
.build();
Response response = chain.proceed(newRequest);
return response;
}
}
public class RetrofitUtil {
// 定义统一的HttpClient并添加拦截器
private static OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new OkHttpInterceptor()).build();
public static Retrofit getComonRetrofit(String baseUrl) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.callbackExecutor(ThreadPool.getThreadsExecutor())
.client(okHttpClient)
.build();
return retrofit;
}
}

访问绝对路径

  1. @Url
    @URL resolved against the base URL.

这种方式是动态的灵活的,不需要提前预知的。

1
2
@GET
Call<ResponseBody> list(@Url String url);
  1. endpoint为绝对路径

这种方式需要在编码时提前预知,与baseUrl的理念是相冲突的,不推荐使用这种方式。

endpoint


Map的使用避免声明冗余的类

QueryMapQuery支持复杂的、不定数的字段。
对应的Body也可以通过定义参数类型为Map来避免声明冗余的类。

以下代码为了Post/PutBody特别定义了个IsReead类,实现方式有些重!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IsRead {
public boolean is_read;
}
public interface Api{
@Post // or @Put
Call<ResponseBody> reqIsRead(@Body IsRead isRead);
}
// send a post or put request
Api api = createApi();
IsRead isRead = new IsRead();
isRead.is_read = true;
api.reqIsRead(isRead);

与如下代码的功能是相同,但更简单明了的:

1
2
3
4
5
6
7
8
9
10
11
public interface Api{
@Post // or @Put
Call<ResponseBody> reqIsRead(@Body Map<String, Boolean> map);
}
// send a post or put request
Api api = createApi();
Map<String, Boolean> map = new HashMap<>();
map.put("is_read", true);
api.reqIsRead(map);

同样的,在请求的返回阶段,如果返回内容都是单纯的key: value,那ResponseBody也可以定义为Map
不必每个接口都有对应的数据类。


RequestBody为String 及 文件上传

App中嵌套的H5页面传给App的内容是Json格式化的字符串,并要作为Body发起Post/Put请求,这时则希望RequestBodyString,则处理为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Api {
@Post // or @Put
Call<ResponseBody> put(@Body RequestBody body);
}
// send a post or put request
Api api = create(Api.class);
String reqString = string;
RequestBody body = RequestBody.create(MediaType.parse("application/json"), reqString);
Call<ResponseBody> call = stringApi.post(body);

如果将MediaType改为图片、视频等对应的MediaType值,则很可很方便的实现文件上传接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface Api {
@Multipart
@POST(ACT_POST_UPLOAD_PHOTO)
Call<UploadPhoto> reqUploadPhoto(@Part MultipartBody.Part file);
}
// upload photo file
File file = getPhotoFile();
// create RequestBody instance from file
MediaType mediaType = MediaType.parse("image/jpeg");
RequestBody requestFile = RequestBody.create(mediaType, file);
// MultipartBody.Part is used to send also the actual file name
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
// Optional:
// add another part within the multipart request
// String descriptionString = "hello, this is description speaking";
// RequestBody description = RequestBody.create(okhttp3.MultipartBody.FORM, descriptionString);
Call<UploadPhoto> call = accountApi.reqPostUploadPhoto(/*description, */body);

后台Json空数据规范

客户端请求数据时,后台对空数据的返回应该是要有规范的,
应该按Json格式返回 [] 空数组或 {} 空对象,不应什么都不返回.

什么都不返回会导致Json解析异常,会误导客户端判定连接为CMCC/ChinaNet等假网络,导致提示网络异常,与实际情况不符。

喂喂,后台同学,都是开发狗,能不能别互相伤害


空数据Void声明

如果只需要发个请求然后根据response codeHTTP_OK(200)即可,而不需要后台回吐额外的数据,在定义接口时可以声明ResponseBodyVoid

1
2
3
4
public interface Api {
@GET
Call<Void> reqIamOK();
}

ResponseBody为String

当要将后台回吐的数据通过App传参给内嵌的H5页面时,这时希望ResponseBodyString,应该这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d("Test", "post() respCode=" + response.code());
try {
String str = response.body().string();
// do something……
} catch (IOException e) {
e.printStackTrace();
}
}
});

ResponseBody的多次读取

当试图去读取response body的原始数据时,由于是从网络上以stream的方式读取的,所以多次读取的话会抛如下异常:

1
2
3
4
5
6
java.lang.IllegalStateException: closed
at com.squareup.okhttp.internal.http.HttpConnection$FixedLengthSource.read(HttpConnection.java:455)
at okio.Buffer.writeAll(Buffer.java:594)
at okio.RealBufferedSource.readByteArray(RealBufferedSource.java:87)
at com.squareup.okhttp.ResponseBody.bytes(ResponseBody.java:56)
at com.squareup.okhttp.ResponseBody.string(ResponseBody.java:82)
1
2
3
4
5
6
ResponseBody responseBody = response.body();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
String responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"))
Log.d("TAG", responseBodyString);

实现了多次读取的功能后,就可以进行下面的统一错误处理了。

参考:
HttpLoggingInterceptor


统一的错误处理

发起一次请求后可能产生的错误有:

  1. 网络问题:

    • 无网络访问失败
    • 链接超时等IO异常
    • 假网络链接
  2. 后台提示错误:

    • 请求参数不规范
    • 业务逻辑错误,如提交的内容包含敏感词
    • Json解析失败
    • response code不是HTTP_OK

以上错误会分散在CallbackonResponse()onFailure()中去。
不利于技术层的数据统计及业务层的错误兼容。
那么在做统一的错误处理时,目标有:

  • onResponse()是纯粹的success回调,剥离了response code或解析失败等异常。
  • 能够读取后台返回的数据源进行处理后,不影响数据源的继续传播与解析。即上文提到的多次读取。

能够统一进行错误处理的方式有Interceptor及对Callback进行override
个人选择Callback override的方式,个人观点希望每个类是尽可能可复用的,对于每一次request,都有对应的Callback,那么就不想再定义新的类(Interceptor)来处理。
而且每一个request都有新的callback的实例对象,也好进行一些个性化的错误处理。

新的Callback代码如下:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public abstract class MyCallback<T> implements Callback<T> {
protected boolean showToast; // 本次request的错误是否弹toast提醒
protected String logMark;
public MyCallback() {
showToast = true;
}
public MyCallback(boolean showToast) {
this.showToast = showToast;
}
/**
* Invoked for a received HTTP response.
* <p>
* Note: An HTTP response may still indicate an application-level failure such as a 404 or 500.
* Call {@link Response#isSuccessful()} to determine if the response indicates success.
*/
@Override
public void onResponse(Call<T> call, Response<T> response){
int respCode = response.code();
Log.d("MyCallback", "onResponse() url[" + getLogMark(call) + "]" + respCode);
if (respCode == HttpURLConnection.HTTP_OK) {
onResponse(call, response, respCode);
} else {
ResponseBody responseBody = null;
if (response != null) {
responseBody = response.errorBody();
}
noHttpOK(respCode, responseBody);
onFailure(call, null, response, respCode);
}
}
protected void noHttpOK(int respCode, ResponseBody respBody) {
int errorCode = 0;
String responseBodyString = "";
if (respBody != null) {
BufferedSource source = respBody.source();
try {
source.request(Long.MAX_VALUE); // Buffer the entire body.
} catch (IOException e) {
e.printStackTrace();
}
Buffer buffer = source.buffer();
try {
responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"));
Log.d("MyCallback", "noHttpOK() " + responseBodyString);
} catch (Throwable t) {
t.printStackTrace();
}
}
ErrorEn responseEn = null;
try {
responseEn = GsonUtil.fromJson(responseBodyString, ErrorEn.class);
} catch (JsonSyntaxException jsExp) {
jsExp.printStackTrace();
}
if (respCode == HttpURLConnection.HTTP_FORBIDDEN) {
// permission error, do logout
// do Logout
Logout.do();
} else if (respCode == HttpURLConnection.HTTP_BAD_GATEWAY) {
if(showToast) {
ToastUtil.showLongToast(BaseApplication.getInstance(),
BaseApplication.getInstance().getString(R.string.error_network) + "(502)");
}
} else if (responseEn != null) {
String strMsg = responseEn.message;
if (respCode == HttpURLConnection.HTTP_PAYMENT_REQUIRED
&& responseEn.errorCode == Login.ELSE_WHERE_LOGIN) {
// 异地登录,会弹对话框,所以不需要Toast重复提示
showToast = false;
}
if (TextUtils.isEmpty(strMsg) == false) {
if (showToast) {
ToastUtil.showToast(BaseApplication.getInstance(), strMsg);
}
}
}
}
/**
* Invoked when a network exception occurred talking to the server or when an unexpected
* exception occurred creating the request or processing the response.
*/
@Override
public void onFailure(Call<T> call, Throwable t){
if (t != null) {
if(showToast) {
ToastUtil.showLongToast(BaseApplication.getInstance(),
BaseApplication.getInstance().getString(R.string.error_network));
}
Log.d("MyCallback", "onFailure() url[" + getLogMark(call) + "]" + t.getMessage());
}
onFailure(call, t, null, -1);
}
/**
* @param respCode HttpURLConnection.responseCode.
* */
public abstract void onResponse(Call<T> call, Response<T> response, int respCode);
/**
* @param response 当服务器返回的respCode不符合预期时,此处response为{@link Callback#onResponse(Call, Response)}中的response
* @param respCode HttpURLConnection.responseCode.如果网络连续不成功/异常等,则值是-1.
* */
public abstract void onFailure(Call<T> call, @Nullable Throwable t, @Nullable Response<T> response, int respCode);
}

About Sodino