Http二次封装思路

用过了现有的一些http请求框架,决定自己也来简单的封装一个。本文只是基于一些http请求框架二次封装的,高手勿喷。源码

http的请求和响应

一个http的请求通常包含请求头、请求体、响应头和响应体,考虑到这些因素,HttpConfigApiResponse就诞生了。HttpConfig

HttpConfig

http参数配置类,应该包含如下属性:

  • baseUrl/url

    如果使用restful形式,baseUrl是不能为空并且url为业务path, 如果是非restful,url必须为请求全路径

  • GET/POST

    有了url,接下来需要有请求的方法类型,由于我这边只用到了GETPOST,所以只对此做了封装。

    特别说明: POST 有三种提交方式(form表单、json形式和 复杂形式)

FORM_DATA("application/x-www-form-urlencoded;charset=utf-8"),
JSON_DATA("application/json;charset=utf-8"),
MULTI_PART_DATA("multipart/form-data;charset=utf-8");
  • headers

    http的请求头封装,采用(Map<String,String>)集合

  • params

    http的请求参数,采用(Map<String,Object>)如果请求方法是GET形式,那么采用拼接字符串的形式将参数拼接到url中; 如果请求方法是POST形式,则需要根据提交参数的方式不同,会有不同的请求体。

  • cacheStrategy

    考虑到App的使用交互和服务器减压,我们要考虑有一些请求可以做一些缓存,那么常用的缓存策略有CACHE_ONLYCACHE_FIRSTNET_ONLYNET_CACHE

  • type

    type是响应数据的type,这个主要用在Http请求结果返回后将json转为bean对象的映射类型,需要考虑泛型和非泛型(Class和ParamizableType)

  • tag

    给每一个请求链接打一个标签,可用于一些其它的操作,如根据tag取消请求

  • isAsync

    当前请求是同步执行还是异步执行的标志,异步执行会在子线程中进行http请求,同步执行在当前线程中执行http请求。

ApiResponse

使用ApiResponse的原因是为了规范请求结果返回的表现形式,他有一个T类型的数据。

  • code

    状态码,与http请求响应状态码一致,200~300 请求成功,304 使用缓存

  • message

    请求响应的错误信息

  • data

    响应的数据,泛型T, 根据httpConfig中的type,映射 json–> bean

Http引擎

IHttpEngine是一个接口,使用者可以根据实际的情况做具体的实现。

public interface IHttpEngine {

    // 开始执行 http请求
    <T> execute(HttpConfig config, MultableLiveData<ApiResponse<T>> liveData);
    // 根据tag取消
    void cancel(Object tag);
}

由于http请求有同步和异步两种情况并且又牵扯到了缓存策略问题(如果先进行缓存返回在执行网络请求并返回数据),在异步回调的情况下这些问题可以通过回调解决,但是在同步情况下,这些问题并不能很好的处理,曾经有使用过将当前请求clone,然后再次调用请求服务器的方法,但是在使用过jetpackLiveData框架后这些问题都可以解决了。

OkhttpEngine

Android开发目前来说大多数项目使用的都是Okhttp来做请求,本次我使用的默认引擎也是使用它来作为默认的实现。OkHttpEngine

okhttp简单配置

对okhttp进行一些简单的配置就可以进行网络请求了,如下:

  • 创建okHttpClient
 private static final OkHttpClient OK_HTTP_CLIENT;

 // 添加日志拦截器
 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
 loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

 OK_HTTP_CLIENT = new OkHttpClient.Builder()
               .connectTimeout(15, TimeUnit.SECONDS)
               .readTimeout(15, TimeUnit.SECONDS)
               .writeTimeout(15, TimeUnit.SECONDS)
               .addInterceptor(loggingInterceptor)
               .build();
  • 添加证书管理
TrustManager[] trustManagers = new TrustManager[]{
    new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
     }
};

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);

实现Http请求

IHttpEngine中的execute方法是具体的http请求方法,所有实现IHttpEngine的方法都需要实现此方法。

@NonNull
@Override
public <T> void execute(@NonNull Config config, 
                        @NonNull MutableLiveData<ApiResponse<T>> liveData) {
    Request request = generateRequest(config);
    Call call = OK_HTTP_CLIENT.newCall(request);
    if (!config.isAsync) {
        execute(call, config, liveData);
    } else {
        enqueue(call, config, liveData);
    }
}

