您的位置 首页 java

Okhttp上传图片失败,居然是服务端的锅?(二)

我们在 提到了在开发中使用Okhttp上传图片失败的场景并给出了解决方法。

但是,我还是太天真了。

在写 这篇文章的时候,由于我们的项目工程支付模块接入了第三方的银行卡开户这个流程,需要客户端将用户所填写的身份证信息和身份证照片(需先拍照后上传至我们自己的服务端拿到对应的图片url)提交到第三方中进行开户,而我们的客户端的debug包又支持切换不同的服务端环境,自然地我们在内网和外网环境最终拿到的图片url就不同了。

开发好的迭代版本需要在不同的服务端环境中相应地测试回归,没问题后就可以发布线上版了,就是这再正常不过的流程,今天我又发现问题了。

出现问题的地方就是,我们的客户端在走第三方银行卡开户的时候,由于内网测试阶段拿到的身份证照片url对应的是内网的url,所以在进行开户的时候第三方就拿不到我们所传的身份证照片,所以每次测试的同事在内网环境需要走这个流程的时候,都要找到我们在代码层面单独对此场景的上传图片host写为外网的api域名,且我们的网络库的域名配置是整个项目工程全局的修改,所以每次改完打包给测试的同事后又要再改回来。

基于这个原因,所以我在写retrofit上传图片的时候考虑到了这点,在图片上传工具中支持在不影响全局上传图片api域名的配置上动态更换上传图片api域名,写这个工具的时候我把这个域名配置成了外网,然后一直就没改过,在一切顺利写好的时候,本着谨慎的态度,便在不同的环境下用测了这个上传工具。

结果,除了我们的外网环境可以成功上传图片外,内网环境、预发布环境、线上环境都用该工具上传图片均失败。

又到了到处排查的时间。

我们还是用charles来看看同个上传图片的请求在不同的环境的请求参数是否一致,最终发现除了内网环境外,其它环境的上传图片的请求对应的请求参数都一样(所以都失败是有一定道理的),截图如下:

我们看到在请求体里边也不存在我们在 提到的 Content-length 键值对,所以这里的MultipartBody写入的内容是一样的,可是为什么会上传失败呢?

我们看接口返回的response中msg文本提示“chunked request bodies not supported yet”,意思就是“不支持分块请求体”,基于我们的经验,肯定不是我们显式调用配置分块请求体的,最终还是让我们发现了请求体的端倪,问题出在“chunked”,那我们就找下有没有对应的chunked配置出现在请求体里边,果然,我们看上传图片失败的请求参数截图:

好家伙,请求header里边Transfer-Encoding:chunked,我们试着打断点去除这个Transfer-Encoding:chunked,结果,图片真的上传成功了,不可思议,我们又看回外网环境下的上传图片请求,发现请求header里边也是有Transfer-Encoding:chunked的,可是外网环境图片可以上传成功,所以我们猜测是因为不同api环境的服务端配置不同,果然,我们的服务端是永远滴神。

知道了问题后就好办了?既然不是我们显式调用的,那就先看看源码,看有没有可能找到解决问题的方法。

我们先找下往header写入Transfer-Encoding:chunked的地方在哪里,最终我们在okhttp自带的BridgeInterceptor拦截器发现了写入的代码,截图如下:

写入的条件是if条件不成立,即body.contentLength()值为-1,问题明朗了,我们在 为了解决服务器不支持我们往MultipartBody内容写入 Content-length 键值对,在自定义的RequestBody重写了contentLength()方法返回-1,所以BridgeInterceptor这个时候会往header写入Transfer-Encoding:chunked。

看到这里你会说我们可以在自定义拦截器中移除掉header的Transfer-Encoding键,但是这里不能这样做,原因就是okhttp自带的BridgeInterceptor拦截顺序在自定义拦截器之后,无论在自定义拦截器怎么改,最终BridgeInterceptor还是会写入Transfer-Encoding:chunked。

自带的BridgeInterceptor又不能修改,那我们只能想办法使得写入Transfer-Encoding的条件不成立,即body.contentLength()值不能为-1,所以我们的自定义RequestBody的contentLength()方法要返回file.length()。这个时候便不会有Transfer-Encoding的写入,我们用charles验证了不同api环境这个时候均可以成功上传图片。

