Retrofit 使用配置

Retrofit 配合 RxJava 的使用是越来越广泛,小到个人项目,大到公司项目,都能看到它,它的使用也是挺简单的,它本身就是对 OkHttp 的一个封装,让开发者在使用网络请求时更加方便、快捷。我用了一段时间之后,觉得虽然封装的挺好的,但是在用到一些东西的时候还是需要来自定义一下的,下面就来分享一下,我在使用过程中的一些自定义小功能。

Retrofit 的使用

连接超时

平时对连接超时没什么印象,因为你即使不设置,默认情况下也是有超时时长的,如果想要设置连接超时时长的话,需要在 OkHttpClient.Builder 的初始化的时候进行设置。例如:

OkHttpClient.Builder builder = new OkHttpClient.Builder()
				.connectTimeout(Mark.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
				.readTimeout(Mark.HTTP_READ_TIMEOUT, TimeUnit.MILLISECONDS)
				.writeTimeout(Mark.HTTP_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
                

在这里我设置了三个连接超时的时长,分别是:

  • connectTimeout 是 TCP Socket 的三次握手(客户端与服务的建立连接)的时长,在这个时间范围内进行 3 次握手;
  • readTimeout 是 TCP Socket 的三次握手和单独读取 IO 操作的时长,包括响应的来源,如果在这个过程中连接失败,则获取不到数据;
  • writeTimeout 则是用于 IO 写入的超时限制。

从 Reftofit 2.5.0 的版本开始,这三个的默认连接超时时长均为 10 秒。

参考地址

公共参数拦截器

在一些情况下,我们需要在请求上添加一些公共信息,比如,用户登录之后携带的验证信息,或是针对不同的请求地址加上一些固有的请求参数。要做到这一点就需要把一些公共的信息提取出来,放在一个拦截器中,之后每一次的网络请求根据限制条件进行判断是否使用参数连接器对请求地址或头部信息进行二次改造。

Interceptor mTokenInterceptor = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        if (Your.sToken == null || alreadyHasAuthorizationHeader(originalRequest)) {
            return chain.proceed(originalRequest);
        }
        Request authorised = originalRequest.newBuilder()
            .header("Authorization", Your.sToken)
            .build();
        return chain.proceed(authorised);
    }
};

或是

Interceptor mTokenInterceptor = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        HttpUrl originalHttpUrl = original.url();
        HttpUrl url = originalHttpUrl.newBuilder()
                            .addQueryParameter("accountsuite", "someThing")
                            .build();
        return chain.proceed(authorised);
    }
};

设置拦截器之后,需要在 OkHttpClient.Builder 上进行应用,通过这个方法.addInterceptor(mTokenInterceptor)加上即可,而且 OkHttp 的拦截器可以同时设置多个,比如在添加参数连接器之后,再加上下面的日志拦截器。

网络请求日志

网络请求日志输出是非常重要的,在调试的时候能及时的查看客户端与后台的数据交互是否存在异常。可是,我在第一次使用的时候是不知道该怎么去配置,看了文档才知道可以自己定义一个 HttpLoggingInterceptor(日志拦截器) 然后设置日志拦截等级并像上面那样给添加到 OkHttpClient.Builder 即可,HttpLoggingInterceptor 一共有 4 个等级:

  • NONE 不打印
  • BASIC 只打印基础信息
    • 请求方法类型(GET、POST……)
    • HTTP 版本(HTTP1.0/HTTP1.1)
    • 请求内容体大小
    • 是否成功返回
    • 耗时时长
    • 内容体大小
  • HEADERS 在 BASIC 级别的基础上增加请求 HEADER 信息
  • BODY 在 HEADERS 级别的基础上增加请求及返回的 BODY 信息

OkHttp 设置网络日志请求拦截器

HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);

OkHttpClient client = new OkHttpClient.Builder()
				.connectTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
				.readTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
				.writeTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
				.addInterceptor(loggingInterceptor)
				.build();

其实,这四个等级已经能满足需求,不过,由于是使用 系统的 log 打印模块,每次只能把单个信息打印出来,而且还不能定位到具体的行数以及线程,真的不方便。

所以,这个时候就需要自己动手去改造原有的 OkHttp 的日志打印,不过我们想要的日志内容基本上是全覆盖了 HttpLoggingInterceptor 的后三个等级。所以,可以参考着他的源码来进行自己修改。
我想要达到的效果就是取消等级限制并使用第三方的日志输出统一打印:

话不多说,上代码:

