我们在 提到了在开发中使用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) {
}
});
搞定,收工。
希望本文可以帮助到您,也希望各位不吝赐教,提出您在使用中的宝贵意见,谢谢。