那么在代码的层面我们还是要解决 提到的问题,同样的RequestBody和MultipartBody是源码,无法修改写入 Content-length 键值对的条件,源码如下:

如果可以使得以下代码不执行就好了:

 sink.writeUtf8("Content-Length: ")
                            .writeDecimalLong(contentLength)
                            .write(CRLF);  

源码无法更改,我们其实可以间接更改源码,先看下RequestBody和MultipartBody的源码,都是在okhttp3包下:

我们在项目工程的java下新建okhttp3包,然后把okhttp源码中的RequestBody和MultipartBody复制过来放到我们新建的okhttp3包下,然后修改下MultipartBody,将

 sink.writeUtf8("Content-Length: ")
                            .writeDecimalLong(contentLength)
                            .write(CRLF);  

注释掉。

这样再去上传图片的时候就可以了。

完善:

为了兼容可能存在的其它场景下上传图片需要写入 Content-length 键值对

1、修改RequestBody,新增个方法来动态控制是否写入 Content-length 键值对,方法截图如下:

2、修改MultipartBody响应RequestBody的修改,修改地方截图如下:

验证过在不同api环境下均可成功上传图片。

扩展:

对于看过 的,可以使用以下的上传图片工具类来上传图片,支持上传图片进度的回调、绑定id(view_id)发起的请求、绑定tag(页面)发起的请求,方便随时取消已发起的请求,比如

这种情况需要在对应的ImageView上蒙层显示进度,且可能有多个ImageView,点击对应的ImageView可重新上传,如正在上传的需要取消,退出页面时要取消所有的请求等等各种场景。

