优雅设计封装基于Okhttp3的网络框架(完):原生HttpUrlConnction请求、多线程分发 及 数据转换

来源:互联网 发布:软件模块化设计原则 编辑:程序博客网 时间:2024/05/26 17:45

前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及参数HttpMethodUrl来实现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设置请求超时时间,setReadTimeOutsetWriteTimeOut设置读写时间。(若有其他需求,此处可继续增加)
/** * @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、请求方式、数据等相关信息,再提供对应的getset方法。

/** * @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中来执行请求。
/** * @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 InterfaceAbstact,例如Http中的Headers、Request、Response等通用的原生接口。

  • 第二层:有了第一层请求接口定义,便于第二层对接口的实现,此框架采用两种方式对接口进行实现,分别是Okhttp和原生的HttpURLConnection。通过这两个相关的API去实现整个Http请求和响应的过程,若还想要做相应的拓展,采用别的第三方http请求库,在此处可增加。(已经预先在第一层定义了足够多的接口实现网络请求的回调,第一层可无需修改)对于整个上层业务来说,无需直接接触到底层Okhttp、HttpURLConnection具体实现,所以提供二次封装的 HttpProvider ,暴露接口给上层调用。(具体底层是调用Http还是HttpURLConnection取决于配置,首先判断Okhttp依赖在项目中是否存在,若有则主要采用Okhttp来进行网络请求,否则采用HttpURLConnection)

  • 第三层:即最上层由 WorkstationConvert组成。Workstation 的中文意思是工作站,用来处理一些线程的调度分发和任务的队列,之所以将它设计在最上层,因为整个多线程、队列机制是与业务层紧密相关的。Convert是为上层开发者提供了更好的接口封装,用于接口返回类型转换、数据解析,例如json、xml等。


3. 文件多线程下载和Http设计封装

整个系列的文章可以分成两个部分,即前5篇博文在重点讲解多线程下载有关设计与编码实现,而后两篇博文则是重点讲解Http请求、响应接口封装,两部分的思维导图如下,讲解顺序也是按此进行:

文件多线程下载思维导图:

这里写图片描述


Http设计封装思维导图:

这里写图片描述


4. 小结

源码

此系列所完成的网络框架封装编码工作暂告一段落,有些功能可能完成的不是很全面,编写此系列过程中收益最大的应当是整体规划封装思想。多线程下载模块多涉及的是Java线程、Http字段有关知识,而后半部分——Http网络框架实现过程中充分体现出了接口、抽象类、实现类这之间的封装思想,而大量的接口和抽象类也体现出整个程序的扩展性、解耦性,这两点从一开始封装网络框架就被视为重点,同时也是我们需要学习的。

此框架可能只算一个简单封装demo,些许部分完成的不是很好,但是这整个封装过程便是精华所在,从一开始的框架架构设计,到功能设计实现、编码优化、bug程序调试等等。这不仅仅只是编码,只涉及到Java单一的内容,同时融合了 Okhttp相关内容、Http协议、接口设计、代码隔离、架构设计、解决思路等综合考虑,此乃重中之重。

编程,或不只是编程。

共勉~



若有错误,虚心指教~

阅读全文
0 0