private static Interceptor logInterceptor() {
		Interceptor logInterceptor = new Interceptor() {
				@Override
				public Response intercept(Chain chain) throws IOException {
						Request original = chain.request();
						Request.Builder requestBuilder = original.newBuilder();
						Request request = requestBuilder.url(original.url()).build();

						RequestBody requestBody = request.body();
						boolean hasRequestBody = requestBody != null;

						StringBuilder requestSb = new StringBuilder();
						Connection connection = chain.connection();
						Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1;
						requestSb.append("请求-->")
										.append(' ')
										.append(request.method())
										.append(' ')
										.append(request.url())
										.append(' ')
										.append(protocol)
										.append("\n");

						if (hasRequestBody) {
								if (requestBody.contentType() != null) {
										requestSb.append("Content-Type: ")
														.append(requestBody.contentType())
														.append("\n");
								}
								if (requestBody.contentLength() != -1) {
										requestSb.append("Content-Length: ")
														.append(requestBody.contentLength())
														.append("-byte body").append("\n");
								}
						}

						Headers requestHeaders = request.headers();
						for (int i = 0, count = requestHeaders.size(); i < count; i++) {
								String name = requestHeaders.name(i);
								requestSb.append(name)
												.append(": ")
												.append(requestHeaders.value(i));
								requestSb.append("\n");
						}

						if (hasRequestBody) {
								Buffer buffer = new Buffer();
								requestBody.writeTo(buffer);

								Charset charset = UTF8;
								MediaType contentType = requestBody.contentType();
								if (contentType != null) {
										charset = contentType.charset(UTF8);
								}
								if (isPlaintext(buffer)) {
										requestSb.append(buffer.readString(charset))
														.append("\n");
										requestSb.append("--> END ").
														append(request.method());
								} else {
										requestSb.append("--> END ")
														.append(request.method())
														.append(" (binary ")
														.append(requestBody.contentLength())
														.append("-byte body omitted)");
								}
						}
						Logger.i(requestSb.toString());

						long startNs = System.nanoTime();
						Response response;
						try {
								response = chain.proceed(request);
						} catch (Exception e) {
								Logger.e("<-- HTTP FAILED: " + e);
								throw e;
						}

						long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
						ResponseBody responseBody = response.body();
						BufferedSource source = null;
						Buffer buffer = null;
						int responseCode = response.code();
						if (responseBody != null) {
								source = responseBody.source();
								source.request(Long.MAX_VALUE);
								buffer = source.buffer();
						}
						long contentLength = responseBody.contentLength();

						StringBuilder responseSb = new StringBuilder();
						responseSb.append("返回<-- ")
										.append(response.code())
										.append(' ')
										.append(response.message())
										.append(' ')
										.append(response.request().url())
										.append(" (")
										.append(tookMs)
										.append("ms").append(')')
										.append("\n");

						Headers headers = response.headers();
						for (int i = 0, count = headers.size(); i < count; i++) {
								responseSb.append(headers.name(i))
												.append(": ")
												.append(headers.value(i))
												.append("\n");
						}
						Charset charset = UTF8;
						MediaType contentType = responseBody.contentType();
						if (contentType != null) {
								try {
										charset = contentType.charset(UTF8);
								} catch (UnsupportedCharsetException e) {
										Logger.i("Couldn't decode the response body; charset is likely malformed.");
										Logger.i("<-- END HTTP");
										return response;
								}
						}
						if (!isPlaintext(buffer)) {
								Logger.i("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
								return response;
						}

						if (contentLength != 0) {
								Logger.i("返回数据: " + buffer.clone().readString(charset));
						}

						responseSb.append("<-- END HTTP (")
										.append(buffer.size())
										.append("-byte body)");
						Logger.i(responseSb.toString());

						return response;
				}
		};
		return logInterceptor;
}

private static boolean isPlaintext(Buffer buffer) {
		try {
				Buffer prefix = new Buffer();
				long byteCount = buffer.size() < 64 ? buffer.size() : 64;
				buffer.copyTo(prefix, 0, byteCount);
				for (int i = 0; i < 16; i++) {
						if (prefix.exhausted()) {
								break;
						}
						int codePoint = prefix.readUtf8CodePoint();
						if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
								return false;
						}
				}
				return true;
		} catch (EOFException e) {
				return false;
		}
}

代码虽然有些长,但还是很容易理解。就是分别将 Request 以及 Response 进行拦截,得到想展示出来的内容信息,然后拼接起来,最后使用 Logger 进行打印。打印出来的效果还是比较不错的。在这里拦截到 Response 之后,我把所有的异常统一进行捕捉了,还可以添加一些其他的异常信息,比如:连接超时的异常、URL 错误的异常等等。

