OkHttp设置全局提交参数——基于拦截器(Interceptor)

###0.前言
在App与后端接口的交互中,后端许多时候都需要app传递一些通用的参数作为请求的标识。通常会选择在HTTP报文的请求头中添加,个别会GET、POST的请求参数中添加。

举个例子:
后端需要通过请求报文判断当前请求的app版本、系统类别、系统版本、市场渠道等。那么就会要求app端将相关参数设置到请求头或者提交参数中,传递给后端。

APP端需要保证每一个接口都传递相关的参数,那么就有以下几种做法:

  • 每一个接口都添加通用参数
  • 封装通用的请求工具类,在工具类里面添加参数
  • 配置拦截器对请求拦截配置

本文采用Retrofit + OkHttp的网络请求框架进行讲解

###1.常规的参数设置

@FormUrlEncoded
@POST(Api.User.LOGIN)
@Headers({
        "Accept: application/json",
        "User-Agent: NowyApp"
})
Observable<BaseEntity<LoginEntity>> login(@Field("phone") String mobile,
                                          @Field("pwd") String psw);

@FormUrlEncoded
@POST(Api.User.GET_USER_INFO)
Observable<BaseEntity<LoginEntity>> getUserInfo(
                                        @Header(ReqHeader.Key.HTTP_AUTHORIZATION)String token,
                                        @Field("userId") String userId);

从上面代码实例可知,可以通过retrofit的@Headers注解或者@Header注解设置请求头,或者通过@Field、@Query设置提交参数。
所以,最适合初学者的方式就是每个接口配置通用参数。

需要注意的是:retrofit 2.0 以后提供了@HeaderMap注解。

###2.封装通用请求工具类
此处以post为例:

@FormUrlEncoded
Observable<BaseEntity<String>> post(@Url String url,
                                    @HeaderMap Map<String,String> headerMap, 
                                    @FieldMap Map<String,String> postMap);

通过编写一个通用的提交方法,再编写一个HttpUtil类:

public static Observable<BaseEntity<String>> post(String url,Map<String,String> postMap){
       Map<String,String> header = new HashMap<>(); //配置通用参数
       header.put("Accept","application/json");
       header.put("User-Agent","NowyApp");
       return ApiManager.getRxNetWorkApi().post(url,header,postMap)
               .onErrorResumeNext(new HttpResponseFunc<BaseEntity<String>>())
               .subscribeOn(Schedulers.io());
   }

通过上面的示例代码可知,我们可以通过封装将重复代码抽离,减少部分工作量。当然,缺点就是代码嵌套的深度更深了。

注意:由于retrofit使用的是动态代理的方式实现网络请求操作,所以接口声明不能使用泛型,否则会遭到类型擦除。

###3.使用OkHttp拦截器实现通用参数配置
在OkHttp中,提供了拦截器addInterceptoraddNetworkInterceptor让开发者对请求进行拦截和处理。其本质就是通过责任链的模式对OkHttp的request进行代理。具体源码可以查阅OkHttp的RealInterceptorChain类。

所以,我们可以通过自定义拦截器Interceptor来封装通用参数。

此处为GitHub上提供的拦截器:

https://github.com/jkyeo/okhttp-basicparamsinterceptor

基本思路就是在拦截器中重新拼接请求参数,再整合为完整的请求。

不过,此方案的代码实现存在一个问题,那就是在拼接通用参数后没有动态更新报文长度,会造成后端接口参数不全问题。

下面给出修改后的完整的Basicparamsinterceptor代码:

/**
 * 设置全局通用的公共参数
 * @link https://github.com/jkyeo/okhttp-basicparamsinterceptor
 * post请求需要刷新Content-Length的数量
 * MultipartBody拼装需要注意分割线(boundary)问题
 *
 */
public class BasicParamsInterceptor implements Interceptor {

    Map<String, String> queryParamsMap = new HashMap<>();
    Map<String, String> paramsMap = new HashMap<>();
    Map<String, String> headerParamsMap = new HashMap<>();
    List<String> headerLinesList = new ArrayList<>();

    private BasicParamsInterceptor() {

    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        Request.Builder requestBuilder = request.newBuilder();

        // process header params inject
        Headers.Builder headerBuilder = request.headers().newBuilder();
        if (headerParamsMap.size() > 0) {
            Iterator iterator = headerParamsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                headerBuilder.add((String) entry.getKey(), (String) entry.getValue());
            }
        }

        //按行添加请求头
        if (headerLinesList.size() > 0) {
            for (String line: headerLinesList) {
                headerBuilder.add(line);
            }
        }

        // process header params end


        // process queryParams inject whatever it's GET or POST
        if (queryParamsMap.size() > 0) {
            request = injectParamsIntoUrl(request.url().newBuilder(), requestBuilder, queryParamsMap);
        }

