自定义OKHttp的拦截器

博文出处:一起来写OKHttp的拦截器,欢迎大家关注我的博客,谢谢!
00:00
一开始就不多说废话了,主要因为工作时遇到了一些使用 OKHttp 拦截器的问题,所以在此特写这篇以作记录。
现如今,做 Android 开发在选择网络框架时,大多数都会首推 Retrofit 。Retrofit 以其简洁优雅的代码俘获了大多数开发者的心。
然而 Retrofit 内部请求也是基于 OKHttp 的,所以在做一些自定义修改 HTTP 请求时,需要对 OKHttp 拦截器具有一定了解。相信熟悉 OKHttp 的同学都知道,OKHttp 内部是使用拦截器来完成请求和响应的,利用的是责任链设计模式。所以可以说,拦截器是 OKHttp 的精髓所在。
那么接下来,我们就通过一些例子来学习怎样编写 OKHttp 的拦截器吧,其实这些例子也正是之前我遇到的情景。
00:01
添加请求 Header
假设现在后台要求我们在请求 API 接口时,都在每一个接口的请求头上添加对应的 token 。使用 Retrofit 比较多的同学肯定会条件反射出以下代码:
@FormUrlEncoded
@POST("/mobile/login.htm")
Call<ResponseBody> login(@Header("token") String token, @Field("mobile") String phoneNumber, @Field("smsCode") String smsCode);
这样的写法自然可以,无非就是每次调用 login API 接口时都把 token 传进去而已。但是需要注意的是,假如现在有十多个 API 接口,每一个都需要传入 token ,难道我们去重复一遍又一遍吗?
相信有良知的程序员都会拒绝,因为这会导致代码的冗余。
那么有没有好的办法可以一劳永逸呢?答案是肯定的,那就要用到拦截器了。
代码很简单:
public class TokenHeaderInterceptor implements Interceptor {

@Override
public Response intercept(Chain chain) throws IOException {
    // get token
    String token = AppService.getToken();
    Request originalRequest = chain.request();
    // get new request, add request header
    Request updateRequest = originalRequest.newBuilder()
            .header("token", token)
            .build();
    return chain.proceed(updateRequest);
}

}
我们先拦截得到 originalRequest ,然后利用 originalRequest 生成新的 updateRequest ,再交给 chain 处理进行下一环。
最后,在 OKHttpClient 中使用:
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new TokenHeaderInterceptor())
.build();