execute方法中大致需要做以下事:

  • 创建request–> genearteRequest
  • 发送http请求 —> execute/enqueue

创建httpRequest

generateRequest()根据HttpConfig中的请求方式来创建不同的http请求。

/**
 * 根据配置信息生成request
 *
 * @param config 配置信息
 * @return request
 */
 @NonNull
 private Request generateRequest(@NonNull Config config) {
    switch (config.method) {
       case Config.GET:
           return generateGetRequest(config);
       case Config.POST:
           return generatePostRequest(config);

       default:
           throw new IllegalStateException("this request method invalidate: " + config.method);
    }
}
GET请求的创建方式

genearteGetReques()方法,利用okhttp的Request类创建request实例,并利用UrlCreator将参数拼接到url中。Url拼接时需要注意使用UrlEncoder编码,不然可能会造成服务器和客户端解析数据不一致的情况。

/**
 * 生成get方式的请求
 *
 * @param config 请求配置
 * @return 返回get方式的request
 */
@NonNull
private Request generateGetRequest(@NonNull Config config) {
   Request.Builder builder = new Request.Builder().get();
   builder.tag(config.tag);
   addHeader(builder, config);
   String url = UrlCreator.generateUrlForParams(config.url(), config.getParams());
   return builder.url(url).build();
}
POST请求的创建方式

POST请求方式提交内容时相对于GET方式要复杂许多,他需要根据提交方式的不同添加不同的header和内容body, 在generatePostRequest()方法中先利用okhttp的request类创建request实例,根据调用者在HttpConfig中设置formData来创建不同形式的body

/**
 * 生成post请求
 *
 * @param config http请求配置信息
 * @return 请求request
 */
@NonNull
private Request generatePostRequest(@NonNull Config config) {
   Request.Builder builder = new Request.Builder().url(config.url());
   builder.tag(config.tag);
   addHeader(builder, config);

   // 根据提交方式添加header信息
   Pair<String, String> header = config.formData.getHeader();
   builder.addHeader(header.first, header.second);

   // 创建body
   RequestBody body = generatePostRequestBody(config);
   return builder.post(body).build();
}

判断formData类型,创建不同的request body

/**
 * 获取post提交体
 *
 * @param config 请求配置信息
 * @return RequestBody
 */
@NonNull
private RequestBody generatePostRequestBody(@NonNull Config config) {
   FormData formData = config.formData;
   switch (formData) {
       case FORM_DATA:
           return getFormDataRequestBody(config);
       case JSON_DATA:
           return getJsonDataRequestBody(config);
       case MULTI_PART_DATA:
           return getMultiDataRequestBody(config);
       default:
           throw new IllegalArgumentException("post formData is invalidate: " + formData);
   }
}
创建FormData(表单)

form表单形式比较简单,只需要创建一个okhttp的FormBody并将param添加,需要注意的是添加param时调用的是addEncoded方法。

/**
 * 生成form data形式的post数据
 *
 * @param config 请求配置
 * @return FromBody
 */
 @NonNull
 private RequestBody getFormDataRequestBody(@NonNull Config config) {
     FormBody.Builder builder = new FormBody.Builder(StandardCharsets.UTF_8);
     Map<String, Object> params = config.getParams();
     for (Map.Entry<String, Object> entry : params.entrySet()) {
          builder.addEncoded(entry.getKey(), String.valueOf(entry.getValue()));
     }
     return builder.build();
 }
json形式

利用okHttp中的RequestBody.create()方法创建一个json形式的body,需要传递json和json形式的header。

/**
 * 生成json形式的post数据
 *
 * @param config 请求配置
 * @return RequestBody
 */
 @NonNull
 private RequestBody getJsonDataRequestBody(@NonNull Config config) {
   if (config.getParams().isEmpty()) {
        throw new IllegalArgumentException("json data is null");
   }
   Object json = config.getParams().get(Config.JSON_KEY);
   return RequestBody.create(String.valueOf(json), MediaType.parse(config.formData.getValue()));
 }
复杂形式的body

复杂形式的body,主要是用在文件上传这一块儿。它需要判断当前param是普通key-value、单文件和多文件。

利用okhttp的MultiparBody创建body对象并根据内容类型调用的不同的body,然后调用addFormDataPart添加到MultipartBody中。

/**
 * 获取复杂的post提交体
 *
 * @param config 请求配置信息
 * @return MultipartBody
 */
