在上一篇基于retrofit的网络框架的终极封装(一)中介绍了顶层api的设计.这里再沿着代码走向往里说.
因为这里讲的是retrofit的封装性使用,因此一些retrofit基础性的使用和配置这里就不讲了.java
全部网络请求相关的参数和配置所有经过第一层的api和链式调用封装到了ConfigInfo中,最后在start()方法中调用retrofit层,开始网络请求.git
/** * 在这里组装请求,而后发出去 * @param <E> * @return */ @Override public <E> ConfigInfo<E> start(ConfigInfo<E> configInfo) { String url = Tool.appendUrl(configInfo.url, isAppendUrl());//组拼baseUrl和urltail configInfo.url = url; configInfo.listener.url = url; //todo 这里token还可能在请求头中,应加上此类状况的自定义. if (configInfo.isAppendToken){ Tool.addToken(configInfo.params); } if (configInfo.loadingDialog != null && !configInfo.loadingDialog.isShowing()){ try {//预防badtoken最简便和直接的方法 configInfo.loadingDialog.show(); }catch (Exception e){ } } if (getCache(configInfo)){//异步,去拿缓存--只针对String类型的请求 return configInfo; } T request = generateNewRequest(configInfo);//根据类型生成/执行不一样的请求对象 /* 这三个方式是给volley预留的 setInfoToRequest(configInfo,request); cacheControl(configInfo,request); addToQunue(request);*/ return configInfo; }
分类生成/执行各种请求:github
private <E> T generateNewRequest(ConfigInfo<E> configInfo) { int requestType = configInfo.type; switch (requestType){ case ConfigInfo.TYPE_STRING: case ConfigInfo.TYPE_JSON: case ConfigInfo.TYPE_JSON_FORMATTED: return newCommonStringRequest(configInfo); case ConfigInfo.TYPE_DOWNLOAD: return newDownloadRequest(configInfo); case ConfigInfo.TYPE_UPLOAD_WITH_PROGRESS: return newUploadRequest(configInfo); default:return null; } }
因此,对retrofit的使用,只要实现如下三个方法就好了:
若是切换到volley或者其余网络框架,也是实现这三个方法就行了.json
newCommonStringRequest(configInfo), newDownloadRequest(configInfo); newUploadRequest(configInfo)
@Override protected <E> Call newCommonStringRequest(final ConfigInfo<E> configInfo) { Call<ResponseBody> call; if (configInfo.method == HttpMethod.GET){ call = service.executGet(configInfo.url,configInfo.params); }else if (configInfo.method == HttpMethod.POST){ if(configInfo.paramsAsJson){//参数在请求体以json的形式发出 String jsonStr = MyJson.toJsonStr(configInfo.params); Log.e("dd","jsonstr request:"+jsonStr); RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr); call = service.executeJsonPost(configInfo.url,body); }else { call = service.executePost(configInfo.url,configInfo.params); } }else { configInfo.listener.onError("不是get或post方法");//暂时不考虑其余方法 call = null; return call; } configInfo.tagForCancle = call; call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, final Response<ResponseBody> response) { if (!response.isSuccessful()){ configInfo.listener.onCodeError("http错误码为:"+response.code(),response.message(),response.code()); Tool.dismiss(configInfo.loadingDialog); return; } String string = ""; try { string = response.body().string(); Tool.parseStringByType(string,configInfo); Tool.dismiss(configInfo.loadingDialog); } catch (final IOException e) { e.printStackTrace(); configInfo.listener.onError(e.toString()); Tool.dismiss(configInfo.loadingDialog); } } @Override public void onFailure(Call<ResponseBody> call, final Throwable t) { configInfo.listener.onError(t.toString()); Tool.dismiss(configInfo.loadingDialog); } }); return call; }
既然要封装,确定就不能用retrofit的常规用法:ApiService接口里每一个接口文档上的接口都写一个方法,而是应该用QueryMap/FieldMap注解,接受一个以Map形式封装好的键值对.这个与咱们上一层的封装思路和形式都是同样的.api
@GET() Call<ResponseBody> executGet(@Url String url, @QueryMap Map<String, String> maps); /** * 注意: * 1.若是方法的泛型指定的类不是ResonseBody,retrofit会将返回的string成用json转换器自动转换该类的一个对象,转换不成功就报错. * 若是不须要gson转换,那么就指定泛型为ResponseBody, * 只能是ResponseBody,子类都不行,同理,下载上传时,也必须指定泛型为ResponseBody * 2. map不能为null,不然该请求不会执行,但能够size为空. * 3.使用@url,而不是@Path注解,后者放到方法体上,会强制先urlencode,而后与baseurl拼接,请求没法成功. * @param url * @param map * @return */ @FormUrlEncoded @POST() Call<ResponseBody> executePost(@Url String url, @FieldMap Map<String, String> map);
/** * 直接post体为一个json格式时,使用这个方法.注意:@Body 不能与@FormUrlEncoded共存 * @param url * @param body * @return */ @POST() Call<ResponseBody> executeJsonPost(@Url String url, @Body RequestBody body);
retrofit其实有请求时传入一个javabean的注解方式,确实能够在框架内部转换成json.可是不适合封装.数组
其实很简单,搞清楚以json形式发出参数的本质: 请求体中的json本质上仍是一个字符串.那么能够将Map携带过来的参数转成json字符串,而后用RequestBody包装一层就行了:缓存
String jsonStr = MyJson.toJsonStr(configInfo.params); RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr); call = service.executeJsonPost(configInfo.url,body);
@GET() <T> Call<BaseNetBean<T>> getStandradJson(@Url String url, @QueryMap Map<String, String> maps); //注:BaseNetBean就是三个标准字段的json: public class BaseNetBean<T>{ public int code; public String msg; public T data; }
这样写会抛出异常:
报的错误服务器
Method return type must not include a type variable or wildcard: retrofit2.Call<T>
JakeWharton的回复:
You cannot. Type information needs to be fully known at runtime in order for deserialization to work.cookie
由于上面的缘由,咱们只能经过retrofit发请求,返回一个String,本身去解析.但这也有坑:网络
1.不能写成下面的形式:
@GET() Call<String> executGet(@Url String url, @QueryMap Map<String, String> maps);
你觉得指定泛型为String它就返回String,不,你还太年轻了.
这里的泛型,意思是,使用retrofit内部的json转换器,将response里的数据转换成一个实体类xxx,好比UserBean之类的,而String类明显不是一个有效的实体bean类,天然转换失败.
因此,要让retrofit不适用内置的json转换功能,你应该直接指定类型为ResponseBody:
@GET() Call<ResponseBody> executGet(@Url String url, @QueryMap Map<String, String> maps);
2.既然不采用retrofit内部的json转换功能,那就要在回调那里本身拿到字符串,用本身的json解析了.那么坑又来了:
泛型擦除:
回调接口上指定泛型,在回调方法里直接拿到泛型,这是在java里很常见的一个泛型接口设计:
public abstract class MyNetListener<T>{ public abstract void onSuccess(T response,String resonseStr); .... } //使用: call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, final Response<ResponseBody> response) { String string = response.body().string(); Gson gson = new Gson(); Type objectType = new TypeToken<T>() {}.getType(); final T bean = gson.fromJson(string,objectType); configInfo.listener.onSuccess(bean,string); ... } ... }
可是,抛出异常:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to xxx
这是由于在运行过程当中,经过泛型传入的类型T丢失了,因此没法转换,这叫作泛型擦除:.
要解析的话,仍是老老实实传入javabean的class吧.因此在最顶层的API里,有一个必须传的Class clazz:
postStandardJson(String url, Map map, Class clazz, MyNetListener listener)
综上,咱们须要传入class对象,彻底本身去解析json.解析已封装成方法.也是根据三个不一样的小类型(字符串,通常json,标准json)
这里处理缓存时,若是要缓存内容,固然是缓存成功的内容,失败的就没必要缓存了.
Tool.parseStringByType(string,configInfo); public static void parseStringByType(final String string, final ConfigInfo configInfo) { switch (configInfo.type){ case ConfigInfo.TYPE_STRING: //缓存 cacheResponse(string, configInfo); //处理结果 configInfo.listener.onSuccess(string, string); break; case ConfigInfo.TYPE_JSON: parseCommonJson(string,configInfo); break; case ConfigInfo.TYPE_JSON_FORMATTED: parseStandJsonStr(string, configInfo); break; } }
json解析框架选择,gson,fastjson随意,不过最好也是本身再包一层api:
public static <T> T parseObject(String str,Class<T> clazz){ // return new Gson().fromJson(str,clazz); return JSON.parseObject(str,clazz); }
private static <E> void parseCommonJson( String string, ConfigInfo<E> configInfo) { if (isJsonEmpty(string)){ configInfo.listener.onEmpty(); }else { try{ if (string.startsWith("{")){ E bean = MyJson.parseObject(string,configInfo.clazz); configInfo.listener.onSuccessObj(bean ,string,string,0,""); cacheResponse(string, configInfo); }else if (string.startsWith("[")){ List<E> beans = MyJson.parseArray(string,configInfo.clazz); configInfo.listener.onSuccessArr(beans,string,string,0,""); cacheResponse(string, configInfo); }else { configInfo.listener.onError("不是标准json格式"); } }catch (Exception e){ e.printStackTrace(); configInfo.listener.onError(e.toString()); } } }
三个字段对应的数据直接用jsonObject.optString来取:
JSONObject object = null; try { object = new JSONObject(string); } catch (JSONException e) { e.printStackTrace(); configInfo.listener.onError("json 格式异常"); return; } String key_data = TextUtils.isEmpty(configInfo.key_data) ? NetDefaultConfig.KEY_DATA : configInfo.key_data; String key_code = TextUtils.isEmpty(configInfo.key_code) ? NetDefaultConfig.KEY_CODE : configInfo.key_code; String key_msg = TextUtils.isEmpty(configInfo.key_msg) ? NetDefaultConfig.KEY_MSG : configInfo.key_msg; final String dataStr = object.optString(key_data); final int code = object.optInt(key_code); final String msg = object.optString(key_msg);
注意,optString后字符串为空的判断:一个字段为null时,optString的结果是字符串"null"而不是null
public static boolean isJsonEmpty(String data){ if (TextUtils.isEmpty(data) || "[]".equals(data) || "{}".equals(data) || "null".equals(data)) { return true; } return false; }
而后就是相关的code状况的处理和回调:
状态码为未登陆时,执行自动登陆的逻辑,自动登陆成功后再重发请求.登陆不成功才执行unlogin()回调.
注意data字段多是一个普通的String,而不是json.
private static <E> void parseStandardJsonObj(final String response, final String data, final int code, final String msg, final ConfigInfo<E> configInfo){ int codeSuccess = configInfo.isCustomCodeSet ? configInfo.code_success : BaseNetBean.CODE_SUCCESS; int codeUnFound = configInfo.isCustomCodeSet ? configInfo.code_unFound : BaseNetBean.CODE_UN_FOUND; int codeUnlogin = configInfo.isCustomCodeSet ? configInfo.code_unlogin : BaseNetBean.CODE_UNLOGIN; if (code == codeSuccess){ if (isJsonEmpty(data)){ if(configInfo.isResponseJsonArray()){ configInfo.listener.onEmpty(); }else { configInfo.listener.onError("数据为空"); } }else { try{ if (data.startsWith("{")){ final E bean = MyJson.parseObject(data,configInfo.clazz); configInfo.listener.onSuccessObj(bean ,response,data,code,msg); cacheResponse(response, configInfo); }else if (data.startsWith("[")){ final List<E> beans = MyJson.parseArray(data,configInfo.clazz); configInfo.listener.onSuccessArr(beans,response,data,code,msg); cacheResponse(response, configInfo); }else {//若是data的值是一个字符串,而不是标准json,那么直接返回 if (String.class.equals(configInfo.clazz) ){//此时,E也应该是String类型.若是有误,会抛出到下面catch里 configInfo.listener.onSuccess((E) data,data); }else { configInfo.listener.onError("不是标准的json数据"); } } }catch (final Exception e){ e.printStackTrace(); configInfo.listener.onError(e.toString()); return; } } }else if (code == codeUnFound){ configInfo.listener.onUnFound(); }else if (code == codeUnlogin){ //自动登陆 configInfo.client.autoLogin(new MyNetListener() { @Override public void onSuccess(Object response, String resonseStr) { configInfo.client.resend(configInfo); } @Override public void onError(String error) { super.onError(error); configInfo.listener.onUnlogin(); } }); }else { configInfo.listener.onCodeError(msg,"",code); } }
先不考虑多线程下载和断点续传的问题,就单单文件下载而言,用retrofit写仍是挺简单的
不能像上面字符流类型的请求同样设置多少s,而应该设为0,也就是不限时:
OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //设置超时
@Streaming //流式下载,不加这个注解的话,会整个文件字节数组所有加载进内存,可能致使oom @GET Call<ResponseBody> download(@Url String fileUrl);
这里用的是一个异步任务框架,其实用Rxjava更好.
SimpleTask<Boolean> simple = new SimpleTask<Boolean>() { @Override protected Boolean doInBackground() { return writeResponseBodyToDisk(response.body(),configInfo.filePath); } @Override protected void onPostExecute(Boolean result) { Tool.dismiss(configInfo.loadingDialog); if (result){ configInfo.listener.onSuccess(configInfo.filePath,configInfo.filePath); }else { configInfo.listener.onError("文件下载失败"); } } }; simple.execute();
byte[] fileReader = new byte[4096]; long fileSize = body.contentLength(); long fileSizeDownloaded = 0; inputStream = body.byteStream(); outputStream = new FileOutputStream(futureStudioIconFile); while (true) { int read = inputStream.read(fileReader); if (read == -1) { break; } outputStream.write(fileReader, 0, read); fileSizeDownloaded += read; Log.d("io", "file download: " + fileSizeDownloaded + " of " + fileSize);// 这里也能够实现进度监听 }
1.添加下载时更新进度的拦截器
okHttpClient .addInterceptor(new ProgressInterceptor())
2.ProgressInterceptor:实现Interceptor接口的intercept方法,拦截网络响应
@Override public Response intercept(Interceptor.Chain chain) throws IOException{ Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),chain.request().url().toString())).build(); }
3 ProgressResponseBody: 继承 ResponseBody ,在内部网络流传输过程当中读取进度:
public class ProgressResponseBody extends ResponseBody { private final ResponseBody responseBody; private BufferedSource bufferedSource; private String url; public ProgressResponseBody(ResponseBody responseBody,String url) { this.responseBody = responseBody; this.url = url; } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } long timePre = 0; long timeNow; private Source source(final Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); totalBytesRead += bytesRead != -1 ? bytesRead : 0; timeNow = System.currentTimeMillis(); if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次状态 timePre = timeNow; EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(), totalBytesRead == responseBody.contentLength(),url)); } return bytesRead; } }; } }
通常进度数据用于更新UI,因此最好设置数据传出的时间间隔,不要太频繁:
timeNow = System.currentTimeMillis(); if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次状态 timePre = timeNow; EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(), totalBytesRead == responseBody.contentLength(),url)); }
注意: MyNetListener与url绑定,以防止不一样下载间的进度错乱.
@Subscribe(threadMode = ThreadMode.MAIN) public void onMessage(ProgressEvent event){ if (event.url.equals(url)){ onProgressChange(event.totalLength,event.totalBytesRead); if (event.done){ unRegistEventBus(); onFinish(); } } }
文件上传相对于普通post请求有区别,你很是须要了解http文件上传的协议:
1.提交一个表单,若是包含文件上传,那么必须指定类型为multipart/form-data.这个在retrofit中经过@Multipart注解指定便可.
2.表单中还有其余键值对也要一同传递,在retrofit中经过@QueryMap以map形式传入,这个与普通post请求同样
3.服务器接收文件的字段名,以及上传的文件路径,经过@PartMap以map形式传入.这里的字段名对应请求体中Content-Disposition中的name字段的值.大多数服务器默认是file.(由于SSH框架默认的是file?)
4.请求体的content-type用于标识文件的具体MIME类型.在retrofit中,是在构建请求体RequestBody时指定的.须要咱们指定.
那么如何得到一个文件的MIMe类型呢?读文件的后缀名的话,不靠谱.最佳方式是读文件头,从文件头中拿到MIME类型.不用担忧,Android有相关的api的
综上,相关的封装以下:
OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS) .connectTimeout(0, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //设置超时
@POST() @Multipart Call<ResponseBody> uploadWithProgress(@Url String url,@QueryMap Map<String, String> options,@PartMap Map<String, RequestBody> fileParameters) ;
这里的回调就不用开后台线程了,由于流是在请求体中,而retrofit已经帮咱们搞定了请求过程的后台执行.
protected Call newUploadRequest(final ConfigInfo configInfo) { if (serviceUpload == null){ initUpload(); } configInfo.listener.registEventBus(); Map<String, RequestBody> requestBodyMap = new HashMap<>(); if (configInfo.files != null && configInfo.files.size() >0){ Map<String,String> files = configInfo.files; int count = files.size(); if (count>0){ Set<Map.Entry<String,String>> set = files.entrySet(); for (Map.Entry<String,String> entry : set){ String key = entry.getKey(); String value = entry.getValue(); File file = new File(value); String type = Tool.getMimeType(file);//拿到文件的实际类型 Log.e("type","mimetype:"+type); UploadFileRequestBody fileRequestBody = new UploadFileRequestBody(file, type,configInfo.url); requestBodyMap.put(key+"\"; filename=\"" + file.getName(), fileRequestBody); } } } Call<ResponseBody> call = service.uploadWithProgress(configInfo.url,configInfo.params,requestBodyMap);
public class UploadFileRequestBody extends RequestBody { private RequestBody mRequestBody; private BufferedSink bufferedSink; private String url; public UploadFileRequestBody(File file,String mimeType,String url) { // this.mRequestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file); this.mRequestBody = RequestBody.create(MediaType.parse(mimeType), file); this.url = url; } @Override public MediaType contentType() { return mRequestBody.contentType(); }
封装在UploadFileRequestBody中,无需经过okhttp的拦截器实现,由于能够在构建RequestBody的时候就包装好(看上面代码),就不必用拦截器了.
到这里,主要的请求执行和回调就算讲完了,但还有一些,好比缓存控制,登陆状态的维护,以及cookie管理,请求的取消,gzip压缩,本地时间校准等等必需的辅助功能的实现和维护,这些将在下一篇文章进行解析.