最流行的网络请求框架Rxjava2+Retrofit之文件上传

转自:https://blog.csdn.net/qq_20521573/article/details/78356747

  • 使用Retrofit上传文件时遇到的坑
  • 实现单文件上传
  • 实现多文件上传

一、使用Retrofit上传文件时遇到的坑。项目中注册接口中有上传头像的功能,本以为上传头像是一个很简单的事情,可万万没想到使用Retrofit上传头像时却遇到了一个大坑。填这个坑花费了足足两天的时间。先来看下上传文件时遇到的异常信息:

java.lang.IllegalArgumentException: Invalid % sequence at 432: --8c46470b-77e7-4dd8-
b4e5-55f6968db182
Content-Disposition: form-data; name="phone"
Content-Length: 11

1551526
--8c46470b-77e7-4dd8-b4e5-55f6968db182
Content-Disposition: form-data; name="password"
Content-Length: 6

123123
--8c46470b-77e7-4dd8-b4e5-55f6968db182
Content-Disposition: form-data; name="uploadFile"; filename="test.txt"
Content-Type: multipart/form-data
Content-Length: 27

Hello World %

--8c46470b-77e7-4dd8-b4e5-55f6968db182--
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

分析上面的日志可以知道,错误的原因是因为文件中存在非法的参数“%”。什么情况啊,难道上传文件还不能包含“%”了这不科学啊。况且试验了图片上传每张图片流中都包含%。也就是所有图片上传均失败。于是自己又写了一个test.txt 
的文件放到了手机里边测试,当test.txt文件中写入Hello World时候测试上传没有 
异常,但当我在Hello World末尾加了%后就出现了java.lang.IllegalArgumentException: Invalid % sequence at 432 
这个异常!刚开始的时候以为是自己上传文件部分代码写的有问题。于是参考网上的代码试了各种上传方法均无济于事。接着开始百度看网上是否有类似问题,但是几乎搜遍了整个百度也没有找到问题的解决方案。后来转战谷歌,强大的谷歌上也没有找到解决办法!无奈之下只好自己调了,于是开始逐一排查自己封装的代码是否有问题。经过不懈的努力终于找到了问题所在。请看下面代码

