前5篇博文完成了此框架的一大模块—–多线程下载,而这两篇文章实现另一大模块——Http基本框架封装,在上一篇博文中完成了HttpHeader的接口定义和实现、状态码定义及response、request接口封装和实现,定义了许多接口和抽象类,在接下来编码过程中会体现出程序的扩展性重要性。
在此篇博文中将添加新功能——原生请求的类库支持,你会发现在此基础上只需增加3个类即可,充分体现出了程序的扩展性。新增功能如下:
- 原生HttpUrlConnction请求和响应
- 业务层多线程分发处理
- 移除请求
- 请求成功类型转换包装处理
(建议阅读此篇文章之前,需理解前两篇文章的讲解,此系列文章是环环相扣,不可缺一,链接如下:)
优雅设计封装基于Okhttp3的网络框架(一):Http网络协议与Okhttp3解析
优雅设计封装基于Okhttp3的网络框架(二):多线程下载功能原理设计 及 简单实现
优雅设计封装基于Okhttp3的网络框架(三):多线程下载功能核心实现 及 线程池、队列机制解析
优雅设计封装基于Okhttp3的网络框架(四):多线程下载添加数据库支持(greenDao)及 进度更新
优雅设计封装基于Okhttp3的网络框架(五):多线程、单例模式优化 及 volatile、构建者模式使用解析
优雅设计封装基于Okhttp3的网络框架(六):HttpHeader接口设计实现 及 Response、Request封装实现
一. 原生HttpConnection方式请求和响应
以下的封装是为了增强此网络框架的功能扩展性,在除了使用Okhttp方式请求外,在此基础上增加最少的类使网络框架可以支持别的类库请求,例如原生的UrlConnction请求。此时前期所封装的接口扩展性就显得很重要了,所以在上一篇博文中我们定义了大量的接口与抽象类,看似复杂冗余,其实都是在为代码扩展性考虑,而此点中将完成原生请求的封装。
1. OriginHttpRequest 原生请求实现类
此类与OkHttpRequest 类相似,都继承于BufferHttpRequest 接口,区别在于一个是原生(HttpURLConnection对象)请求实现类,一个是Okhttp(OkhttpClient对象)请求实现类。所以两者大体实现类似,只是底层执行对象、操作API不同(区别主要体现在executeInternal
方法实现上)。组成如下:
- 定义成员变量HttpURLConnection及参数HttpMethod、Url来实现Okhttp的请求过程。
- 提供构造方法初始化以上3个成员变量。
- 实现抽象方法
getMethod()
、getUri()
。(这两个抽象方法实现简单,只需返回成员变量即可) - 实现抽象方法
executeInternal(HttpHeader header, byte[] data)
:
- 对header进行处理,循环该参数将所有请求头封装至HttpURLConnection
- 判断data即传输参数是否为空,写入到HttpURLConnection输出流中。
- 最后封装完毕,创建原生的响应实现类OriginHttpResponse,将HttpURLConnection传入其构造方法,最后将原生响应实现类OriginHttpResponse返回出去即可。
/**
* @function OriginHttpRequest 原生请求实现类(继承BufferHttpRequest接口)
* @author lemon guo
*/
public class OriginHttpRequest extends BufferHttpRequest {
private HttpURLConnection mConnection;
private String mUrl;
private HttpMethod mMethod;
public OriginHttpRequest(HttpURLConnection connection, HttpMethod method, String url) {
this.mConnection = connection;
this.mUrl = url;
this.mMethod = method;
}
@Override
protected HttpResponse executeInternal(HttpHeader header, byte[] data) throws IOException {
for (Map.Entry<String, String> entry : header.entrySet()) {
mConnection.addRequestProperty(entry.getKey(), entry.getValue());
}
mConnection.setDoOutput(true);
mConnection.setDoInput(true);
mConnection.setRequestMethod(mMethod.name());
mConnection.connect();
if (data != null && data.length > 0) {
OutputStream out = mConnection.getOutputStream();
out.write(data,0,data.length);
out.close();
}
OriginHttpResponse response = new OriginHttpResponse(mConnection);
return response;
}
@Override
public HttpMethod getMethod() {
return mMethod;
}
@Override
public URI getUri() {
return URI.create(mUrl);
}
}
2. 原生工厂类 OriginHttpRequestFactory
与OkHttpRequest后续设计实现类似,需要在实现类的基础上对HttpRequest对象进行封装,提供方法供上层接口调用,那具体的网络请求是调用HttpURLConnection 来完成。
- 定义成员变量HttpURLConnection
- 为此类提供构造方法初始化成员变量
- 实现接口中的
createHttpRequest
方法,即创建OriginHttpRequest 对象并返回。 - 再提供一些基本方法
setConnectionTimeOut
设置请求超时时间,setReadTimeOut
、setWriteTimeOut
设置读写时间。(若有其他需求,此处可继续增加)
/**
* @function 实现类 OriginHttpRequestFactory(返回HttpRequest对象)
* @author lemon guo
*/
public class OriginHttpRequestFactory implements HttpRequestFactory {
private HttpURLConnection mConnection;
public OriginHttpRequestFactory() {
}
public void setReadTimeOut(int readTimeOut) {
mConnection.setReadTimeout(readTimeOut);
}
public void setConnectionTimeOut(int connectionTimeOut) {
mConnection.setConnectTimeout(connectionTimeOut);
}
@Override
public HttpRequest createHttpRequest(URI uri, HttpMethod method) throws IOException {
mConnection = (HttpURLConnection) uri.toURL().openConnection();
return new OriginHttpRequest(mConnection, method, uri.toString());
}
}
3. 原生响应实现类 OriginHttpResponse
相对应的,同Okhttp中的响应实现类OkHttpResponse类似,继承抽象类AbstractHttpResponse,实现父类的方法:
- 实现类内部定义重要成员变量:HttpURLConnection 。
- 为实现类提供构造方法,参数为HttpURLConnection 。
- 实现类内部待实现的方法具体编码都依赖于HttpURLConnection成员变量。
代码量虽然不少,主要是实现方法,但是编码简单,查看即可理解,代码如下:
/**
* @function 响应实现类 OriginHttpResponse
* @author lemon Guo
*/
public class OriginHttpResponse extends AbstractHttpResponse {
private HttpURLConnection mConnection;
public OriginHttpResponse(HttpURLConnection connection) {
this.mConnection = connection;
}
@Override
public HttpStatus getStatus() {
try {
return HttpStatus.getValue(mConnection.getResponseCode());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public String getStatusMsg() {
try {
return mConnection.getResponseMessage();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public long getContentLength() {
return mConnection.getContentLength();
}
@Override
protected InputStream getBodyInternal() throws IOException {
return mConnection.getInputStream();
}
@Override
protected void closeInternal() {
mConnection.disconnect();
}
@Override
public HttpHeader getHeaders() {
HttpHeader header = new HttpHeader();
for (Map.Entry<String, List<String>> entry : mConnection.getHeaderFields().entrySet()) {
header.set(entry.getKey(), entry.getValue().get(0));
}
return header;
}
}
4. 供开发者调用类HttpRequestProvider ☆☆☆☆☆
以上原生请求方式封装完毕后,可以发现总共新增了**OriginHttpRequest、OriginHttpRequestFactory、OriginHttpResponse**3个类而已,这说明此网络框架代码的扩展性还是可行的,在后续想要添加别的请求类库,只要新增此3种代码即可。
在新增完代码后,最后需要在HttpRequestProvider进行判断调用,这是一个供开发者调用的类
public HttpRequestProvider() {
if (OKHTTP_REQUEST) {
mHttpRequestFactory = new OkHttpRequestFactory();
} else {
mHttpRequestFactory = new OriginHttpRequestFactory();
}
}
在其构造方法中进行判断更改,可以直接改变网络请求所使用的依赖类库!
二. 业务层多线程分发处理
上一点已经完成此网络框架对原生UrlConnction请求的支持,但是还有一个重点没有完成——多线程处理请求,大家都知道在主线程进行网络请求会出现异常,此点就是为了完成异步请求。
1. 队列中的请求对象 MultiThreadRequest
在请求队列中需要定义业务层的相关接口,用于上层开发人员调用,上层只关注请求成功success
还是失败fail
,对于底层具体试下并不关心。
在代码实现之前再次强调此网络框架中“队列”的概念,因为在处理多线程请求时,不可能无限制的创建多个线程来处理,而一个队列中存储的是一个Request对象,存储着请求Url、请求方式、数据等相关信息,再提供对应的get
、set
方法。
/**
* @funtion 业务层多线程分发处理,队列中的Request对象MultiThreadRequest
* @author lemon Guo
*/
public class MultiThreadRequest {
private String mUrl;
private HttpMethod mMethod;
private byte[] mData;
private MultiThreadResponse mResponse;
private String mContentType;
public String getUrl() {
return mUrl;
}
public void setUrl(String url) {
mUrl = url;
}
//相对应的get/set方法
......
}
2. 响应对象 MultiThreadRequest
根据上一点所讲,上层只关心请求结果成功还是失败,所以响应接口只有以下两个方法。
/**
* @funtion 响应抽象类MultiThreadResponse
* @author lemon Guo
*/
public abstract class MultiThreadResponse<T> {
public abstract void success(MultiThreadRequest request, T data);
public abstract void fail(int errorCode, String errorMsg);
}
3. 工作站WorkStation
接下来需要一个类来处理多线程中的请求,属于服务的一种,用于处理多线程的控制和队列的管理。
- 内部维护一个线程池成员变量,这里为了能够快速响应多个线程的同时请求数据,将线程数量最大值设置为
Integer.MAX_VALUE
。再引入两个队列,一个队列存储着请求request,另一个存储着cache,即待执行的请求request队列(考虑到处理线程数量超过最大限制时)。 - 提供构造方法初始化成员变量HttpRequestProvider,经过前期封装后,获取请求request对象由专门供上层调用的类HttpRequestProvider完成。
- 提供
add
方法将请求任务添加到队列中。注意在这里需要做一个开启线程最大数判断,例如最多同时开启60个线程处理请求:
- 若超过则将request添加cache队列中,等待执行。
- 若未超过,则通过HttpRequestProvider获取请求request对象,最后由线程池执行。注意,既然是由线程池执行,这里还需要一个Runnable,后续编写。
-提供finish
方法,在线程池执行Runnable时,即请求结束会调用此方法,将完成的Request移除队列。
/**
* @funtion 业务层多线程分发处理:用于处理多线程的控制和队列的管理 MultiThreadRequest
* @author lemon Guo
*/
public class WorkStation {
private static final int MAX_REQUEST_SIZE = 60;
private static final ThreadPoolExecutor sThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
private AtomicInteger index = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("http thread name is " + index.getAndIncrement());
return thread;
}
});
private Deque<MultiThreadRequest> mRunning = new ArrayDeque<>();
private Deque<MultiThreadRequest> mCache = new ArrayDeque<>();
private HttpRequestProvider mRequestProvider;
public WorkStation() {
mRequestProvider = new HttpRequestProvider();
}
public void add(MultiThreadRequest request) {
if (mRunning.size() > MAX_REQUEST_SIZE) {
mCache.add(request);
} else {
doHttpRequest(request);
}
}
public void doHttpRequest(MultiThreadRequest request) {
HttpRequest httpRequest = null;
try {
httpRequest = mRequestProvider.getHttpRequest(URI.create(request.getUrl()), request.getMethod());
} catch (IOException e) {
e.printStackTrace();
}
sThreadPool.execute(new HttpRunnable(httpRequest, request, this));
}
public void finish(MultiThreadRequest request) {
mRunning.remove(request);
if (mRunning.size() > MAX_REQUEST_SIZE) {
return;
}
if (mCache.size() == 0) {
return;
}
Iterator<MultiThreadRequest> iterator = mCache.iterator();
while (iterator.hasNext()) {
MultiThreadRequest next = iterator.next();
mRunning.add(next);
iterator.remove();
doHttpRequest(next);
}
}
}
4. HttpRunnable
在专门用于处理多线程的控制和队列的管理类WorkStation中维护了一个线程池,用来执行网络请求,所以需要对应的Runnable来执行下载任务。
- 成员变量有基本Http封装的接口HttpRequest、多线程请求的接口MultiThreadRequest、管理多线程和队列类WorkStation。
- WorkStation主要用于
run
方法执行完后调用此对象中的方法,来移除队列中已执行完的request。 - 将HttpRequest中的重要请求数据获取并封装到MultiThreadRequest中来执行请求。
- WorkStation主要用于
/**
* @funtion 业务层多线程分发处理:用于处理下载任务
* @author lemon Guo
*/
public class HttpRunnable implements Runnable {
private HttpRequest mHttpRequest;
private MultiThreadRequest mRequest;
private WorkStation mWorkStation;
public HttpRunnable(HttpRequest httpRequest, MultiThreadRequest request, WorkStation workStation) {
this.mHttpRequest = httpRequest;
this.mRequest = request;
this.mWorkStation = workStation;
}
@Override
public void run() {
try {
mHttpRequest.getBody().write(mRequest.getData());
HttpResponse response = mHttpRequest.execute();
String contentType = response.getHeaders().getContentType();
mRequest.setContentType(contentType);
if (response.getStatus().isSuccess()) {
if (mRequest.getResponse() != null) {
mRequest.getResponse().success(mRequest, new String(getData(response)));
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
mWorkStation.finish(mRequest);
}
}
public byte[] getData(HttpResponse response) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream((int) response.getContentLength());
int len;
byte[] data = new byte[512];
try {
while ((len = response.getBody().read(data)) != -1) {
outputStream.write(data, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
return outputStream.toByteArray();
}
}
5. HttpApiProvider上层调用API
以上代码基本完成,但是为了方便上层调用,需要在此基础上封装一个接口的API,类似之前专门提供Request对象的HttpRequestProvider,此类名为HttpApiProvider
public class HttpApiProvider {
private static final String ENCODING = "utf-8";
private static WorkStation sWorkStation = new WorkStation();
/*
* 对请求参数进行编码处理
* */
public static byte[] encodeParam(Map<String, String> value) {
if (value == null || value.size() == 0) {
return null;
}
StringBuffer buffer = new StringBuffer();
int count = 0;
try {
for (Map.Entry<String, String> entry : value.entrySet()) {
buffer.append(URLEncoder.encode(entry.getKey(), ENCODING)).append("=").
append(URLEncoder.encode(entry.getValue(), ENCODING));
if (count != value.size() - 1) {
buffer.append("&");
}
count++;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return buffer.toString().getBytes();
}
public static void helloWorld(String ul, Map<String, String> value, MultiThreadResponse response) {
MultiThreadRequest request = new MultiThreadRequest();
request.setUrl(ul);
request.setMethod(HttpMethod.POST);
request.setData(encodeParam(value));
request.setResponse(response);
sWorkStation.add(request);
}
}
6. 测试
以上封装功能已经完成,接下来以一个POST请求来测试这一系列HTTP封装请求,代码如下:
Map<String, String> map = new HashMap<>();
map.put("username", "nate");
map.put("userage", "12");
MoocApiProvider.helloWorld("http:....../web/HelloServlet", map, new MoocResponse<Person>() {
@Override
public void success(MoocRequest request, Person data) {
Logger.debug("nate", data.toString());
}
@Override
public void fail(int errorCode, String errorMsg) {
}
});
结果显示:
可以看出日志打印,代表POST请求成功,以上代码封装无误。
三. 数据类型自动转换
现在还剩下一个需求需要完善,即响应请求到的数据更加简单直接,是对象类型而并非xml、json数据,所以这涉及到了数据类型转换,为了整个程序的扩展性考虑,首要的还是来封装接口。
1. 数据类型转换接口Convert
parse
方法中进行类型转换。isCanParse
方法判断此数据是否可以进行转换
/**
* @funtion 数据类型转换接口
* @author lemon Guo
*/
public interface Convert {
Object parse(HttpResponse response, Type type) throws IOException;
boolean isCanParse(String contentType);
Object parse(String content, Type type) throws IOException;
}
2. 转换实现类JsonConvert
如上在实现接口之后,可以定义不同类型转换的实现类来实现此接口,此项目中我只定义了JsonConvert,用来进行Json数据转换,相应的,还可以定义XmlConvert实现类等等。
这里方法的具体实现都是借助开源库Gson来解析数据,比较常用的方法,实现不难,代码如下:
/**
* @funtion 转换实现类JsonConvert
* @author lemon Guo
*/
public class JsonConvert implements Convert {
private Gson gson = new Gson();
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
@Override
public Object parse(HttpResponse response, Type type) throws IOException {
Reader reader = new InputStreamReader(response.getBody());
return gson.fromJson(reader, type);
}
@Override
public boolean isCanParse(String contentType) {
return CONTENT_TYPE.equals(contentType);
}
@Override
public Object parse(String content, Type type) throws IOException {
return gson.fromJson(content, type);
}
}
3. 解析Response数据WrapperResponse
以上封装好类型转换后,需要将此结合到网络请求中,在对MultiThreadResponse做一个上层封装,相当于一层过滤,将获取到的响应数据通过类型转换后再返回。
WrapperResponse 继承于MultiThreadResponse,实现其抽象方法success
,在成功响应方法中对数据进行解析类型转换操作。
/**
* @funtion WrapperResponse类型转换封装 Response
* @author lemon Guo
*/
public class WrapperResponse extends MultiThreadResponse<String> {
private MultiThreadResponse mMoocResponse;
private List<Convert> mConvert;
public WrapperResponse(MultiThreadResponse moocResponse, List<Convert> converts) {
this.mMoocResponse = moocResponse;
this.mConvert = converts;
}
@Override
public void success(MultiThreadRequest request, String data) {
for (Convert convert : mConvert) {
if (convert.isCanParse(request.getContentType())) {
try {
Object object = convert.parse(data, getType());
mMoocResponse.success(request, object);
} catch (IOException e) {
e.printStackTrace();
}
return;
}
}
}
public Type getType() {
Type type = mMoocResponse.getClass().getGenericSuperclass();
Type[] paramType = ((ParameterizedType) type).getActualTypeArguments();
return paramType[0];
}
@Override
public void fail(int errorCode, String errorMsg) {
}
}
4. 修改API调用类HttpApiProvider
public static void helloWorld(String ul, Map<String, String> value, MultiThreadResponse response) {
MultiThreadRequest request = new MultiThreadRequest();
request.setUrl(ul);
request.setMethod(HttpMethod.POST);
request.setData(encodeParam(value));
request.setResponse(response);
sWorkStation.add(request);
}
如上,这是未解析响应数据时Api暴露网络请求接口中的实现,其中使用的响应数据是MultiThreadResponse ,在我们封装好可自动解析的数据后,修改使用WrapperResponse ,代码如下:
public static void helloWorld(String ul, Map<String, String> value, MultiThreadResponse response) {
MultiThreadRequest request = new MultiThreadRequest();
WrapperResponse wrapperResponse = new WrapperResponse(response, sConverts);
request.setUrl(ul);
request.setMethod(HttpMethod.POST);
request.setData(encodeParam(value));
// request.setResponse(response);
request.setResponse(wrapperResponse);
sWorkStation.add(request);
}
再次测试,结果正确,以上类型转换封装无误,此网络框架封装完成。
四. 网络框架总结
此系列文章旨于:基于okhttp3原始框架来设计封装一个满足业务需求、扩展性强、耦合度低的网络框架。具体框架功能为:
- 封装基本的网络请求
- 扩展其对数据库的支持
- 对多文件上传、多线程文件下载的支持
- 对Json数据解析等功能的支持
1.整体代码
以上是这个网络框架EasyOkhttp封装的全部代码,看似代码量并不少,但是其中定义了大量的接口和抽象类,注重扩展性和解耦性,所以读者可在我封装的基础上继续拓展,根据自身需求添加代码。
2. 架构设计
如上图所示,此框架可以分为三个层次:
第一层:便于框架扩展,第一层即最底层是Http Interface和Abstact,例如Http中的Headers、Request、Response等通用的原生接口。
第二层:有了第一层请求接口定义,便于第二层对接口的实现,此框架采用两种方式对接口进行实现,分别是Okhttp和原生的HttpURLConnection。通过这两个相关的API去实现整个Http请求和响应的过程,若还想要做相应的拓展,采用别的第三方http请求库,在此处可增加。(已经预先在第一层定义了足够多的接口实现网络请求的回调,第一层可无需修改)对于整个上层业务来说,无需直接接触到底层Okhttp、HttpURLConnection具体实现,所以提供二次封装的 HttpProvider ,暴露接口给上层调用。(具体底层是调用Http还是HttpURLConnection取决于配置,首先判断Okhttp依赖在项目中是否存在,若有则主要采用Okhttp来进行网络请求,否则采用HttpURLConnection)
第三层:即最上层由 Workstation 和Convert组成。Workstation 的中文意思是工作站,用来处理一些线程的调度分发和任务的队列,之所以将它设计在最上层,因为整个多线程、队列机制是与业务层紧密相关的。Convert是为上层开发者提供了更好的接口封装,用于接口返回类型转换、数据解析,例如json、xml等。
3. 文件多线程下载和Http设计封装
整个系列的文章可以分成两个部分,即前5篇博文在重点讲解多线程下载有关设计与编码实现,而后两篇博文则是重点讲解Http请求、响应接口封装,两部分的思维导图如下,讲解顺序也是按此进行:
文件多线程下载思维导图:
Http设计封装思维导图:
4. 小结
此系列所完成的网络框架封装编码工作暂告一段落,有些功能可能完成的不是很全面,编写此系列过程中收益最大的应当是整体规划封装思想。多线程下载模块多涉及的是Java线程、Http字段有关知识,而后半部分——Http网络框架实现过程中充分体现出了接口、抽象类、实现类这之间的封装思想,而大量的接口和抽象类也体现出整个程序的扩展性、解耦性,这两点从一开始封装网络框架就被视为重点,同时也是我们需要学习的。
此框架可能只算一个简单封装demo,些许部分完成的不是很好,但是这整个封装过程便是精华所在,从一开始的框架架构设计,到功能设计实现、编码优化、bug程序调试等等。这不仅仅只是编码,只涉及到Java单一的内容,同时融合了 Okhttp相关内容、Http协议、接口设计、代码隔离、架构设计、解决思路等综合考虑,此乃重中之重。
编程,或不只是编程。
共勉~
若有错误,虚心指教~