Java 基础(二十)网络框架设计 MyVolley (上)

1,148 阅读12分钟

上周答应大家的,手撸一个网络请求框架。
学了快两个月的 java 基础,现在我们来手撸一个网络请求框架练练手。
手写一个网络请求框架需要掌握的知识点比较多,其中牵涉到设计模式、集合、泛型、多线程及并发、网络编程等知识,算是对 java 基本功比较全面的考查,同时,对架构能力也有一定的要求。

需求

先来看看需求~~

  • 支持请求 JSON 文本类型学,音频,图片类型,批量下载。上传~
  • 请求各种数据时,调用层不用关心上传参数的封装
  • 获取数据后,调用层不用关心 JSON 数据的解析
  • 回调时,调用层只需要知道传入的 JSON 的对应响应类
  • 回调响应结果发生在主线程(线程切换)
  • 对下载,上传扩展
  • 支持高并发请求,请求队列一次获取,可以设置最大并发数,设置先请求先执行

架构设计

首先我们来回顾一下一次网络请求的流程。

1.准备请求
2.根据请求的参数发起网络请求
3.请求结果处理及回调

然后我们调用的代码大概长这个样子~

Volley.sendRequest(null, url, GankResponse.class, GET, new IDataListener<GankResponse>() {
        @Override
        public void onSuccess(GankResponse response) {}

        @Override
        public void onFail(int errorCode, String errorMsg) {}
});

哈哈,我凭空想象的,其实我并没有用过 Volley。

嗯,我们最终的目的也是把代码写成这样子。

刚刚我们说了一次网络请求大概是三个步骤,接下来,我们就根据这三个步骤来设计网络架构。敲黑板,记住了这三个阶段。

阶段一:准备请求

1.准备参数

这个阶段一般是从 Activity 界面发起的,说白一点好像就是我客户端页面不知道显示什么样的数据,去找服务器要。一次网络请求实际上就是和服务区的一次交互,在交互过程中,根据 HTTP 协议(之前讲过,弄不明白的回去翻翻我的文章),我们需要准备以下几个参数

  • url 网址
  • requestParams 请求参数
  • 请求方法(GET、POST 等)
  • responseBean 接收响应的 bean
  • 请求结果回调给 activity (IDataListener①)

其中前三个参数是用来做网络请求的,后两个参数是用来做请求响应的。

因此,我们这里需要一个一个 bean (RequestHolder ②)来封装请求参数RequestHolder需要如下属性

  • requestParams
  • url
  • 请求方法类型(GET、POST)

然后我们需要一个类(IHttpService ③)去发起这个网络请求,这个类最少需要以下几个属性

  • RequestHolder 实例
  • excute 方法执行网络请求
  • IHttpListener④(持有请求结果处理类实例)

    至于上面说的这个IHttpListener,属于阶段三,我们来看看IHttpListener。

阶段三:请求结果处理及回调 IHttpListener

我们先跳过阶段二。

上面我们说了,IHttpService持有IHttpListener,并且在请求结束之后要处理并回调。
请求结果无非就是两种,成功和不成功。
所以这个类只需要有两种行为(方法)和一个属性

  • onSuccess
  • onFail
  • 持有 IDataListener的实例
  • 解析数据

  • onSuccess
    IHttpService 在网络请求成功之后调用这个方法,IHttpListener 需要解析数据(比如说 json 传、InputStream 转换成 image、File 等)然后调用 IDataListener 返回给 activity

  • onFail
    IHttpService 网络请求失败调用这个方法,IDataListener 直接调用 IDataListener 将错误信息返回给 activity

  • 解析数据
    这一步是发生在请求成功之后得到源数据,然后根据源数据的不同类型做不同的解析,最后调用IHttpListener 返回给 activity。

阶段二:根据请求的参数发起网络请求

根据我们刚才的设计,网络请求的真正发起是IHttpService 的 excuse 方法。然后网络请求需要在子线程,我们可以把一次网络请求看成是一个异步任务,所以我们需要一个类去实现 Runnable 接口,然后在 run 方法里去调用 IHttpService 的excuse 执行网络请求。暂且我们把这个实现了 Runnable 接口的类叫 HttpTask⑤,这个类比较简单,只需要持有 IHttpService 的实例即可。

  • IHttpService 实例

