一个网络库的封装过程

最近团队调研决定使用 GraphQL 方案替代 RestApi。Android 方面有个 Apollo 社区维护功能相对完整的库,对不同查询与缓存支持的比较完善,但是资源耗费相当严重,尤其是 codegen 对编译时的环境配置要求非常高,决定放弃这个库采用 rawhttp 的方式直接实现 GraphQL 协议。本文来讲讲对 GraphQL 协议支持过程的实现思路。

首先,GraphQL 协议是自成体系不依赖下层传输的,也就是说我们可以理解为只要能够通信,按照 GraphQL 的通信协议就能完成数据交换。通常情况下这个协议的传输层使用 http(s) 的方式。那么就一下子拉近到了我们熟知的领域,完成一次 http 请求采用 okhttp,rxjava,缓存的处理使用 DiskLruCache, 站在巨人的肩膀上看的更远。

构建一个使用 api 的统一入口,包括发起请求,配置缓存、okhttpclient、url 等等,由于参数比较多我们使用建造者模式

public class OkGraphQL {
private String baseUrl;
private OkHttpClient okHttpClient;
private Cache cache;

... Getter 方法

public static class Builder {
private OkGraphQL okGraphQL;

public Builder() {
okGraphQL = new OkGraphQL();
}

public OkGraphQL build() {
return okGraphQL;
}

public Builder cache(Cache cache) {
okGraphQL.cache = cache;
return this;
}

public Builder okClient(OkHttpClient okHttpClient) {
okGraphQL.okHttpClient = okHttpClient;
return this;
}

public Builder baseUrl(String baseUrl) {
okGraphQL.baseUrl = baseUrl;
return this;
}
}
}

其中 Cache 是个 interface,主要是 get、set 功能,根据场景可以采用不用的实现如 sqlite,文件等,我们这里直接使用著名的 DiskLruCache

