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
前言 本文默认ConverterFactory
为GsonConverterFactory
。
请求阶段
Header的统一处理
访问绝对路径
Map的使用避免声明冗余的类
RequestBody为String 及 文件上传
返回阶段
后台Json空数据规范
空数据Void声明
ResponseBody为String
ResponseBody的多次读取
统一的错误处理
使用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; } }
访问绝对路径
@Url @URL resolved against the base URL.
这种方式是动态的灵活的,不需要提前预知的。
1 2 @GET Call<ResponseBody> list(@Url String url);
endpoint 为绝对路径
这种方式需要在编码时提前预知,与baseUrl
的理念是相冲突的,不推荐使用这种方式。
Map 的使用避免声明冗余的类QueryMap 为Query 支持复杂的、不定数的字段。 对应的Body 也可以通过定义参数类型为Map 来避免声明冗余的类。
以下代码为了Post
/Put
的Body
特别定义了个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 请求,这时则希望RequestBody 为String ,则处理为:
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 code 为HTTP_OK(200) 即可,而不需要后台回吐额外的数据,在定义接口时可以声明ResponseBody 为Void 。
1 2 3 4 public interface Api { @GET Call<Void> reqIamOK(); }
ResponseBody为String 当要将后台回吐的数据通过App 传参给内嵌的H5 页面时,这时希望ResponseBody 为String ,应该这么做:
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
统一的错误处理 发起一次请求后可能产生的错误有:
网络问题:
后台提示错误:
请求参数不规范
业务逻辑错误,如提交的内容包含敏感词
Json解析失败
response code
不是HTTP_OK
以上错误会分散在Callback
的onResponse()
及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