然后针对一个异步任务 HttpTask,我们要做一个并发管理,所以需要引入线程池来管理所有的 HttpTask。所以我们需要一个单例的 ThreadPoolManger⑥ 来管理所有的 HttpTask,ThreadPoolManger 里面我们可以直接用 Executors 创建一个线程池,至于线程池的细节,限制最大并发、阻塞、生产消费者模型是怎么实现的,可以回过头去java 技术线程相关的内容。

ThreadPoolManger

  • ExecutorService 实例

Volley 类

好,到这里,我们的网络请求需要的东西差不多就准备好了,现在我们可以在 Activity 里面创建RequestHolder、IHttpService、IHttpListener、HttpTask等类发起网络请求了,但是好像要创建的东西比较多,而且很多东西不是调用者需要关心的东西,容易出错。对于客户端来说,我只需要根据服务器的要求,准备请求参数以及相应参数的类型去接收服务器的响应即可,而不需要关心请求的过程。
所以我们在这里创建一个 Volley⑦类,去封装请求、响应相关的各种类,然后将一个 HttpTask 丢到线程池里面去。这个类很简单,只需要一个静态的 sendRequest 方法即可,方法参数需要传入 url、requestParams等。

  • sendRequest

小结

逻辑有点乱,我来捋一下逻辑。首先是从 Activity 开始,有一个和服务器交互的需求。步骤如下:

1.调用 Volley 的静态方法sendRequest,传入方法需要的 url、RequestParams 等各种参数。
2.Volley 的sendRequest 方法根据方法参数,构建RequestHolder、IHttpService、IHttpListener、HttpTask等类,并将HttpTask 丢进ThreadPoolManger的线程池里面等待执行。
3.ThreadPoolManger的线程池里面实际上是一个阻塞队列,会根据任务取出HttpTask这个任务并执行。
4.HttpTask的 run 方法被调用,run 方法调用IHttpService 的 excuse 方法,正式发起网络请求。
5.网络请求结束,IHttpService 调用IHttpListener 的 成功/失败 的方法进行相应的处理。
6.网络请求成功,IHttpListener 解析 源数据,并将解析的数据通过 IDataListener回调给 activity。
7.网络请求失败,IHttpListener 直接调用IDataListener的 fail 方法告知 activity 请求失败。

哦,对了,线程切换忘记讲,大家思考一下,应该在哪个类切回主线程?答案我会在代码中体现。

架构设计大概就是这样子,为了便于大家理解,我画了一个类关系图,大家凑合着看看。

填坑

之前给大家吹了牛逼,说会实现哪些需求,用到哪些知识点,用到哪些设计模式,在撸代码之前,我先给大家讲解一下。

  • 支持请求 JSON 文本类型,音频,图片类型,批量下载、上传~
    支持请求 JSON 类、图片、音频等资源的下载,这个做了扩展预留。我们只需要根据请求的数据类型,做不同的实现即可,不同的实现都需要继承 IHttpService 接口。

  • 请求各种数据时,调用层不用关心上传参数的封装
    直接调用public static IHttpService sendRequest(requestParams,url,responseClass,type,listener)方法即可。

  • 获取数据后,调用层不用关心 JSON 数据的解析
    数据的解析同IHttpService,同样只需要根据不同的数据类型,做不同的 IHttpListener 实现即可。

  • 回调时,调用层只需要知道传入的 JSON 的对应响应类
    使用了泛型,会在IHttpListener 的 Json 数据实现类里面将 json 数据转换成响应类的 bean。

  • 回调响应结果发生在主线程(线程切换)
    已实现,在IHttpListener 的实现类里面。

  • 对下载,上传扩展
    预留了接口,同样只需要对IHttpService 和 IHttpListener接口进行扩展,下一篇文章会讲解实现。

  • 支持高并发请求,请求队列一次获取,可以设置最大并发数,设置先请求先执行
    通过线程池实现。