主要就是参考 OkHttp 里的 HttpLoggingInterceptor 这个类,可以自己尝试一下。

  • 请求打印
01-30 11:09:55.085 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:288)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 请求--> POST http://192.168.199.186:8080/report/api/v1/drp/checkInputBill/create?accountsuite=demo http/1.1
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Type: application/json; charset=UTF-8
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Length: 1576-byte body
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Conteng-type: application/json;charset=UTF-8
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Authorization: Basic YWRtaW46MTExMTEx
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ {"checkInputBill":[{"createOperatorName":"admin","createOperatorCode":"admin","handWorkBill":"4001","checkNoticeBillId":"31e4fb9dd3b441dea77904ec9a435b7e","checkInputBillItems":[{"quantity":"1","barCode1":"0001V002201","sizeName":"XS","skuIds":"4028471e6116e54201611c4311070062","productCode":"0001V","colorName":"米红","barCode2":"0001V002201"},{"quantity":"1","barCode1":"0010003","sizeName":"165/92A","skuIds":"aaf9426940bb5b7b0140bee72e873f5e","productCode":"001首饰","colorName":"均色","barCode2":"0010003"},{"quantity":"1","barCode1":"000DX0039","sizeName":"160/88A","skuIds":"4028810c5b88f46c015b896754810002","productCode":"0040Z","colorName":"纯白","barCode2":"0040Z0102"},{"quantity":"4","barCode1":"000DY0340","sizeName":"165/92A","skuIds":"4028810c5b88f46c015b896754820006","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0203"},{"quantity":"1","barCode1":"000DY0539","sizeName":"170/96A","skuIds":"4028810c5b88f46c015b896754820007","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0204"},{"quantity":"2","barCode1":"1030620027103","sizeName":"165/92A","skuIds":"aaf9426948850ec40148893130014f7c","productCode":"杏色V领下摆纱衫","colorName":"浅杏","barCode2":"null"}],"createOperatorId":"40288996207b57eb01207b5c81ee0004","remark":"4001:4001","updateTime":"2018-01-12 16:42:38","createTime":"2018-01-12 15:29:49","backGroundColorId":"1","checkNoticeBill":"PDT18011100009","checkInputBillId":"dc1c7d1d3b454da3903e0beec7da2e8f"}],"checkType":1,"checkTime":"2018-02-01 02:30:00","operatorId":"40288996207b57eb01207b5c81ee0004"}
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ --> END POST
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════
  • 结果打印
01-30 11:09:55.264 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:386)
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 返回数据: {"result":true,"checkInputBill":[{"createOperatorName":"admin","createOperatorCode":"admin","handWorkBill":"4001","checkNoticeBillId":"31e4fb9dd3b441dea77904ec9a435b7e","checkInputBillItems":[{"quantity":"1","barCode1":"0001V002201","sizeName":"XS","skuIds":"4028471e6116e54201611c4311070062","productCode":"0001V","colorName":"米红","barCode2":"0001V002201"},{"quantity":"1","barCode1":"0010003","sizeName":"165/92A","skuIds":"aaf9426940bb5b7b0140bee72e873f5e","productCode":"001首饰","colorName":"均色","barCode2":"0010003"},{"quantity":"1","barCode1":"000DX0039","sizeName":"160/88A","skuIds":"4028810c5b88f46c015b896754810002","productCode":"0040Z","colorName":"纯白","barCode2":"0040Z0102"},{"quantity":"4","barCode1":"000DY0340","sizeName":"165/92A","skuIds":"4028810c5b88f46c015b896754820006","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0203"},{"quantity":"1","barCode1":"000DY0539","sizeName":"170/96A","skuIds":"4028810c5b88f46c015b896754820007","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0204"},{"quantity":"2","barCode1":"1030620027103","sizeName":"165/92A","skuIds":"aaf9426948850ec40148893130014f7c","productCode":"杏色V领下摆纱衫","colorName":"浅杏","barCode2":"null"}],"createOperatorId":"40288996207b57eb01207b5c81ee0004","remark":"4001:4001","updateTime":"2018-01-12 16:42:38","createTime":"2018-01-12 15:29:49","backGroundColorId":"1","checkNoticeBill":"PDT18011100009","checkInputBillId":"dc1c7d1d3b454da3903e0beec7da2e8f","method":"update"}]}
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:390)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 返回<-- 200 OK http://192.168.199.186:8080/report/api/v1/drp/checkInputBill/create?accountsuite=demo (176ms)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Type: application/json;charset=UTF-8
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Transfer-Encoding: chunked
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Server: Jetty(7.6.14.v20131031)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Cache-Control: public, max-age=60
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ <-- END HTTP (1512-byte body)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════