        // process post body inject
        if (paramsMap != null && paramsMap.size() > 0
                && request != null && request.method().equals("POST")) {
            if (request.body() instanceof FormBody) {
                FormBody.Builder newFormBodyBuilder = new FormBody.Builder();
                if (paramsMap.size() > 0) {
                    Iterator iterator = paramsMap.entrySet().iterator();
                    while (iterator.hasNext()) {
                        Map.Entry entry = (Map.Entry) iterator.next();
                        newFormBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());
                    }
                }
                FormBody oldFormBody = (FormBody) request.body();
                int paramSize = oldFormBody == null ? 0 : oldFormBody.size();
                if (paramSize > 0) {
                    for (int i=0;i<paramSize;i++) {
                        newFormBodyBuilder.add(oldFormBody.name(i), oldFormBody.value(i));
                    }
                }

                FormBody newFormBody = newFormBodyBuilder.build();
                long newContentLength = newFormBody.contentLength();
                requestBuilder.post(newFormBody);


                updateHeader(headerBuilder,requestBuilder,newContentLength);
                request = requestBuilder.build();

            } else if (request.body() instanceof MultipartBody) {
                MultipartBody multipartBody = ((MultipartBody)request.body());
                List<MultipartBody.Part> oldParts = null ;

                String boundary = null;
                if(multipartBody != null){
                    boundary = multipartBody.boundary();
                    oldParts =  multipartBody.parts();
                }

                /**
                 * 保证分割线(boundary)与请求头相同
                 */
                MultipartBody.Builder multipartBuilder;
                if(boundary != null){
                    multipartBuilder = new MultipartBody.Builder(boundary).setType(MultipartBody.FORM);
                }else{
                    multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
                }

                Iterator iterator = paramsMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = (Map.Entry) iterator.next();
                    multipartBuilder.addFormDataPart((String) entry.getKey(), (String) entry.getValue());
                }



                if (oldParts != null && oldParts.size() > 0) {
                    for (MultipartBody.Part part : oldParts) {
                        multipartBuilder.addPart(part);
                    }
                }
                MultipartBody newMultipartBody = multipartBuilder.build();
                long newContentLength = newMultipartBody.contentLength();
                requestBuilder.post(newMultipartBody);
                updateHeader(headerBuilder,requestBuilder,newContentLength);
                request = requestBuilder.build();
            }

        }else{
            request = requestBuilder.build();
        }

        requestBuilder.headers(headerBuilder.build());

        return chain.proceed(request);
    }

    private void updateHeader(Headers.Builder headerBuilder, Request.Builder requestBuilder, long newContentLength){
        headerBuilder.set("Content-Length",String.valueOf(newContentLength));
        requestBuilder.headers(headerBuilder.build());
    }

    private boolean canInjectIntoBody(Request request) {
        if (request == null) {
            return false;
        }
        if (!TextUtils.equals(request.method(), "POST")) {
            return false;
        }
        RequestBody body = request.body();
        if (body == null) {
            return false;
        }
        MediaType mediaType = body.contentType();
        if (mediaType == null) {
            return false;
        }
        if (!TextUtils.equals(mediaType.subtype(), "x-www-form-urlencoded")) {
            return false;
        }
        return true;
    }

    // func to inject params into url
    private Request injectParamsIntoUrl(HttpUrl.Builder httpUrlBuilder, Request.Builder requestBuilder, Map<String, String> paramsMap) {
        if (paramsMap.size() > 0) {
            Iterator iterator = paramsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());
            }
            requestBuilder.url(httpUrlBuilder.build());
            return requestBuilder.build();
        }

        return null;
    }

    private static String bodyToString(final RequestBody request){
        try {
            final RequestBody copy = request;
            final Buffer buffer = new Buffer();
            if(copy != null)
                copy.writeTo(buffer);
            else
                return "";
            return buffer.readUtf8();
        }
        catch (final IOException e) {
            return "did not work";
        }
    }

    public static class Builder {

        BasicParamsInterceptor interceptor;

        public Builder() {
            interceptor = new BasicParamsInterceptor();
        }

        public Builder addParam(String key, String value) {
            interceptor.paramsMap.put(key, value);
            return this;
        }

        public Builder addParamsMap(Map<String, String> paramsMap) {
            interceptor.paramsMap.putAll(paramsMap);
            return this;
        }

        public Builder addHeaderParam(String key, String value) {
            interceptor.headerParamsMap.put(key, value);
            return this;
        }

        public Builder addHeaderParamsMap(Map<String, String> headerParamsMap) {
            interceptor.headerParamsMap.putAll(headerParamsMap);
            return this;
        }

        public Builder addHeaderLine(String headerLine) {
            int index = headerLine.indexOf(":");
            if (index == -1) {
                throw new IllegalArgumentException("Unexpected header: " + headerLine);
            }
            interceptor.headerLinesList.add(headerLine);
            return this;
        }

        public Builder addHeaderLinesList(List<String> headerLinesList) {
            for (String headerLine: headerLinesList) {
                int index = headerLine.indexOf(":");
                if (index == -1) {
                    throw new IllegalArgumentException("Unexpected header: " + headerLine);
                }
                interceptor.headerLinesList.add(headerLine);
            }
            return this;
        }

        public Builder addQueryParam(String key, String value) {
            interceptor.queryParamsMap.put(key, value);
            return this;
        }

        public Builder addQueryParamsMap(Map<String, String> queryParamsMap) {
            interceptor.queryParamsMap.putAll(queryParamsMap);
            return this;
        }

        public BasicParamsInterceptor build() {
            return interceptor;
        }

    }
}

使用方式:

private static OkHttpClient.Builder buildOkHttp()  {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.addInterceptor(createHttpLog());
        builder.addNetworkInterceptor(
                new BasicParamsInterceptor.Builder()
                        //-----------------------------header-----------------------
                        //添加手机型号
                        .addHeaderParam(ReqHeader.Key.HTTP_PHONETYPE, Build.MODEL)
                        .addHeaderParam(ReqHeader.Key.HTTP_PHONE_TYPE, "android")

                        //-----------------------------POST-----------------------
                        .addParam(ReqHeader.Key.HTTP_PHONETYPE, "android")
                        .build());
        return builder;
    }

注意点:如果通用参数是动态变化的,需要重新配置拦截器。

###4.总结
OkHttp通过拦截器让开发者更加灵活的扩展请求,实现个性化定制需求。前提是熟悉OkHttp项目。

END

–Nowy

–2019.02.11

分享到