会用到的知识点
  • 泛型
    请求参数、回调参数都是泛型实现
  • 请求队列
    线程池里面做了封装,这里直接调用concurrent 包里面的工具类 Executors创建了线程池,里面用到了 LinkedBlockingQueue 。

  • 阻塞队列
    LinkedBlockingQueue 就是阻塞队列,具体参考 concurrent 工具包的讲解。

  • 线程拒绝策略
    待实现

用到的设计模式
  • 模板方法模式

    模板方法:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
    我的HttpTask 里面的网络请求痛的都是 IHttpService 类,根据请求的不同数据,使用不同的子类,同理还有 IHttpListener 。

  • 单例模式
    ThreadPoolManger 是单例

  • 策略模式
    根据不同的请求数据,选择使用IHttpService JSonHTTPService 或者FileDownHttpService(这个类在文件下载模块中)

  • 生产者消费者模式
    线程池本身就是一个生产者消费者模型,Activity 发生一个请求,Volley 将请求生产成一个 HttpTask 丢进线程池,然后请求结束就相当于消费了这个请求。

撸代码

架构设计出来了,类关系图也设计好了,接下来我们就开始撸代码了。

从哪里开始撸呢? 你可以选择按照逻辑顺序,从 activity 里面调用 Volley 开始撸,差什么类就撸什么类,遇神杀神遇佛杀佛。
然而我不推荐这种方法,这种方法容易出错,效率低。

首先,我们先来看看,上面分析的时候,我标记的几个重点类对象

①IDataListener
②RequestHolder
③IHttpService
④IHttpListener
⑤HttpTask
⑥ThreadManger
⑦Volley

接下来,我们就来按照这个顺序撸代码吧。

IDataListener

这个类很简单,只需要做一个请求结果回调。

public interface IDataListener<T> {
    /**
     * @param t 响应参数
     */
    void onSuccess(T t);

    void onFail(int errorCode, String errorMsg);
}

其中 T 是泛型操作,因为我们也不知道返回结果会是什么类型的数据结构。

RequestHolder

现在直接撸这个类,我们会遇到问题,因为RequestHolder会持有 IHttpService 和 IHttpListener 的引用,所以我们应该先把两个接口撸出来。

这个类很简单,只需要持有IHttpService、IHttpListener、URL、requestParams、RequestHolder 等参数就行了,没有任何逻辑操作。

注意:这里只需要持有IHttpService、和IHttpListener的接口引用,不要去持有这两个接口的实例引用。

IHttpService

由于 IHttpService 被 RequestHolder 持有且是真正的网络请求相关的类构造类以及执行类,所以我们需要定义设置一下接口方法。

public interface IHttpService {

    /**
     * 设置 url
     *
     * @param url url address
     */
    void setUrl(String url);

    /**
     * 设置处理接口
     *
     * @param listener 处理接口
     */
    void setHttpListener(IHttpListener listener);

    /**
     * 设置请求参数
     *
     * @param data 请求参数 byte 数组
     */
    void setRequestData(byte[] data);

    /**
     * 执行请求
     */
    void excute();

    void cancel();

    boolean isCancel();

    void setRequestHeader(Map<String, String> map);

    void setRequestType(String type);

}

定义了以上接口方法,有些参数真的是懒得写注释了,你们应该都看得懂。

然后在 IHttpService 的实现类里面需要做具体的 Http 请求,这里我偷个懒,直接用了 HttpClient。具体实现类 JsonHttpService大家可以去下载我的源码阅读。

HttpListener

这个类是交给 IHttpService 类在网络请求结束之后负责解析数据的,由于网络请求的结果只会有两种--成功和失败,所以这里就不定义解析数据的方法了,因为它是在网络请求成功之后调用的。

public interface IHttpListener {

    /**
     * 网络请求成功回调
     *
     * @param httpEntity 网络请求返回结果
     */
    void onSuccess(HttpEntity httpEntity);

    void onFail(int errorCode, String errorMsg);
}

然后 IHttpListener 持有 IDataListener 实例,最后调用 IDataListener 来回调给 Activity。
对了,这里需要做线程切换哦,具体实现代码也很简单,请下载源码自行阅读。

HttpTask