可以看到 Logger 打印的内容还是挺丰富的,包含了行号,线程再加上那个范围框,更能直观的看出日志内容。

与 RxJava 相结合

现在网络请求都是 Retrofit 配合 RxJava ,针对 RxJava 有两点可以进行优化改进的地方。

IO 线程与 Android 主线程的切换

要知道在 Android 中是不能将耗时操作放在主线程的,以前自己通过 Thread + HttpURLConnection 实现网络请求的时候使用的是 Handler 进行处理线程切换,现在换成了 RxJava 就更方便了,不过,每次都要写那两行去切换线程,虽然就那么两行,但也还是不想写。

RxJava 有一个操作符 compose() 异常的强大,可以在保持原有 RxJava 编程风格不变的情况下完成自定义的操作。 ObservableTransformer 是 RxJava 中一个强大的接口,用于将类型 A 转换成类型 B ,这么说有些牵强了,其实就是把上游的内容经过 ObservableTransformer 转换成下游想要的类型,当然如果类型不变,也是可以进行的或是进行一些操作。

这里就需要结合 compose 和 ObservableTransformer 来一起使用,看代码吧。

public static <T> ObservableTransformer<T, T> applySchedulers() {
		return new ObservableTransformer<T, T>() {
				@Override
				public ObservableSource<T> apply(Observable<T> upstream) {
						return upstream.subscribeOn(Schedulers.io())
										.observeOn(AndroidSchedulers.mainThread());
				}
		};
}

当然,如果习惯用 Java 8 的 Lambda 表达式,看起来就更简洁了。简洁是挺简洁的,不过有些不熟悉的方法,就完全不知道什么意思了,像我这样的新手还是不用的好。

public static <T> ObservableTransformer<T, T> applySchedulers() {
		return upstream -> upstream.subscribeOn(Schedulers.io())
						.observeOn(AndroidSchedulers.mainThread());
}

网络数据封装

在实际的开发过程中,基本上服务器返回的内容都是这样的:

{
    resultNo: 200,
    message: "成功"
    result: [],
    error: false
}

这样的返回信息,包含了一些状态信息及具体的数据,其中 resultNo 和 message 以及 error 都是类型不会变的状态信息,只有其中的 result 是会动态变化的具体数据,可能里面是一个 List 也可能里面是一个 布尔值变量,甚至还有可能嵌套好几层。所以可以把 result 定义为泛型 T ,这样在具体到单一个接口数据的时候再指定类型即可。

public class BaseResponse<T> {
    private boolean error;
    private T results;

    /*
     * get and set 
     */
}

如果是这样的话,我们在定义网络接口的时候,就可以指明实际返回的数据类型了。例如:

@GET("data/{type}/{size}/{page}")
Observable<BaseResponse<List<Map<String, Object>>>> typeData(@Path("type") String type, @Path("size") int size, @Path("page") int page);

不过,这样使用了 BaseResponse 之后,在 Observable 实现订阅( subscribe )的时候,提供的参数类型也就成了 BaseResponse<List<Map<String, Object>>> response ,这时候,需要我们从 response 中再取一次,多写一行?不可能的。

上面在进行线程切换的时候,用到了 ObservableTransformer 这里也可以用,上面是相同的事件类型使用它来进行转换,只不过现在变成了,不同类型的数据转换。

public static <T> ObservableTransformer<BaseResponse<T>, T> handleResult() {
		return new ObservableTransformer<BaseResponse<T>, T>() {
				@Override
				public ObservableSource<T> apply(Observable<BaseResponse<T>> upstream) {
						return upstream.flatMap(new Function<BaseResponse<T>, ObservableSource<T>>() {
								@Override
								public ObservableSource<T> apply(BaseResponse<T> response) throws Exception {
										if (response == null) {
												return Observable.empty();
										} else if (response.getResults() == null) {
												return Observable.empty();
										} else {
												return Observable.just(response.getResults());
										}
								}
						});
				}
		};
}

同样的,实现 ObservableTransformer 中的 apply() 方法,这时候就不是简单的线程切换了,而是需要进行类型之间的转换。这里我只是简单的进行类型转换,其实还可以加入一些简单的逻辑判断。在这里,我们已经可以获取到 BaseResponse 了,那么我们可以根据其中的一些公共内容进行判断,然后进行发射(RxJava)不同的事件,比如,跟后台开发约定好一些错误代码,如 404 ,那么在这里,就可以加入 针对 404 的事件处理,可以是 发射一个自定义异常或是其他的操作;也可以是针对返回的 result 进行内容判断,判断一次是否为空。