代码如下:

 /**
 * 作者:lpx on 2021/7/2 18:19
 * Email :1966353889@qq.com
 * Describe:配置上传接口请求服务
 * update on 2021/7/6 11:01
 */public interface UploadService {
    /**
     * 上传图片
     */    @Headers(Constant.Request.HOST_TYPE + ":" + Constant.Request.IMG)
    @Multipart
    @POST("uploadImage")
    Flowable<JsonObject> upLoadImage(@QueryMap Map<String, Object> map, @Part MultipartBody.Part file);
}  
 /**
 * 作者:lpx on 2021/7/5 15:43
 * Email :1966353889@qq.com
 * Describe:上传管理工具(暂用于上传图片)
 * update on 2021/7/9 14:13
 */public final class UploadManager {
    private static UploadManager mInstance;
    /**
     * 上传接口请求服务
     */    private UploadService apiService;
    /**
     * 存储绑定了对应id的某个请求(辅助实现自行或外部调用取消请求)
     */    private ConcurrentHashMap<Integer, UploadBody> requestMap;
    /**
     * 存储绑定了同个tag的所有请求,一般是某个页面(辅助实现自行或外部调用取消请求)
     */    private ConcurrentHashMap<String, ConcurrentHashMap<Integer, String>> sourceMap;

    private UploadManager() {
        requestMap = new ConcurrentHashMap<>();
        sourceMap = new ConcurrentHashMap<>();
    }

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

    /**
     * 上传图片
     */    public Disposable uploadImage(File file, OnUploadListener listener) {
        if (file == null) {
            throw new NullPointerException("file is null");
        }
        return getDisposable(null, file, listener);
    }

    /**
     * 上传图片
     *
     * @param id 标识id(一般指View的id)
     */    public void uploadImage(Integer id, File file, OnUploadListener listener) {
        if (file == null) {
            throw new NullPointerException("file is null");
        }
        if (id == null) {
            throw new NullPointerException("id is null");
        }
        remove(id);
        UploadBody uploadBody = new UploadBody.Builder()
                .disposable(getDisposable(id, file, listener))
                .listener(listener)
                .build();
        requestMap.put(id, uploadBody);
    }

    /**
     * 上传图片
     *
     * @param id 标识id(一般指View的id)
     */    public void uploadImage(String tag, Integer id, File file, OnUploadListener listener) {
        if (file == null) {
            throw new NullPointerException("file == null");
        }
        if (id == null) {
            throw new NullPointerException("id is null");
        }
        remove(id);
        UploadBody uploadBody = new UploadBody.Builder()
                .disposable(getDisposable(id, file, listener))
                .listener(listener)
                .build();
        ConcurrentHashMap<Integer, String> singleSourceMap = sourceMap.get(tag);
        if (singleSourceMap != null) {
            singleSourceMap.put(id, tag);
        } else {
            singleSourceMap = new ConcurrentHashMap<>();
            singleSourceMap.put(id, tag);
            sourceMap.put(tag, singleSourceMap);
        }
        requestMap.put(id, uploadBody);
    }

    /**
     * 程序退出时调用(因为可能存在多个打开的页面使用到UploadManager,所以仅退出页面时不建议调用此方法)
     */    public void disposeAll() {
        if (requestMap == null) {
            return;
        }
        for (Map.Entry<Integer, UploadBody> entry : requestMap.entrySet()) {
            UploadBody mCacheUploadBody = entry.getValue();
            if (mCacheUploadBody != null) {
                if (!mCacheUploadBody.mDisposable.isDisposed()) {
                    mCacheUploadBody.mDisposable.dispose();
                }
                if (mCacheUploadBody.mListener != null) {
                    mCacheUploadBody.mListener = null;
                }
            }
        }
        requestMap.clear();
        sourceMap.clear();
    }

    /**
     * 取消对应id(一般指View的id)发起的请求
     */    public void disposeById(Integer id) {
        if (requestMap == null || id == null) {
            return;
        }
        remove(id);
    }

    /**
     * 取消对应tag(一般包含一个或多个View的id)发起的请求
     */    public void disposeByTag(String tag) {
        if (sourceMap == null) {
            return;
        }
        ConcurrentHashMap<Integer, String> singleSourceMap = sourceMap.get(tag);
        if (singleSourceMap != null) {
            for (Map.Entry<Integer, String> entry : singleSourceMap.entrySet()) {
                remove(entry.getKey());
            }
            sourceMap.remove(tag);
        }
    }

    /**
     * 取消请求
     */    private void remove(Integer id) {
        if (requestMap == null || id == null) {
            return;
        }
        UploadBody mCacheUploadBody = requestMap.get(id);
        if (mCacheUploadBody != null) {
            if (!mCacheUploadBody.mDisposable.isDisposed()) {
                mCacheUploadBody.mDisposable.dispose();
            }
            if (mCacheUploadBody.mListener != null) {
                mCacheUploadBody.mListener = null;
            }
            requestMap.remove(id);
        }
    }

    private Disposable getDisposable(final Integer id, File file, final OnUploadListener listener) {
        Map<String, Object> map = new HashMap<>();
        map.put("watermark", listener != null && listener.watermark() ? 1 : 0);
        RequestBody requestBody = createRequestBody(MediaType.parse("image/jpeg"), file, listener);
        MultipartBody.Part body = MultipartBody.Part.createFormData("file"/*img*/, /*file.getName()*/"image.jpg", requestBody);
        UploadService uploadService;
        if (listener != null && !listener.isMatching()) {
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .readTimeout(20, TimeUnit.SECONDS)
                    .connectTimeout(20, TimeUnit.SECONDS)
                    .writeTimeout(20, TimeUnit.SECONDS)
                    .addInterceptor(new Interceptor() {
                        @Override
                        public okhttp3.Response intercept(Chain chain) throws IOException {
                            Request request = chain.request();
                            Request.Builder builder = request.newBuilder();
                            List<String> headerValues = request.headers("host_type");
                            if (headerValues.size() > 0) {
                                builder.removeHeader("host_type");
                                String headerValue = headerValues.get(0);
                                HttpUrl newBaseUrl;
                                if (!TextUtils.isEmpty(listener.hostUrl())) {
                                    newBaseUrl = HttpUrl.parse(listener.hostUrl());
                                } else {
                                    newBaseUrl = HttpUrl.parse(listener.getServer().getUrl(Integer.parseInt(headerValue)));
                                }
                                HttpUrl oldHttpUrl = request.url();
                                HttpUrl newFullUrl = oldHttpUrl.newBuilder().scheme(newBaseUrl.scheme()).host(newBaseUrl.host()).port(newBaseUrl.port()).build();
                                return chain.proceed(builder.url(newFullUrl).build());
                            } else {
                                return chain.proceed(builder.build());
                            }
                        }
                    })
                    .addInterceptor(new HeaderInterceptor())
                    .retryOnConnectionFailure(true)
                    .build();
            Retrofit retrofit = new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                   .baseUrl("xxxx")//这里替换为请求主机头地址
                    .client(okHttpClient)
                    .build();
            uploadService = retrofit.create(UploadService.class);
        } else {
            if (apiService == null) {
                OkHttpClient okHttpClient = new OkHttpClient.Builder()
                        .readTimeout(20, TimeUnit.SECONDS)
                        .connectTimeout(20, TimeUnit.SECONDS)
                        .writeTimeout(20, TimeUnit.SECONDS)
                        .addInterceptor(new HostInterceptor())
                        .addInterceptor(new HeaderInterceptor())
                        .retryOnConnectionFailure(true)
                        .build();
                Retrofit retrofit = new Retrofit.Builder()
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                        .baseUrl("xxxx")//这里替换为请求主机头地址
                        .client(okHttpClient)
                        .build();
                apiService = retrofit.create(UploadService.class);
            }
            uploadService = apiService;
        }
        return RxUtils.rx(uploadService.upLoadImage(map, body), new OnNextOnError<JsonObject>() {
            @Override
            public void onError(Response response) {
                if (id != null) {
                    UploadBody mCacheUploadBody = requestMap.get(id);
                    if (mCacheUploadBody != null) {
                        if (mCacheUploadBody.mDisposable != null && !mCacheUploadBody.mDisposable.isDisposed()) {
                            mCacheUploadBody.mDisposable.dispose();
                        }
                        requestMap.remove(id);
                    }
                }
                if (listener != null) {
                    listener.onFail(id, response.status, response.getMsg());
                }
            }

            @Override
            public void onNext(JsonObject jsonObject) {
                if (id != null) {
                    UploadBody mCacheUploadBody = requestMap.get(id);
                    if (mCacheUploadBody != null) {
                        if (mCacheUploadBody.mDisposable != null && !mCacheUploadBody.mDisposable.isDisposed()) {
                            mCacheUploadBody.mDisposable.dispose();
                        }
                        requestMap.remove(id);
                    }
                }
                if (listener != null) {
                    try {
                        JSONObject object = new JSONObject(jsonObject.toString());
                        if (object.optInt("state", 0) == 1) {
                            List<ImageBean> list = GsonTools.getData(jsonObject.toString(), ImageBean.class);
                            if (list != null && list.size() > 0) {
                                listener.onSuccess(id, list);
                            } else {
                                listener.onFail(id, -1, object.optString("msg", "上传图片失败,请重新上传(未返回图片远程地址)"));
                            }
                        } else {
                            listener.onFail(id, -1, object.optString("msg", "上传图片失败,请重新上传(state不等于1)"));
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                        listener.onFail(id, -1, "上传图片失败,请重新上传(state不等于1)");
                    }
                }
            }
        });

    }

    /**
     * 创建用于上传图片的RequestBody
     *
     * @param file 当前待上传的文件
     */    private RequestBody createRequestBody(final @Nullable MediaType contentType, final File file, final OnUploadListener listener) {
        return new RequestBody() {
            private long currentLength;

            @Override
            public @Nullable
            MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                return file.length();
            }

            @Override
            public void writeTo(@Nullable BufferedSink sink) throws IOException {
                ForwardingSink forwardingSink = new ForwardingSink(sink) {
                    final long totalLength = file.length();

                    @Override
                    public void write(Buffer source, long byteCount) throws IOException {
                        /*这里可以获取到写入的长度*/                        currentLength += byteCount;
                        /*回调进度*/                        if (listener instanceof OnProgressListener) {
                            ((OnProgressListener) listener).onProgress(totalLength, currentLength);
                        }
                        super.write(source, byteCount);
                    }
                };
                /*转一下*/                BufferedSink bufferedSink = Okio.buffer(forwardingSink);
                /*写数据*/                Source source = null;
                try {
                    source = Okio.source(file);
                    bufferedSink.writeAll(source);
                } finally {
                    Util.closeQuietly(source);
                }
                /*刷新一下数据*/                bufferedSink.flush();
            }
        };
    }

    /**
     * 上传监听
     */    public abstract static class OnUploadListener {
        /**
         * 上传成功
         *
         * @param id 对应view的id
         */        public abstract void onSuccess(Integer id, List<ImageBean> bean);

        /**
         * 上传失败
         *
         * @param id 对应view的id
         */        public abstract void onFail(Integer id, int status, String message);

        /**
         * 上传的图片是否加水印(默认为true,重写可覆盖)
         */        public boolean watermark() {
            return true;
        }

        /**
         * 上传的图片的请求Host对应的环境(默认跟随当前设置的环境,重写可覆盖)
         */        public String environment() {
            if (NetworkConfig.getInstance().getServer() instanceof IoServer) {
                return Constant.IO;
            } else if (NetworkConfig.getInstance().getServer() instanceof OrgServer) {
                return Constant.ORG;
            } else if (NetworkConfig.getInstance().getServer() instanceof PreServer) {
                return Constant.PRE;
            } else {
                return Constant.ORI;
            }
        }

        /**
         * 上传的图片的请求Host地址(优先使用这里设置的地址,重写可覆盖)
         */        public String hostUrl() {
            return null;
        }

        /**
         * 上传的图片的请求Host对应的环境(对应设置的environment)
         */        final BaseServer getServer() {
            String envi = environment();
            if (envi.equals(Constant.IO)) {
                return new IoServer();
            }
            if (envi.equals(Constant.ORG)) {
                return new OrgServer();
            }
            if (envi.equals(Constant.PRE)) {
                return new PreServer();
            }
            return new OriServer();
        }

        /**
         * 当前需要的的环境是否跟当前设置的环境匹配
         */        final boolean isMatching() {
            String envi = environment();
            if (envi.equals(Constant.IO) && NetworkConfig.getInstance().getServer() instanceof IoServer) {
                return true;
            }
            if (envi.equals(Constant.ORG) && NetworkConfig.getInstance().getServer() instanceof OrgServer) {
                return true;
            }
            if (envi.equals(Constant.PRE) && NetworkConfig.getInstance().getServer() instanceof PreServer) {
                return true;
            }
            if (envi.equals(Constant.ORI) && NetworkConfig.getInstance().getServer() instanceof OriServer) {
                return true;
            }
            return false;
        }
    }

    /**
     * 上传监听(批量上传)
     */    public abstract static class OnProgressListener extends OnUploadListener {
        /**
         * 上传进度回调
         *
         * @param totalLength   文件总大小(byte)
         * @param currentLength 当前已上传的文件大小(byte)
         */        public abstract void onProgress(long totalLength, long currentLength);
    }

    public static class UploadBody {
        private Disposable mDisposable;
        private OnUploadListener mListener;

        private UploadBody(Builder builder) {
            mDisposable = builder.mDisposable;
            mListener = builder.mListener;
        }

        public static class Builder {
            private Disposable mDisposable;
            private OnUploadListener mListener;

            public Builder disposable(Disposable disposable) {
                mDisposable = disposable;
                return this;
            }

            public Builder listener(OnUploadListener listener) {
                mListener = listener;
                return this;
            }

            public UploadBody build() {
                return new UploadBody(this);
            }
        }
    }
}  

使用示例:

 UploadManager.getInstance().uploadImage(upLoadFileDialog.getViewId(), file, new UploadManager.OnProgressListener() {
                                @Override
                                public String environment() {
                                   /*动态配置环境,默认是跟随项目工程api环境*/                                    return Constant.ORG;
                                }
                                @Override
                                public void onSuccess(Integer id, List<ImageBean> bean) {
                                    
                                }

                                @Override
                                public void onFail(Integer id, int status, String message) {
                                    
                                }

                                @Override
                                public void onProgress(long totalLength, long currentLength) {
                                    
                                }
                            });  

搞定,收工。

希望本文可以帮助到您,也希望各位不吝赐教,提出您在使用中的宝贵意见,谢谢。

文章来源:智云一二三科技

文章标题:Okhttp上传图片失败,居然是服务端的锅?(二)

文章地址:https://www.zhihuclub.com/174659.shtml

关于作者: 智云科技

热门文章

网站地图