前言
上个知识点介绍了OKHttp的基本使用,在Activity中写了大量访问网络的代码,这种代码写起来很无聊,并且对技术没什么提升。在真实的企业开发中,肯定是把这些代码封装起来,做一个库,给Activity调用。
封装之前我们需要考虑以下这些问题:
- 封装基本的公共方法给外部调用。get请求,Post请求,PostFile
- 官方建议OkHttpClient实例只new一次,那我们网络请求库可以做成单例模式。
- 如果同一时间访问同一个api多次,我们是不是应该取消之前的请求?
- 如果用户连接Http代理了,就不让访问,防止用户通过抓包工具看我们的接口数据。
- 每个接口都要带上的参数如何封装?例如app版本号,设备号,登录之后的用户token,这些参数可能每次请求都要带上。
- 返回的json字符串转成实体对象
- 访问服务器是异步请求,如何在主线程中调用回调接口?
代码实现
首先需要在线引用以下三个依赖库
compile 'com.squareup.okhttp3:okhttp:3.2.0' //okhttp
compile 'com.google.code.gson:gson:2.7' //解析jsons数据
compile 'io.github.lizhangqu:coreprogress:1.0.2' //上传下载回调监听
新建一个HttpConfig类,用来配置一些请求参数,存储请求服务器的公共参数。设置连接时间等。。。
public class HttpConfig {
private boolean debug=false;//true:debug模式
private String userAgent="";//用户代理 它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。
private boolean agent=true;//有代理的情况能不能访问,true:有代理能访问 false:有代理不能访问
private String tagName="Http";
private int connectTimeout=10;//连接超时时间 单位:秒
private int writeTimeout=10;//写入超时时间 单位:秒
private int readTimeout=30;//读取超时时间 单位:秒
//通用字段
private List<NameValuePair> commonField=new ArrayList<>();
public boolean isDebug() {
return debug;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public boolean isAgent() {
return agent;
}
public void setAgent(boolean agent) {
this.agent = agent;
}
public String getTagName() {
return tagName;
}
public void setTagName(String tagName) {
this.tagName = tagName;
}
public List<NameValuePair> getCommonField() {
return commonField;
}
public int getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
public int getWriteTimeout() {
return writeTimeout;
}
public void setWriteTimeout(int writeTimeout) {
this.writeTimeout = writeTimeout;
}
public int getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
}
/**
* 更新参数
* @param key
* @param value
*/
public void updateCommonField(String key,String value){
boolean result = true;
for(int i=0;i<commonField.size();i++){
NameValuePair nameValuePair = commonField.get(i);
if(nameValuePair.getName().equals(key)){
commonField.set(i,new NameValuePair(key,value));
result = false;
break;
}
}
if(result){
commonField.add(new NameValuePair(key,value));
}
}
/**
* 删除公共参数
* @param key
*/
public void removeCommonField(String key){
for(int i=commonField.size()-1;i>=0;i--){
if(commonField.get(i).equals("key")){
commonField.remove(i);
}
}
}
/**
* 添加请求参数
* @param key
* @param value
*/
public void addCommonField(String key,String value){
commonField.add(new NameValuePair(key,value));
}
}
我们给服务器提交表单数据都是通过name跟vulue的形式提交数据,封装了NameValuePair类。如果是上传文件,isFile设置true就行。
public class NameValuePair {
private String name;//请求名称
private String value;//请求值
private boolean isFile=false;//是否是文件
public NameValuePair(String name, String value){
this.name=name;
this.value = value;
}
public NameValuePair(String name, String value,boolean isFile){
this.name=name;
this.value = value;
this.isFile=isFile;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public boolean isFile() {
return isFile;
}
public void setFile(boolean file) {
isFile = file;
}
}
请求服务器成功或者失败都会回调Callback接口,我们封装HttpResponseHandler抽象类来实现这个接口,对服务器返回的数据以及状态码进行了简单的过滤,最终最调用自己的onFailure跟onSuccess方法。
public abstract class HttpResponseHandler implements Callback {
public HttpResponseHandler(){
}
public void onFailure(Call call, IOException e){
onFailure(-1,e.getMessage().getBytes());
}
public void onResponse(Call call, Response response) throws IOException {
int code =response.code();
byte[] body = response.body().bytes();
if(code>299){
onFailure(response.code(),body);
}else{
Headers headers = response.headers();
Header[] hs = new Header[headers.size()];
for (int i=0;i<headers.size();i++){
hs[i] = new Header(headers.name(i),headers.value(i));
}
onSuccess(code,hs,body);
}
}
public void onFailure(int status,byte[] data){
}
// public void onProgress(int bytesWritten, int totalSize) {
// }
public abstract void onSuccess(int statusCode, Header[] headers, byte[] responseBody);
}
Get请求封装
封装后的HTTPCaller代码如下,现在里面只有get请求,如果一下子把post请求跟postFile方法贴进来,代码有点多。
public class HTTPCaller {
private static HTTPCaller _instance = null;
private OkHttpClient client;//okhttp对象
private Map<String,Call> requestHandleMap = null;//以URL为KEY存储的请求
private CacheControl cacheControl = null;//缓存控制器
private Gson gson = null;
private HttpConfig httpConfig=new HttpConfig();//配置信息
private HTTPCaller() {}
public static HTTPCaller getInstance(){
if (_instance == null) {
_instance = new HTTPCaller();
}
return _instance;
}
/**
* 设置配置信息 这个方法必需要调用一次
* @param httpConfig
*/
public void setHttpConfig(HttpConfig httpConfig) {
this.httpConfig = httpConfig;
client = new OkHttpClient.Builder()
.connectTimeout(httpConfig.getConnectTimeout(), TimeUnit.SECONDS)
.writeTimeout(httpConfig.getWriteTimeout(), TimeUnit.SECONDS)
.readTimeout(httpConfig.getReadTimeout(), TimeUnit.SECONDS)
.build();
gson = new Gson();
requestHandleMap = Collections.synchronizedMap(new WeakHashMap<String, Call>());
cacheControl =new CacheControl.Builder().noStore().noCache().build();//不使用缓存
}
public <T> void get(Class<T> clazz,final String url,Header[] header,final RequestDataCallback<T> callback) {
this.get(clazz,url,header,callback,true);
}
/**
* get请求
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param callback 回调接口
* @param autoCancel 是否自动取消 true:同一时间请求一个接口多次 只保留最后一个
* @param <T>
*/
public <T> void get(final Class<T> clazz,final String url,Header[] header,final RequestDataCallback<T> callback, boolean autoCancel){
if (checkAgent()) {
return;
}
add(url,getBuilder(url, header, new MyHttpResponseHandler(clazz,url,callback)),autoCancel);
}
private Call getBuilder(String url, Header[] header, HttpResponseHandler responseCallback) {
url=Util.getMosaicParameter(url,httpConfig.getCommonField());//拼接公共参数
Request.Builder builder = new Request.Builder();
builder.url(url);
builder.get();
return execute(builder, header, responseCallback);
}
private Call execute(Request.Builder builder, Header[] header, Callback responseCallback) {
boolean hasUa = false;
if (header == null) {
builder.header("Connection","close");
builder.header("Accept", "*/*");
} else {
for (Header h : header) {
builder.header(h.getName(), h.getValue());
if (!hasUa && h.getName().equals("User-Agent")) {
hasUa = true;
}
}
}
if (!hasUa&&!TextUtils.isEmpty(httpConfig.getUserAgent())){
builder.header("User-Agent",httpConfig.getUserAgent());
}
Request request = builder.cacheControl(cacheControl).build();
Call call = client.newCall(request);
call.enqueue(responseCallback);
return call;
}
public class MyHttpResponseHandler<T> extends HttpResponseHandler {
private Class<T> clazz;
private String url;
private RequestDataCallback<T> callback;
public MyHttpResponseHandler(Class<T> clazz,String url,RequestDataCallback<T> callback){
this.clazz=clazz;
this.url=url;
this.callback=callback;
}
@Override
public void onFailure(int status, byte[] data) {
clear(url);
try {
printLog(url + " " + status + " " + new String(data, "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sendCallback(callback);
}
@Override
public void onSuccess(int status,final Header[] headers, byte[] responseBody) {
try {
clear(url);
String str = new String(responseBody,"utf-8");
printLog(url + " " + status + " " + str);
T t = gson.fromJson(str, clazz);
sendCallback(status,t,responseBody,callback);
} catch (Exception e){
if (httpConfig.isDebug()) {
e.printStackTrace();
printLog("自动解析错误:" + e.toString());
}
sendCallback(callback);
}
}
}
private void autoCancel(String function){
Call call = requestHandleMap.remove(function);
if (call != null) {
call.cancel();
}
}
private void add(String url,Call call) {
add(url,call,true);
}
/**
* 保存请求信息
* @param url 请求url
* @param call http请求call
* @param autoCancel 自动取消
*/
private void add(String url,Call call,boolean autoCancel) {
if (!TextUtils.isEmpty(url)){
if (url.contains("?")) {//get请求需要去掉后面的参数
url=url.substring(0,url.indexOf("?"));
}
if(autoCancel){
autoCancel(url);//如果同一时间对api进行多次请求,自动取消之前的
}
requestHandleMap.put(url,call);
}
}
private void clear(String url){
if (url.contains("?")) {//get请求需要去掉后面的参数
url=url.substring(0,url.indexOf("?"));
}
requestHandleMap.remove(url);
}
private void printLog(String content){
if(httpConfig.isDebug()){
Log.i(httpConfig.getTagName(),content);
}
}
/**
* 检查代理
* @return
*/
private boolean checkAgent() {
if (httpConfig.isAgent()){
return false;
} else {
String proHost = android.net.Proxy.getDefaultHost();
int proPort = android.net.Proxy.getDefaultPort();
if (proHost==null || proPort<0){
return false;
}else {
Log.i(httpConfig.getTagName(),"有代理,不能访问");
return true;
}
}
}
//更新字段值
public void updateCommonField(String key,String value){
httpConfig.updateCommonField(key,value);
}
public void removeCommonField(String key){
httpConfig.removeCommonField(key);
}
public void addCommonField(String key,String value){
httpConfig.addCommonField(key,value);
}
private <T> void sendCallback(RequestDataCallback<T> callback){
sendCallback(-1,null,null,callback);
}
private <T> void sendCallback(int status,T data,byte[] body,RequestDataCallback<T> callback){
CallbackMessage<T> msgData = new CallbackMessage<T>();
msgData.body = body;
msgData.status = status;
msgData.data = data;
msgData.callback = callback;
Message msg = handler.obtainMessage();
msg.obj = msgData;
handler.sendMessage(msg);
}
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
CallbackMessage data = (CallbackMessage)msg.obj;
data.callback();
}
};
private class CallbackMessage<T>{
public RequestDataCallback<T> callback;
public T data;
public byte[] body;
public int status;
public void callback(){
if(callback!=null){
if(data==null){
callback.dataCallback(null);
}else{
callback.dataCallback(status,data,body);
}
}
}
}
}
- getInstance 用来获取对象类的对象,因为是单例模式,这个方法无论调用多少次只会创建一个对象。
- setHttpConfig 创建了HTTPCaller对象的时候必须要调用该方法来初始化一些对象,通过参数httpConfig获取连接时间来初始化OkHttpClient对象,用WeakHashMap来存储每一次的请求。还需要初始化Gson对象。
- get 有两个同名方法,这是方法重载,如果你想同一时间访问同一接口多次,用下面那个方法,最后一个一个参数传false,如果没有要求就用上面那个。我们看到在get方法里面首先调用了getBuilder发起get请求,getBuilder会返回一个Call对象,这个方法的第三个参数传入一个MyHttpResponseHandler对象,这个对象实现了Callback接口,请求服务器成功或者失败会把结果回调这个类。最外层调用了add方法,请求url作为key,Call作为vuelue保存到map里面。
- getBuilder 这个方法的第一行调用Util的getMosaicParameter方法拼接公共参数,然后根据url生成Request.Builder对象,继续调用execute。
- execute 首先设置请求头信息,一般没有特殊要求请求都不需要设置,通过builder对象的cacheControl方法设置缓存控制,通过build方法生成Request,通过OkHttpClient对象的newCall方法生成一个call对象,最后调用enqueue方法,进行异步请求,传入一个回调接口。
MyHttpResponseHandler类继承我们自己写的HttpResponseHandler抽象类,重写两个方法onFailure跟onSuccess。
onFailure 先从map中删除这个请求,打印错误log,发送回调。
onSuccess 先从map中删除这个请求,把json字符串转成对象。发送回调。
- autoCancel 从map中删除这个请求,调用Call.call取消http请求。
- add 有两个同名的方法,方法重载。我们来分析后面这个。首先判断这个url是否为空,为空的话肯定啥也干不了。然后需要截取?前面的信息,不然参数不一样我们就没法判断是不是同一时间多次请求同一接口了。然后判断要不要自动取消。最后存入map。经过前面的分析我们都知道发起get请求的时候会调用add方法把call请求保存到map里面去,请求成功回调中会把call从map中删除。假如手机网络状态不稳定,同一时间请求了10次同一个接口,都没有回调回来,这也是为什么我们需要在添加的时候会自动取消之前的请求。
- clear 从map中删除请求
- printLog 打印log
- sendCallback 有两个方法,方法重载。把返回内容,状态,byte数组,回调封装到CallbackMessage对象,通过handerl把这个对象发送到主线程中,然后调用CallbackMessage对象的callback方法。
Post请求封装
通过以上对HTTPCaller类各个方法的解释,相信你知道了get请求服务器的整个调用流程。现在我们对HTTPCaller类增加post请求的方法以及上传文件的方法。postFile方法增加了ProgressUIListener参数,通过这个接口监听上传文件进度的回调。
public <T> void post(final Class<T> clazz, final String url, Header[] header, List<NameValuePair> params, final RequestDataCallback<T> callback) {
this.post(clazz,url, header, params, callback,true);
}
/**
*
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param params 参数
* @param callback 回调
* @param autoCancel 是否自动取消 true:同一时间请求一个接口多次 只保留最后一个
* @param <T>
*/
public <T> void post(final Class<T> clazz,final String url, Header[] header, final List<NameValuePair> params, final RequestDataCallback<T> callback, boolean autoCancel) {
if (checkAgent()) {
return;
}
add(url,postBuilder(url, header, params, new HTTPCaller.MyHttpResponseHandler(clazz,url,callback)),autoCancel);
}
private Call postBuilder(String url, Header[] header, List<NameValuePair> form, HttpResponseHandler responseCallback) {
try {
if (form == null) {
form = new ArrayList<>(2);
}
form.addAll(httpConfig.getCommonField());//添加公共字段
FormBody.Builder formBuilder = new FormBody.Builder();
for (NameValuePair item : form) {
formBuilder.add(item.getName(), item.getValue());
}
RequestBody requestBody = formBuilder.build();
Request.Builder builder = new Request.Builder();
builder.url(url);
builder.post(requestBody);
return execute(builder, header, responseCallback);
} catch (Exception e) {
if (responseCallback != null)
responseCallback.onFailure(-1, e.getMessage().getBytes());
}
return null;
}
/**
* 上传文件
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param form 请求参数
* @param callback 回调
* @param <T>
*/
public <T> void postFile(final Class<T> clazz, final String url, Header[] header,List<NameValuePair> form,final RequestDataCallback<T> callback) {
postFile(url, header, form, new HTTPCaller.MyHttpResponseHandler(clazz,url,callback),null);
}
/**
* 上传文件
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param form 请求参数
* @param callback 回调
* @param progressUIListener 上传文件进度
* @param <T>
*/
public <T> void postFile(final Class<T> clazz, final String url, Header[] header,List<NameValuePair> form,final RequestDataCallback<T> callback,ProgressUIListener progressUIListener) {
add(url, postFile(url, header, form, new HTTPCaller.MyHttpResponseHandler(clazz,url,callback),progressUIListener));
}
/**
* 上传文件
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param name 名字
* @param fileName 文件名
* @param fileContent 文件内容
* @param callback 回调
* @param <T>
*/
public <T> void postFile(final Class<T> clazz,final String url,Header[] header,String name,String fileName,byte[] fileContent,final RequestDataCallback<T> callback){
postFile(clazz,url,header,name,fileName,fileContent,callback,null);
}
/**
* 上传文件
* @param clazz json对应类的类型
* @param url 请求url
* @param header 请求头
* @param name 名字
* @param fileName 文件名
* @param fileContent 文件内容
* @param callback 回调
* @param progressUIListener 回调上传进度
* @param <T>
*/
public <T> void postFile(Class<T> clazz,final String url,Header[] header,String name,String fileName,byte[] fileContent,final RequestDataCallback<T> callback,ProgressUIListener progressUIListener) {
add(url,postFile(url, header,name,fileName,fileContent,new HTTPCaller.MyHttpResponseHandler(clazz,url,callback),progressUIListener));
}
private Call postFile(String url, Header[] header,List<NameValuePair> form,HttpResponseHandler responseCallback,ProgressUIListener progressUIListener){
try {
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
MediaType mediaType = MediaType.parse("application/octet-stream");
form.addAll(httpConfig.getCommonField());//添加公共字段
for(int i=form.size()-1;i>=0;i--){
NameValuePair item = form.get(i);
if(item.isFile()){//上传文件
File myFile = new File(item.getValue());
if (myFile.exists()){
String fileName = Util.getFileName(item.getValue());
builder.addFormDataPart(item.getName(), fileName,RequestBody.create(mediaType, myFile));
}
}else{
builder.addFormDataPart(item.getName(), item.getValue());
}
}
RequestBody requestBody;
if(progressUIListener==null){//不需要回调进度
requestBody=builder.build();
}else{//需要回调进度
requestBody = ProgressHelper.withProgress(builder.build(),progressUIListener);
}
Request.Builder requestBuider = new Request.Builder();
requestBuider.url(url);
requestBuider.post(requestBody);
return execute(requestBuider, header, responseCallback);
} catch (Exception e) {
e.printStackTrace();
Log.e(httpConfig.getTagName(),e.toString());
if (responseCallback != null)
responseCallback.onFailure(-1, e.getMessage().getBytes());
}
return null;
}
private Call postFile(String url, Header[] header,String name,String filename,byte[] fileContent, HttpResponseHandler responseCallback,ProgressUIListener progressUIListener) {
try {
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
MediaType mediaType = MediaType.parse("application/octet-stream");
builder.addFormDataPart(name,filename,RequestBody.create(mediaType, fileContent));
List<NameValuePair> form = new ArrayList<>(2);
form.addAll(httpConfig.getCommonField());//添加公共字段
for (NameValuePair item : form) {
builder.addFormDataPart(item.getName(),item.getValue());
}
RequestBody requestBody;
if(progressUIListener==null){//不需要回调进度
requestBody=builder.build();
}else{//需要回调进度
requestBody = ProgressHelper.withProgress(builder.build(),progressUIListener);
}
Request.Builder requestBuider = new Request.Builder();
requestBuider.url(url);
requestBuider.post(requestBody);
return execute(requestBuider, header,responseCallback);
} catch (Exception e) {
if (httpConfig.isDebug()) {
e.printStackTrace();
Log.e(httpConfig.getTagName(), e.toString());
}
if (responseCallback != null)
responseCallback.onFailure(-1, e.getMessage().getBytes());
}
return null;
}
增加下载文件的方法
get请求跟post请求都有了,上传文件就是post请求,继续增加downloadFile方法用来下载文件,通用的MyHttpResponseHandler不能处理回调,又增加了DownloadFileResponseHandler类处理下载回调。
public void downloadFile(String url, String saveFilePath, Header[] header, ProgressUIListener progressUIListener) {
downloadFile(url,saveFilePath, header, progressUIListener,true);
}
public void downloadFile(String url,String saveFilePath, Header[] header,ProgressUIListener progressUIListener,boolean autoCancel) {
if (checkAgent()) {
return;
}
add(url,downloadFileSendRequest(url,saveFilePath, header, progressUIListener),autoCancel);
}
private Call downloadFileSendRequest(String url, final String saveFilePath, Header[] header, final ProgressUIListener progressUIListener){
Request.Builder builder = new Request.Builder();
builder.url(url);
builder.get();
return execute(builder, header, new HTTPCaller.DownloadFileResponseHandler(url,saveFilePath,progressUIListener));
}
public class DownloadFileResponseHandler implements Callback {
private String saveFilePath;
private ProgressUIListener progressUIListener;
private String url;
public DownloadFileResponseHandler(String url,String saveFilePath,ProgressUIListener progressUIListener){
this.url=url;
this.saveFilePath=saveFilePath;
this.progressUIListener=progressUIListener;
}
@Override
public void onFailure(Call call, IOException e) {
clear(url);
try {
printLog(url + " " + -1 + " " + new String(e.getMessage().getBytes(), "utf-8"));
} catch (UnsupportedEncodingException encodingException) {
encodingException.printStackTrace();
}
}
@Override
public void onResponse(Call call, Response response) throws IOException {
printLog(url + " code:" + response.code());
clear(url);
ResponseBody responseBody = ProgressHelper.withProgress(response.body(),progressUIListener);
BufferedSource source = responseBody.source();
File outFile = new File(saveFilePath);
outFile.delete();
outFile.createNewFile();
BufferedSink sink = Okio.buffer(Okio.sink(outFile));
source.readAll(sink);
sink.flush();
source.close();
}
}
RequestDataCallback接口,封装了三个方法,自己想要什么数据就去重写对应的方法。
public abstract class RequestDataCallback<T> {
//返回json对象
public void dataCallback(T obj) {
}
//返回http状态和json对象
public void dataCallback(int status, T obj) {
dataCallback(obj);
}
//返回http状态、json对象和http原始数据
public void dataCallback(int status, T obj, byte[] body) {
dataCallback(status, obj);
}
}
还有Util公共类,有三个静态方法。
public class Util {
/**
* 获取文件名称
* @param filename
* @return
*/
public static String getFileName(String filename){
int start=filename.lastIndexOf("/");
// int end=filename.lastIndexOf(".");
if(start!=-1){
return filename.substring(start+1,filename.length());
}else{
return null;
}
}
/**
* 拼接公共参数
* @param url
* @param commonField
* @return
*/
public static String getMosaicParameter(String url, List<NameValuePair> commonField){
if (TextUtils.isEmpty(url))
return "";
if (url.contains("?")) {
url = url + "&";
} else {
url = url + "?";
}
url += getCommonFieldString(commonField);
return url;
}
private static String getCommonFieldString(List<NameValuePair> commonField){
StringBuffer sb = new StringBuffer();
try{
int i=0;
for (NameValuePair item:commonField) {
if(i>0){
sb.append("&");
}
sb.append(item.getName());
sb.append('=');
sb.append(URLEncoder.encode(item.getValue(),"utf-8"));
i++;
}
}catch (Exception e){
}
return sb.toString();
}
}