@NonNull
@SuppressWarnings("unchecked")
private RequestBody getMultiDataRequestBody(@NonNull Config config) {

    MultipartBody.Builder builder = new MultipartBody.Builder();
    builder.setType(MultipartBody.FORM);

    for (Map.Entry<String, Object> entry : config.getParams().entrySet()) {
        String key = entry.getKey();
        Object value = entry.getValue();

        if (value instanceof File) {
            File file = (File) value;
            RequestBody requestBody = MultipartBody.create(file, getFileMediaType(file));
            builder.addFormDataPart(key, file.getName(), requestBody);
        } else if (value instanceof List) {
            List<File> files = (List<File>) value;
            for (int i = 0; i < files.size(); i++) {
                File file = files.get(i);
                RequestBody requestBody = MultipartBody.create(file, getFileMediaType(file));
                builder.addFormDataPart(key + i, file.getName(), requestBody);
            }
        } else {
            builder.addFormDataPart(key, String.valueOf(value));
        }
    }
    return builder.build();
}

添加文件时需要有文件的类型,文件类型的获取方式是通过UrlConnection的getFileNameMap方法获取。

/**
 * 获取文件的type类型
 *
 * @param file 文件
 * @return MediaType
 */
@Nullable
private MediaType getFileMediaType(@NonNull File file) {
    FileNameMap fileNameMap = URLConnection.getFileNameMap();
    String contentTypeFor = fileNameMap.getContentTypeFor(file.getAbsolutePath());
    if (contentTypeFor == null) {
        contentTypeFor = "application/octet-stream";
    }
    return MediaType.parse(contentTypeFor);

通过以上的几种方式就可以创建一个http请求了,接下来需要发送请求了,发送请求需要通过httpConfig中的isAsync来判断是开启一个子线程还是在当前线程中执行操作(同步与异步)。

发送http请求

发送http请求会有同步请求(execute)和异步请求(enqueue), 同步请求是在当前线程中发送http请求,异步请求采用okhttp线程池发送请求。处理请求时在合适的地方需要判断该请求是否被取消。

 if (call.isCanceled()) {
    return;
 }
同步请求(execute)

在发送http请求时会根据缓存策略进行响应的处理,而同步请求没有办法做到return后可以继续进行网络请求,所以引入了LiveData来发送数据。

目前缓存策略有四种,我们需要根据判断来进行不同的操作,关于缓存的创建和获取后续会有说明,这块儿只有根据缓存策略处理数据。

/**
 * 同步执行的方法
 */
@SuppressWarnings("unchecked")
private <T> void execute(Call call, Config config, MutableLiveData<ApiResponse<T>> liveData) {
    ApiResponse<T> apiResponse;
    Logs.d("execute before cache: " + Thread.currentThread().getName());
    // 只访问本地数据
    if (config.cacheStrategy == Config.CACHE_ONLY) {
        apiResponse = readCache(call.request().url().toString());
        liveData.postValue(apiResponse);
        return;
    }

    // 先访问本地数据,然后再发起网络请求
    if (config.cacheStrategy == Config.CACHE_FIRST) {
        apiResponse = readCache(call.request().url().toString());
        liveData.postValue(apiResponse);
    }

    Logs.d("execute current thread: " + Thread.currentThread().getName());
    // ..... 此处开始进行http网络请求


    if (call.isCanceled()) {
        return;
    }
    // liveData发送数据
    liveData.postValue(apiResponse);
    if (config.cacheStrategy != Config.NET_ONLY) {
        saveCache(call.request().url().toString(), apiResponse);
    }
}

使用okhttp的execute方法发送http请求,并利用ConvertFactory进行数据解析,ConvertFactory后续会介绍。

 try {
      Response response = call.execute();
      IConvert<Response, T> convert = ConvertFactory.create();
      apiResponse = convert.convert(response, config.type);
 } catch (IOException e) {
      e.printStackTrace();
      apiResponse = new ApiResponse<>();
      apiResponse.status = 500;
      apiResponse.message = e.getMessage();
 }
异步请求(enqueue)

异步请求与同步请求一样也需要进行缓存策略进行缓存处理,这块儿的处理逻辑一致,所以下面的代码块将这部分省略。使用okhttp的enqueue方法发送http请求,在onResponse中并利用ConvertFactory进行数据的解析,在onFailure中自定义错误信息的返回。

private <T> void enqueue(Call call, Config config, MutableLiveData<ApiResponse<T>> liveData) {
        //... 缓存策略判断数据处理返回
        ...
        // 开始请求服务器
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                if (call.isCanceled()) {
                    return;
                }
                ApiResponse<T> apiResponse = new ApiResponse<>();
                apiResponse.status = 500;
                apiResponse.message = e.getMessage();
                liveData.postValue(apiResponse);
            }

            @SuppressWarnings("unchecked")
            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                if (call.isCanceled()) {
                    return;
                }
                IConvert<Response, T> convert = ConvertFactory.create();
                ApiResponse<T> apiResponse = convert.convert(response, config.type);
                liveData.postValue(apiResponse);
                if (config.cacheStrategy != Config.NET_ONLY) {
                    saveCache(call.request().url().toString(), apiResponse);
                }
            }
        });
    }