private IdeaApi() {
        //   日志拦截器
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor((message) -> {

            try {
                String text = URLDecoder.decode(message, "utf-8");
                LogUtils.e("OKHttp-----", text);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                LogUtils.e("OKHttp-----", message);
            }
        });
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        File cacheFile = new File(Utils.getContext().getCacheDir(), "cache");
        Cache cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb

        LogUtils.e(UserInfoTools.getUserId(Utils.getContext()));
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(IdeaApiService.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .connectTimeout(IdeaApiService.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .addNetworkInterceptor(new HttpCacheInterceptor())
                .cache(cache)
                .addInterceptor((chain) -> {     //  统一配置配置请求头
                    Request request = chain.request().newBuilder()
                            .addHeader("user_id", UserInfoTools.getUserId(Utils.getContext()))
                            .addHeader("psw_just_test", "4567")
                            .build();
                    return chain.proceed(request);
                })
                .addInterceptor(loggingInterceptor)
                .build();


        Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().create();

        Retrofit retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(IdeaApiService.API_SERVER_URL)
                .build();
        service = retrofit.create(IdeaApiService.class);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

这段代码是对Retrofit进行封装并添加了拦截器,当我把 .addInterceptor(loggingInterceptor)这一行注释掉以后,奇迹出现了。点击注册后竟然神奇般的注册成功了!(发现了问题所在,兴奋至极啊,真的差一点就放弃了!)注意这行代码,是为Retrofit添加了日志拦截器!继续跟进代码看这个拦截器的内容,经过调试发现竟然是因为HttpLoggingInterceptor 中setLevel 
的一行代码导致的,即loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 
这行代码。于是试了试把BOEY改成了HEADERS后再试测发现这个异常就没有再出现了!但是改过之后日志拦截器会有些问题,就是只能拦截请求头的日志,请求体中的日志无法拦截,其实已经失去了拦截日志的意义了!但是这个问题终归解决了。只需要改一个单词或者去掉日志拦截器!修改后是这样的:loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);

二 、实现单文件上传的两种方法 
解决了上面的异常后心里一下再踏实了好多!于是接下来看看如何使用Retrofit来上传文件把!假如说我们注册时接口有三个参数:手机号、密码、头像。

1.单文件上传方法(1)

首先定义注册接口方法:

    @Multipart
    @POST("user/register.do")
    Observable<BasicResponse<RegisterBean>> register(@Part("phone") RequestBody phone,@Part("password") RequestBody password,@Part MultipartBody.Part image);
  • 1
  • 2
  • 3
  • 4

注意上面上面方法加了@Multipart的注解。对于上传文件必须要加这个注解,不然会报异常!另外方法中有三个参数,即我们注册时所需要的参数!返回值是我我们自定义的Observable(详见上一片文章),接下来在我们注册的页面调用这个方法,如下:

 File file = new File(picPath);
 //  图片参数
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        MultipartBody.Part body = MultipartBody.Part.createFormData("uploadFile", file.getName(), requestFile);
 //  手机号参数
RequestBody phoneBody = RequestBody.create(MediaType.parse("multipart/form-data"), phone);
//  密码参数
RequestBody pswBody = RequestBody.create(MediaType.parse("multipart/form-data"), password);

IdeaApi.getApiService()
                .register(phoneBody,pswBody,body)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<BasicResponse<RegisterBean>>(this, true) {
                    @Override
                    public void onSuccess(BasicResponse<RegisterBean> response) {
                        EventBus.getDefault().post(new RegisterSuccess("register success"));
                        showToast("注册成功,请登陆");
                        finish();
                    }
                });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

显然,上面的方法有个弊端。当我们接口中需要的参数较少时使用上面的方法无可厚非。但假如说我们接口中需要的参数非常多,那么上面的方法使用起来就麻烦了。因此接下来我们可以看下第二种方法如何使用。

2.单文件上传方法(2),同样以注册接口为例,先定义注册接口的方法:

    @Multipart
    @POST("user/register.do")
    Observable<BasicResponse<RegisterBean>> register(@Part List<MultipartBody.Part> partList);
  • 1
  • 2
  • 3

可以发现方法中的参数变成了List《MultipartBody.Part》的集合。这样所有的参数我们只需要放到这个集合里边即可!是不是方便了很多?所以推荐使用这种写法!接下来看注册页面如何调用这个方法:

扫描二维码关注公众号,回复: 2364500 查看本文章
File file = new File(picPath);
        RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("phone", phone)
                .addFormDataPart("password", password)
                .addFormDataPart("uploadFile", file.getName(), imageBody);
        List<MultipartBody.Part> parts = builder.build().parts();

        IdeaApi.getApiService()
                .register(parts)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<BasicResponse<RegisterBean>>(this, true) {
                    @Override
                    public void onSuccess(BasicResponse<RegisterBean> response) {
                        EventBus.getDefault().post(new RegisterSuccess("register success"));
                        showToast("注册成功,请登陆");
                        finish();
                    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这样是不是比第一种方法清爽了很多呢!

三、 实现多文件上传。 
对于多图上传其实跟单文件上传没有多大区别,只不过多了些参数而已。跟但图片类似同样可以有两种方法。 
1.多文件传方法(1): 
首先定义多文件上传接口:

    @POST("upload/")
    Observable<BasicResponse> uploadFiles(@Part("filename") String description,
                                   @Part("pic\"; filename=\"image1.png") RequestBody imgs1,
                                   @Part("pic\"; filename=\"image2.png") RequestBody imgs2,);
  • 1
  • 2
  • 3
  • 4

调用接口上传图片:

       File file = new File(picPath);
       RequestBody requestFile1 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
       MultipartBody.Part body = MultipartBody.Part
       .createFormData("uploadFile", file.getName(), requestFile);

        RequestBody requestFile2 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
       MultipartBody.Part body = MultipartBody.Part
       .createFormData("uploadFile", file.getName(), requestFile);

        IdeaApi.getApiService()
                .uploadFiles("pictures",requestFile1,requestFile2 )
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<BasicResponse<RegisterBean>>(this, true) {
                    @Override
                    public void onSuccess(BasicResponse<RegisterBean> response) {
                        EventBus.getDefault().post(new RegisterSuccess("register success"));
                        showToast("注册成功,请登陆");
                        finish();
                    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

或者使用第二种方法实现多文件上传,如下: 
1.多文件上传方法(2),采用map集合来存放多个图片RequestBody参数。 
首先定义多文件上传接口

  @POST()
  Observable<BasicResponse> uploadFiles(
        @Part("filename") String description,
        @PartMap() Map<String, RequestBody> maps);
  • 1
  • 2
  • 3
  • 4

然后调用接口实现多文件上传

    File file = new File(picPath);
    RequestBody requestFile1 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
    MultipartBody.Part body = MultipartBody.Part
            .createFormData("uploadFile", file.getName(), requestFile);

    RequestBody requestFile2 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
    MultipartBody.Part body = MultipartBody.Part
            .createFormData("uploadFile", file.getName(), requestFile);
    Map<String,RequestBody> map=new HashMap<>();
    map.put("文件1",requestFile1 );
    map.put("文件2",requestFile2 );

    IdeaApi.getApiService()
                .uploadFiles("pictures",map)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<BasicResponse<RegisterBean>>(this, true) {
                    @Override
                    public void onSuccess(BasicResponse<RegisterBean> response) {
                        EventBus.getDefault().post(new RegisterSuccess("register success"));
                        showToast("注册成功,请登陆");
                        finish();
                    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

至此关于Retrofit上传文件已经基本完成。但由于多文件上传并没有实际验证,因此如果存在问题请留言告知。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_20521573/article/details/78356747

猜你喜欢

转载自blog.csdn.net/huanglei201502/article/details/81185950