Retrofit retrofit = new Retrofit.Builder().baseUrl(BuildConfig.BASE_URL)
.client(client).addConverterFactory(GsonConverterFactory.create()).build();
改变请求体
除了增加请求头之外,拦截器还可以改变请求体。
假设现在我们有如下需求:在上面的 login 接口基础上,后台要求我们传过去的请求参数是要按照一定规则经过加密的。
规则如下:
请求参数名统一为content;
content值:JSON 格式的字符串经过 AES 加密后的内容;
举个例子,根据上面的 login 接口,现有
{"mobile":"157xxxxxxxx", "smsCode":"xxxxxx"}
JSON 字符串,然后再将其加密。最后以 content=[加密后的 JSON 字符串] 方式发送给后台。
看完了上面的 TokenHeaderInterceptor 之后,这需求对于我们来说可以算是信手拈来:
public class RequestEncryptInterceptor implements Interceptor {

private static final String FORM_NAME = "content";
private static final String CHARSET = "UTF-8";

@Override
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RequestBody body = request.body();
    if (body instanceof FormBody) {
        FormBody formBody = (FormBody) body;
        Map<String, String> formMap = new HashMap<>();
        // 从 formBody 中拿到请求参数,放入 formMap 中
        for (int i = 0; i < formBody.size(); i++) {
            formMap.put(formBody.name(i), formBody.value(i));
        }
        // 将 formMap 转化为 json 然后 AES 加密
        Gson gson = new Gson();
        String jsonParams = gson.toJson(formMap);
        String encryptParams = AESCryptUtils.encrypt(jsonParams.getBytes(CHARSET), AppConstant.getAESKey());
        // 重新修改 body 的内容
        body = new FormBody.Builder().add(FORM_NAME, encryptParams).build();
    }
    if (body != null) {
        request = request.newBuilder()
                .post(body)
                .build();
    }
    return chain.proceed(request);
}

}
代码中已经添加了关键的注释,相信我已经不需要多解释什么了。
经过了这两种拦截器,相信同学们已经充分体会到了 OKHttp 的优点和与众不同。
最后,自定义拦截器的使用情景通常是对所有网络请求作统一处理。如果下次你也碰到这种类似的需求,别忘记使用自定义拦截器哦!
00:02
呃呃呃,按道理来讲应该要结束了。
但是,我在这里开启一个番外篇吧,不过目标不是针对拦截器而是 ConverterFactory 。
还是后台需求,login 接口返回的数据也是经过 AES 加密的。所以需要我们针对所有响应体都做解密处理。
另外,还有很重要的一点,就是数据正常和异常时返回的 JSON 格式不一致。
在业务数据正常的时候(即 code 等于 200 时):
{
"code":200,
"msg":"请求成功",
"data":{
"nickName":"Hello",
"userId": "1234567890"
}
}
业务数据异常时(即 code 不等于 200 时):
{
"code":7008,
"msg":"用户名或密码错误",
"data":"用户名或密码错误"
}
而这会在使用 Retrofit 自动从 JSON 转化为 bean 类时报错。因为 data 中的正常数据中是 JSON ,而另一个异常数据中是字符串。
那么,如何解决上述的两个问题呢?
利用 自定义 ConverterFactory !!
我们先创建包名 retrofit2.converter.gson ,为什么要创建这个包名呢?
因为自定义的 ConverterFactory 需要继承 Converter.Factory ,而 Converter.Factory 类默认是包修饰符。
代码如下:
public final class CustomConverterFactory extends Converter.Factory {

private final Gson gson;

public static CustomConverterFactory create() {
    return create(new Gson());
}

@SuppressWarnings("ConstantConditions") // Guarding public API nullability.
public static CustomConverterFactory create(Gson gson) {
    if (gson == null) throw new NullPointerException("gson == null");
    return new CustomConverterFactory(gson);
}

private CustomConverterFactory(Gson gson) {
    this.gson = gson;
}

@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    // attention here!
    return new CustomResponseConverter<>(gson, adapter);
}

@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonRequestBodyConverter<>(gson, adapter);
}

}
从代码中可知,CustomConverterFactory 内部是根据 CustomResponseConverter 来转化 JSON 的,这才是我们的重点。
class CustomResponseConverter<T> implements Converter<ResponseBody, T> {

private final Gson gson;
private final TypeAdapter<T> adapter;
private static final String CODE = "code";
private static final String DATA = "data";

CustomResponseConverter(Gson gson, TypeAdapter<T> adapter) {
    this.gson = gson;
    this.adapter = adapter;
}

@Override
public T convert(ResponseBody value) throws IOException {
    try {
        String originalBody = value.string();
        // 先 AES 解密
        String body = AESCryptUtils.decrypt(originalBody, AppConstant.getAESKey());
        // 再获取 code 
        JSONObject json = new JSONObject(body);
        int code = json.optInt(CODE);
        // 当 code 不为 200 时,设置 data 为 null,这样转化就不会出错了
        if (code != 200) {
            Map<String, String> map = gson.fromJson(body, new TypeToken<Map<String, String>>() {
            }.getType());
            map.put(DATA, null);
            body = gson.toJson(map);
        }
        return adapter.fromJson(body);
    } catch (Exception e) {
        throw new RuntimeException(e.getMessage());
    } finally {
        value.close();
    }
}

}
代码也是很简单的,相信也不需要解释了。o(∩_∩)o
最后就是使用了 CustomConverterFactory :
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new TokenHeaderInterceptor())
.addInterceptor(new RequestEncryptInterceptor())
.build();
Retrofit retrofit = new Retrofit.Builder().baseUrl(BuildConfig.BASE_URL)
.client(client).addConverterFactory(CustomConverterFactory.create()).build();
好了,这下真的把该讲的都讲完了,大家可以散了。
完结了。
再见!
再见!
再见!
重要的说三遍!!!
再说最后一遍,再见!!!
00:03
References

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容