取消请求

cancle()方法中,利用Okhttp来取消请求,主要是比对tag, 如下:

@Override
public void cancel(@NonNull Object tag) {
    if (OK_HTTP_CLIENT == null) {
        return;
    }

    //查找当前需要取消的tag是否在未执行的请求中
    for (Call call : OK_HTTP_CLIENT.dispatcher().queuedCalls()) {
        if (tag.equals(call.request().tag())) {
            call.cancel();
        }
    }

    //查找当前需要请求的tag是否在正在执行的请求中
    for (Call call : OK_HTTP_CLIENT.dispatcher().runningCalls()) {
        if (tag.equals(call.request().tag())) {
            call.cancel();
        }
    }
}

响应数据转换

从服务器返回数据后,需要将数据格式进行转换成数据模型bean,由于数据返回的形式有多种,所以解析方式也会有多种,所以这块儿可以考虑使用数据解析工厂类去做数据解析,不同的解析数据做不同的逻辑实现,达到解偶。

定义一个Convert类,利用泛型的方式进行参数传递。

public interface IConvert<T, R> {
    // 数据的返回形式必须是ApiResponse, type即为泛型T的类型
    @NonNull
    ApiResponse<R> convert(@NonNull T t, @NonNull Type type);
}

这边提供了一种简单的convert调用方式,后面可以要考虑进行扩展选择:

 IConvert<Response, T> convert = ConvertFactory.create();
 apiResponse = convert.convert(response, config.type);

ConverFactoryIConvert的实现类。

public class ConvertFactory<R> implements IConvert<Response, R> {
    private static ConvertFactory convertFactory;


    public static ConvertFactory create() {
        if (convertFactory == null) {
            convertFactory = new ConvertFactory();
        }
        return convertFactory;
    }

    ...........
}

本地缓存

数据缓存采用的是Room数据库进行数据的存储,room是jetpack中的一种组件。它的创建方式也很简单。

创建数据库表

// Entity 表示该对象是一张数据库表
@Entity(tableName = "cache")
public class Cache {
    // primarykey表示表的主键
    @PrimaryKey
    @NonNull
    public String key;
    // ColumnInfo表示该字段在表中显示的字段名
    @ColumnInfo(name = "_data")
    public byte[] data;
}

创建数据库

// entities 表示 需要在该数据库中创建的表,可以创建多张
// version 数据库的版本号
// exportSchema 导出表创建的语句
@Database(entities = Cache.class, version = 1, exportSchema = true)
public abstract class CacheDatabase extends RoomDatabase {
    private static final CacheDatabase cacheDatabase;

    static {
        Application application = AppGlobals.getApplication();
        cacheDatabase = Room.databaseBuilder(application, CacheDatabase.class, "net_cache.db")
                .allowMainThreadQueries()
                .build();
    }

    // 数据库与Dao关联
    public abstract CacheDao getCacheDao();

    public static CacheDatabase get() {
        return cacheDatabase;
    }
} 

创建数据库表操作类Dao

// Dao 用来表示当前类是数据库表的操作类
@Dao
public interface CacheDao {
    // Insert 表示增加一条记录到数据库表中
    // onConflict 表示如果添加数据时出现冲突的解决策略
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void save(@NonNull Cache cache);

    // Delete 表示删除一条记录
    @Delete
    int delete(@NonNull Cache cache);