public class CacheImpl implements Cache {
DiskLruCache diskLruCache;

public CacheImpl(File directory, long maxSize) {
try {
if (!directory.exists()) {
directory.mkdirs();
}
diskLruCache = DiskLruCache.open(directory, 20180307, 1, maxSize);
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public String get(String key) {
try {
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} else {
return null;
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

@Override
public void save(String key, String value) {
try {
DiskLruCache.Editor editor = diskLruCache.edit(key);
OutputStream os = editor.newOutputStream(0);
os.write(value.getBytes());
editor.commit();
diskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}

构建一个 api 的使用入口

okGraphQL = new OkGraphQL.Builder()
.okClient(okHttpClient)
.cache(new CacheImpl(
new File(context.getCacheDir(), "graphql"),
1024 * 1024 * 30))
.baseUrl("http://localhost/graphql")
.build();

接下来的重点是实现 GraphQL 协议,通常一个 GraphQL 的查询字符串都比较长,自然想到把每个查询放入 asset 中的每个文件里,然后通过合理的组织文件目录结构来编写每个查询,最后通过一个类把这些文件与所需要的参数组织成标准的 GraphQL 语句把请求发出去,并把结果合理回调

设计了如下目录结构组织每个查询语句:

graphql             //查询的根目录
├── fragments //fragments的根目录
│   │── coin
│ └── ...
└── market //根据业务,可设多层文件夹
│── index
└── ...

然后在 OkGraphQL 中添加获取查询的入口

public Query queryFrom(String fileName) {
return this.query(Utils
.renderFromAsset("graphql/" + fileName));
}

public Mutation mutationFrom(String fileName) {
return this.mutation(Utils
.renderFromAsset("graphql/" + fileName));
}

从文件中获取字符串的方法封装为 Utils.renderFromAsset,参数是 asset 文件的名成,返回值是文件的内容。

接下来的一个重点是标准 GraphQL 语句的拼装,可以抽象为 BaseQuery,实际上 Query 和 Mutation 都是它的子类。

我们通过 BaseQuery 暴露出 variable 方法和 fragment 方法来接收相关参数,然后在 getContent 方法中将所有参数拼接为标准的 GraphQL 语句,最后通过 toObservable 方法返回 rxjava 形式的结果输出。

然后一大重点是缓存,我们知道传统 http 请求的缓存可以用 url 做 key,但是在 GraphQL 中 url 通常是不会变的,而它的查询语句往往直接决定的查询的结果,因此我们选择将 GraphQL 的查询语句,也就是 getContent 的结果做 md5 作为缓存的 key,相应的我们只缓存有效数据,也就是 api 正常返回情况下的 data 数据。这样既减少了文件操作复杂度,增加了对缓存内容的可控性同时也能完全满足业务场景的需要。根据业务模型我们定义了三种缓存策略

public enum CachePolicy {
/**
* Response using cache only
*
* Will request for network data when there is no cache exists
*/
CACHEONLY,

/**
* Request for network every time, won't use cache
*/
NOCACHE,

/**
* Using cahce for subscriber, refreshing with network data another time
*
* It means that you will have got twice invoked for business logic
*/
WITHCACHE
}

对缓存策略的支持在 BaseQuery 的 toObservable 中实现,下面我们就来看下 BaseQuery 的实现逻辑:

public class BaseQuery {
private CachePolicy cachePolicy = CachePolicy.NOCACHE;

private final String query;
private final String prefix;
private final OkGraphQL okGraphQL;

private final List<VariableValues> variableValues = new ArrayList<>();
private final List<String> fragments = new ArrayList<>();

public BaseQuery(OkGraphQL okGraphql, String prefix, String query) {
this.okGraphQL = okGraphql;
this.prefix = prefix;
this.query = query;
}

public BaseQuery variable(String key, Object value) {
variableValues.add(new VariableValues(key, value));
return this;
}

public BaseQuery fragmentFrom(String fileName) {
fragments.add(Utils
.renderFromAsset("graphql/fragments/" + fileName));
return this;
}

public BaseQuery fragment(String fragment) {
fragments.add(fragment);
return this;
}

public BaseQuery cachePolicy(CachePolicy cachePolicy) {
this.cachePolicy = cachePolicy;
return this;
}

public String getContent() {
// ...
// 根据 GraphQL 标准语法拼装 query,prefix,variableValues,fragments
}

public <T> Observable<T> toObservable(Class<T> clz) {
return Observable.create((Observable.OnSubscribe<T>) subscriber -> {
String queryContent = getContent();

// 将查询语句做 md5 的值作为缓存的 key
String cacheKey = Utils.getCacheKey(queryContent);

if (cachePolicy != CachePolicy.NOCACHE) {

//取缓存策略
String cachedString = okGraphQL.getCache().get(cacheKey);
if (cachedString != null) {
T bean = App.gson.fromJson(cachedString, clz);
if (bean != null) {
subscriber.onNext(bean);

if (cachePolicy == CachePolicy.CACHEONLY) {
//如果正常取到缓存,并且缓存逻辑为 CACHEONLY,则结束逻辑
subscriber.onCompleted();
return;
}
}
}
}

// 发起 rawhttp 请求
Response resp = null;
try {
resp = okGraphQL.getOkHttpClient().newCall(
new Request.Builder()
.url(okGraphQL.getBaseUrl())
.addHeader("accept", "application/json")
.addHeader("content-type", "application/json")
.post(RequestBody.create(MediaType.parse("application/json"), queryContent))
.build())
.execute();
} catch (IOException e) {
e.printStackTrace();
subscriber.onError(new HttpException(e.getMessage(), 0));
return;
}

if (resp.isSuccessful()) {
// http 请求成功的情况
JsonObject respObj = null;
try {
respObj = App.gson.fromJson(resp.body().string(), JsonObject.class);
} catch (IOException e) {
e.printStackTrace();
subscriber.onError(new GsonNullException("Error when parse respObj!"));
return;
}

if (respObj.get("errors") != null) {
JsonObject errObj = respObj.get("errors").getAsJsonArray().get(0).getAsJsonObject();
String errMessage = errObj.get("message").getAsString();
subscriber.onError(new ServerException(errMessage));
return;
}

if (respObj.get("data") != null) {
JsonElement dataElement = respObj.get("data");
T bean = App.gson.fromJson(dataElement, clz);
if (bean == null) {
subscriber.onError(new GsonNullException("Can't deserilize data object."));
} else {
// 正常取回 api 返回的的数据 json,存入缓存
okGraphQL.getCache().save(cacheKey, dataElement.toString());

subscriber.onNext(bean);
subscriber.onCompleted();
}
return;
}

} else if (resp.code() == 401 || resp.code() == 403) {
// ...
// Not authorized, trigger signout flow
} else {
subscriber.onError(new HttpException(resp.message(), resp.code()));
}
})
// 将 http 请求放入 io 线程
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}

至此大部分请求逻辑封装完成,为了对回调结果有一个统一的 hook 我们封装了一个 subscriber 来统一处理

public class NetSubscriber<T> extends Subscriber<T> {

private static final String TAG = "NetSubscriber";
private NetSubOnNext mSubOnNext;
private NetSubOnError mSubOnError;
private NetSubOnComplete mSubOnComplete;

public interface NetSubOnComplete {
void onComplete();
}

public interface NetSubOnNext<T> {
void onNext(T bean);
}

public interface NetSubOnError {
void onError(Throwable e);
}

public NetSubscriber() {
}

public NetSubscriber(NetSubOnComplete subOnComplete) {
this.mSubOnComplete = subOnComplete;
}

public NetSubscriber(NetSubOnNext<T> subOnNext) {
this.mSubOnNext = subOnNext;
}

public NetSubscriber(NetSubOnNext<T> subOnNext, NetSubOnError subOnError) {
this.mSubOnNext = subOnNext;
this.mSubOnError = subOnError;
}

public NetSubscriber(NetSubOnNext<T> subOnNext,
NetSubOnError subOnError,
NetSubOnComplete subOnComplete) {
this.mSubOnNext = subOnNext;
this.mSubOnError = subOnError;
this.mSubOnComplete = subOnComplete;
}

@Override
public final void onCompleted() {
if (mSubOnComplete != null) mSubOnComplete.onComplete();
}

@Override
public final void onError(Throwable e) {
if (e instanceof ServerException) {
// handle server error

} else if (e instanceof HttpException) {
// handle http error

} else if (e instanceof GsonNullException) {
// handle gson error

} else if (e instanceof UnknownHostException) {
// ...

} else {

}
}

@Override
public final void onNext(T bean) {
if (mSubOnNext != null) {
try {
mSubOnNext.onNext(bean);
} catch (Exception e) {
Log.d("NetSubscriber", "Error in subscriber");
e.printStackTrace();
}
}
}

}

至此我们可以简单的发起一个请求:

编写 asset 中 graphql 目录下 market/index 文件

{
marketTagList(tagType: PREFER) {
tagType
tagName
}
marketList(tagType: COIN, tagName: "BTC") {
exchange
pairSymbol
prefer
rawPrice
rawPriceUnit
price
priceUnit
}
}

发起请求并接收结果

mOkGraphQL.queryFrom("market/index")
.cachePolicy(CachePolicy.NOCACHE)
.toObservable(MarketBean.class)
.subscribe(new NetSubscriber<>(bean -> {
Log.d("######", "######:" + App.gson.toJson(bean));

}, e -> {
Log.d("######", "######: error");
e.printStackTrace();

}, () -> {
Log.d("######", "######: completed");

}))

框架开发过程中强绑定了业务场景,没有足够的精力把代码抽出成独立库,但是我把主要文件都放在了 https://github.com/timqi/android-graphql,相信看完本文可以非常简单的把 GraphQL 支持集成进你的项目。