整套网络请求写完了,我们需要一个类来调用 IHttpService 的 excuse 的方法去执行网络请求,且excuse方法必须在子线程。我们把一次网络请求封装成一个异步任务,即一个 Runnable,代码很简单。

public class HttpTask<T> implements Runnable {

    private IHttpService mHttpService;

    public HttpTask(RequestHolder<T> holder) throws UnsupportedEncodingException {
        mHttpService = holder.getHttpService();
        mHttpService.setHttpListener(holder.getHttpListener());
        mHttpService.setRequestType(holder.getRequestType());
        mHttpService.setUrl(holder.getUrl());
        mHttpService.setRequestHeader(holder.getRequestHeader());
        T requestParams = holder.getRequestParams();
        if (requestParams != null) {
            String requestInfo = JSON.toJSONString(requestParams);
            mHttpService.setRequestData(requestInfo.getBytes("UTF-8"));
        }
    }

    @Override
    public void run() {
        if (!mHttpService.isCancel()) {
            mHttpService.excute();
        }
    }
}

ThreadPoolManger

最后我们需要一个线程池管理HttpTask。所以我们需要一个ThreadPoolManger,因为这个类必须保证唯一,所以单例。线程池管理我们直接用 concurrent 包封装好的Executors 类创建,这个类代码很简单,如下:

public class ThreadPoolManger {
    private static volatile ThreadPoolManger mInstance;
    private final ExecutorService mExecutorService;

    private ThreadPoolManger() {
        mExecutorService = Executors.newFixedThreadPool(4);
    }

    public static ThreadPoolManger getInstance() {
        if (mInstance == null) {
            synchronized (ThreadPoolManger.class) {
                mInstance = new ThreadPoolManger();
            }
        }
        return mInstance;
    }

    public void execute(HttpTask task) {
        mExecutorService.execute(task);
    }

}

Volley

最后创建IHttpService、RequestHolder、IHttpListener、HttpTask 等类,并将HttpTask 丢进线程池。这一系列操作对于调用者来说是不需要关心的,可以隐藏,所以需要一个 Volley 类去隐藏这个过程。代码也很简单:

public class Volley {
    static HashMap<String, String> mGlobalHeader = new HashMap<>();

    public static <T, M> IHttpService sendRequest(T requestParams, String url, Class<M> responseClass, @RequestType String type,IDataListener<M> listener) {
        IHttpService jsonHttpService = new JsonHttpService();

        IHttpListener jsonDealListener = new JsonDealListener<>(listener, responseClass);

        RequestHolder<T> requestHolder = new RequestHolder<>(requestParams, jsonHttpService, jsonDealListener, url,type);

        try {
            HttpTask<T> task = new HttpTask<>(requestHolder);
            ThreadPoolManger.getInstance().execute(task);
        } catch (UnsupportedEncodingException e) {
            listener.onFail(0, e.getMessage());
        }
        return jsonHttpService;
    }


    public static void setGlobalHeader(String key, String value) {
        mGlobalHeader.put(key, value);
    }

}

至此,除了IHttpListener 和 IHttpService 的实现类没有贴上来,整个网络请求架构的雏形已经完成了。
我们来测试一下~

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(View view) {
        String url = "http://gank.io/api/data/Android/10/1";
        for (int i = 0; i < 50; i++) {
            Volley.sendRequest(null, url, GankResponse.class, GET, new IDataListener<GankResponse>() {
                @Override
                public void onSuccess(GankResponse response) {
                    Log.e("___", "请求成功");
                }

                @Override
                public void onFail(int errorCode, String errorMsg) {
                    Log.e("___error", errorMsg + "__errorCode:" + errorCode);
                }
            });
        }
    }
}

以上是在 MainActivity 里面点击了一个按钮,做了一个50次网络请求的并发操作,亲测成功。

下期预告

下期我们将基于这个架构,继续做下载的扩展。

其中会涉及到多线程下载,断点续传等各种操作,如果时间来的及的话,还会结合数据库做一些神奇的操作。

哦,对了,贴上

代码地址

如果我的文章能给你带来帮助,请记得点个 star,么么哒~