    // Update 表示更新一条记录
    // onConflict 表示如果更新数据时出现冲突的解决策略
    @Update(onConflict = OnConflictStrategy.REPLACE)
    int update(@NonNull Cache cache);

    // Query 查找一条记录
    @Query("SELECT * FROM cache WHERE `key`=:key")
    Cache query(@NonNull String key);
}

Type类型

由于java中的泛型在编译完毕后会擦除该类型,所以我们无法在同步执行或者多级泛型嵌套的情况下拿到泛型的实际类型。目前的解决方式有两种。

调用者传入实际类型

提供可以传入实际类型的方法,调用处将明确类型传递。

利用子类实现的方式获取实际类型

Java代码在编译成字节码class文件时会保留子类的泛型信息。所以大部分的json数据解析都是采用的此方法,如:Gson中的TypeToken 以及fasjson中的TypeReference。

public abstract class TypeToken<T> {
    // 泛型T的实际类型
    protected Type type;

    public TypeToken() {
        Type superClass = getClass().getGenericSuperclass();
        // 获取泛型的实际类型
        Type oriType = ((ParameterizedType) superClass).getActualTypeArguments()[0];

        if (oriType instanceof Class) {
            type = oriType;
        } else {
            //修复在安卓环境中问题
            type = putCacheTypeIfAbsent(oriType);
        }
    }
}

由于本次封装的返回的数据类型是ApiResponse<T>所以需要对泛型进行二次解析,所以新建了一个ApiResponseToken。

public abstract class ApiResponseToken<T> extends TypeToken<T> {

    public ApiResponseToken() {
        Type superClass = getClass().getGenericSuperclass();

        Type oriType = ((ParameterizedType) superClass).getActualTypeArguments()[0];

        if (oriType instanceof Class) {
            type = oriType;
        } else {
            // 解决ApiResponse<T>这种情况
            if (oriType instanceof ParameterizedType) {
                oriType = ((ParameterizedType) oriType).getActualTypeArguments()[0];
            }
            type = putCacheTypeIfAbsent(oriType);
        }
    }
}

由于本次框架的数据返回使用了LiveData<T>的形式,并通过订阅的方式实现数据返回,所以又添加了一个类型,用于自行获取type。

public abstract class HttpObserver<T> extends ApiResponseToken<T> 
        implements Observer<T> {
}

在使用LiveData的observe方法订阅时,在创建一个HttpObserserver实例,在构造方法中就可以解析出T的类型,就不需要我们手动传入T的真实类型了。

 /**
 * 开始订阅请求网络数据
 */
public <T> void observe(LifecycleOwner owner,
                        HttpObserver<ApiResponse<T>> observer) {
    // 由于HttpObserver是继承ApiResponseToken的,
    // 所以可以快速的获取到泛型T的实际类型
    Type type = observer.getType();
    mConfig.type = type;
    Logs.e("type: " + type);
}

使用方式:

.observe(owner, 
        // 通过new HtppObserver的方式就可以拿到具体的泛型值
         new HttpObserver<ApiResponse<JSONObject>>() {
             @Override
             public void onChanged(ApiResponse<JSONObject> apiResponse) {
                  .....
              }
        }
);

Http请求入口类

LiveHttp的封装,LiveHttp主要提供了以下功能:

  • 持有一个默认的http引擎

  • 可支持设置baseUrl和设置引擎的入口

  • 设置HttpConfig中的参数

  • 执行Http请求入口

  • 提供取消http请求的入口

  • 支持链式调用

以上几点都很简单,主要是来说一下执行http请求入口:

/**
 * 开始订阅请求网络数据
 */
public <T> void observe(LifecycleOwner owner,
                        HttpObserver<ApiResponse<T>> observer) {
    // 获取泛型实际类型
    Type type = observer.getType();
    mConfig.type = type;
    Logs.d("type: " + type);

    if (TextUtils.isEmpty(mConfig.url())) {
        throw new IllegalArgumentException("请求路径不能为空");
    }

    MutableLiveData<ApiResponse<T>> liveData = new MutableLiveData<>();
    // liveData的订阅必须是要在主线程中
    TaskExecutor.get().postToMain(() -> liveData.observe(owner, observer));
    sHttpEngine.execute(mConfig, liveData);
}

原文地址:https://www.cnblogs.com/xiaowj/p/13909612.html

 

posted @ 2020-04-26 13:13  jxiaow  阅读(456)  评论(0编辑  收藏  举报