使用Retrofit 2.0上传包括图片在内的多部分表单数据

199

我正在尝试使用Retrofit 2.0进行 HTTP POST 请求到服务器

MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
MediaType MEDIA_TYPE_IMAGE = MediaType.parse("image/*");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    imageBitmap.compress(Bitmap.CompressFormat.JPEG,90,byteArrayOutputStream);
profilePictureByte = byteArrayOutputStream.toByteArray();

Call<APIResults> call = ServiceAPI.updateProfile(
        RequestBody.create(MEDIA_TYPE_TEXT, emailString),
        RequestBody.create(MEDIA_TYPE_IMAGE, profilePictureByte));

call.enqueue();

服务器返回一个错误,说这个文件无效。

这很奇怪,因为我曾经使用其他库在iOS上尝试过以相同格式上传同样的文件,但上传成功了。

我想知道使用Retrofit 2.0上传图片的正确方式是什么?

我应该先将其保存到磁盘再上传吗?

P.S.:我已经使用Retrofit进行了其他Multipart请求,它们都成功完成了。问题出在当我试图将字节包含在请求体中时。


https://github.com/square/retrofit/issues/1217 - Raghunandan
http://square.github.io/retrofit/2.x/retrofit/retrofit2/http/Query.html - Daniel Viglione
13个回答

223

使用Retrofit 2上传文件并携带文件名,有一种正确的方式,不需要任何hack

定义API接口:

@Multipart
@POST("uploadAttachment")
Call<MyResponse> uploadAttachment(@Part MultipartBody.Part filePart); 
                                   // You can add other parameters too

上传文件的方式如下:

File file = // initialize file here

MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), RequestBody.create(MediaType.parse("image/*"), file));

Call<MyResponse> call = api.uploadAttachment(filePart);

这只展示了文件上传,你也可以使用@Part注解在同一方法中添加其他参数。


4
我们如何使用MultipartBody.Part发送多个文件? - Praveen Sharma
1
我需要使用“image []”作为键来发送图像集合。我尝试过@Part("images[]") List<MultipartBody.Part> images,但它会出现错误,提示“使用MultipartBody.Part的@Part参数不应包括部件名称”。 - Praveen Sharma
3
如何在多部分中添加密钥。 - andro
这应该被标记为正确答案,因为它使用更少和更简单的代码完成了工作。 - Farrukh Najmi
1
@jimmy0251 可以帮我解决这个问题 @Part("channel[comment][image]")。我需要上传图片到这个 channel[comment][image] 键值中,你知道该怎么做吗? - Rahul sharma
显示剩余12条评论

209
我将在1.9版和2.0版中突出解决方案,因为它对某些人很有用。
在1.9版中,我认为更好的解决方法是将文件保存到磁盘并将其用作类型化文件,如下所示:

RetroFit 1.9

(我不知道您的服务器端实现)是否有类似于此的API接口方法。
@POST("/en/Api/Results/UploadFile")
void UploadFile(@Part("file") TypedFile file,
                @Part("folder") String folder,
                Callback<Response> callback);

并像这样使用它

TypedFile file = new TypedFile("multipart/form-data",
                                       new File(path));

对于RetroFit 2,请使用以下方法

RetroFit 2.0(这是一个解决RetroFit 2中已经修复的问题的变通方法,正确的方法请参考jimmy0251的答案

API接口:

public interface ApiInterface {

    @Multipart
    @POST("/api/Accounts/editaccount")
    Call<User> editUser(@Header("Authorization") String authorization,
                        @Part("file\"; filename=\"pp.png\" ") RequestBody file,
                        @Part("FirstName") RequestBody fname,
                        @Part("Id") RequestBody id);
}

使用方法如下:

File file = new File(imageUri.getPath());

RequestBody fbody = RequestBody.create(MediaType.parse("image/*"),
                                       file);

RequestBody name = RequestBody.create(MediaType.parse("text/plain"),
                                      firstNameField.getText()
                                                    .toString());

RequestBody id = RequestBody.create(MediaType.parse("text/plain"),
                                    AZUtils.getUserId(this));

Call<User> call = client.editUser(AZUtils.getToken(this),
                                  fbody,
                                  name,
                                  id);

call.enqueue(new Callback<User>() {

    @Override
    public void onResponse(retrofit.Response<User> response,
                           Retrofit retrofit) {

        AZUtils.printObject(response.body());
    }

    @Override
    public void onFailure(Throwable t) {

        t.printStackTrace();
    }
});

5
是的,我认为这是一个关于Retrofit 2.0的问题(https://github.com/square/retrofit/issues/1063),你可能需要继续使用1.9版本。 - insomniac
2
看我的编辑,我还没有尝试过它,欢迎你去试试。 - insomniac
1
我已经成功使用 Retrofit 2.0 示例上传了一张图片。 - jerogaren
3
你可以将接口更改为 @Multipart @POST("/api/Accounts/editaccount") Call<User> editUser(@PartMap Map<String, RequestBody> params); 当你有文件时,使用以下代码: Map<String, RequestBody> map = new HashMap<>(); RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file); map.put("file\"; filename=\"" + file.getName(), fileBody); - insomniac
2
@insomniac 是的,我刚刚发现了这个,也可以使用 MultiPartBody.Part - Bhargav
显示剩余9条评论

25

我在注册用户时使用了Retrofit 2.0,可以从注册账户中发送multipart/form文件图像和文本。

在我的RegisterActivity中,使用了一个AsyncTask。

//AsyncTask
private class Register extends AsyncTask<String, Void, String> {

    @Override
    protected void onPreExecute() {..}

    @Override
    protected String doInBackground(String... params) {
        new com.tequilasoft.mesasderegalos.dbo.Register().register(txtNombres, selectedImagePath, txtEmail, txtPassword);
        responseMensaje = StaticValues.mensaje ;
        mensajeCodigo = StaticValues.mensajeCodigo;
        return String.valueOf(StaticValues.code);
    }

    @Override
    protected void onPostExecute(String codeResult) {..}

在我的Register.java类中,我使用了Retrofit进行同步调用。

import android.util.Log;
import com.tequilasoft.mesasderegalos.interfaces.RegisterService;
import com.tequilasoft.mesasderegalos.utils.StaticValues;
import com.tequilasoft.mesasderegalos.utils.Utilities;
import java.io.File;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call; 
import retrofit2.Response;
/**Created by sam on 2/09/16.*/
public class Register {

public void register(String nombres, String selectedImagePath, String email, String password){

    try {
        // create upload service client
        RegisterService service = ServiceGenerator.createUser(RegisterService.class);

        // add another part within the multipart request
        RequestBody requestEmail =
                RequestBody.create(
                        MediaType.parse("multipart/form-data"), email);
        // add another part within the multipart request
        RequestBody requestPassword =
                RequestBody.create(
                        MediaType.parse("multipart/form-data"), password);
        // add another part within the multipart request
        RequestBody requestNombres =
                RequestBody.create(
                        MediaType.parse("multipart/form-data"), nombres);

        MultipartBody.Part imagenPerfil = null;
        if(selectedImagePath!=null){
            File file = new File(selectedImagePath);
            Log.i("Register","Nombre del archivo "+file.getName());
            // create RequestBody instance from file
            RequestBody requestFile =
                    RequestBody.create(MediaType.parse("multipart/form-data"), file);
            // MultipartBody.Part is used to send also the actual file name
            imagenPerfil = MultipartBody.Part.createFormData("imagenPerfil", file.getName(), requestFile);
        }

        // finally, execute the request
        Call<ResponseBody> call = service.registerUser(imagenPerfil, requestEmail,requestPassword,requestNombres);
        Response<ResponseBody> bodyResponse = call.execute();
        StaticValues.code  = bodyResponse.code();
        StaticValues.mensaje  = bodyResponse.message();
        ResponseBody errorBody = bodyResponse.errorBody();
        StaticValues.mensajeCodigo  = errorBody==null
                ?null
                :Utilities.mensajeCodigoDeLaRespuestaJSON(bodyResponse.errorBody().byteStream());
        Log.i("Register","Code "+StaticValues.code);
        Log.i("Register","mensaje "+StaticValues.mensaje);
        Log.i("Register","mensajeCodigo "+StaticValues.mensaje);
    }
    catch (Exception e){
        e.printStackTrace();
    }
}
}
在RegisterService的接口中。
public interface RegisterService {
@Multipart
@POST(StaticValues.REGISTER)
Call<ResponseBody> registerUser(@Part MultipartBody.Part image,
                                @Part("email") RequestBody email,
                                @Part("password") RequestBody password,
                                @Part("nombre") RequestBody nombre
);
}

用于解析 InputStream 响应的实用程序

public class Utilities {
public static String mensajeCodigoDeLaRespuestaJSON(InputStream inputStream){
    String mensajeCodigo = null;
    try {
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    inputStream, "iso-8859-1"), 8);
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
        }
        inputStream.close();
        mensajeCodigo = sb.toString();
    } catch (Exception e) {
        Log.e("Buffer Error", "Error converting result " + e.toString());
    }
    return mensajeCodigo;
}
}

22

更新Retrofit2.0中的图片上传代码

public interface ApiInterface {

    @Multipart
    @POST("user/signup")
    Call<UserModelResponse> updateProfilePhotoProcess(@Part("email") RequestBody email,
                                                      @Part("password") RequestBody password,
                                                      @Part("profile_pic\"; filename=\"pp.png")
                                                              RequestBody file);
}

MediaType.parse("image/*")更改为MediaType.parse("image/jpeg")

RequestBody reqFile = RequestBody.create(MediaType.parse("image/jpeg"),
                                         file);
RequestBody email = RequestBody.create(MediaType.parse("text/plain"),
                                       "upload_test4@gmail.com");
RequestBody password = RequestBody.create(MediaType.parse("text/plain"),
                                          "123456789");

Call<UserModelResponse> call = apiService.updateProfilePhotoProcess(email,
                                                                    password,
                                                                    reqFile);
call.enqueue(new Callback<UserModelResponse>() {

    @Override
    public void onResponse(Call<UserModelResponse> call,
                           Response<UserModelResponse> response) {

        String
                TAG =
                response.body()
                        .toString();

        UserModelResponse userModelResponse = response.body();
        UserModel userModel = userModelResponse.getUserModel();

        Log.d("MainActivity",
              "user image = " + userModel.getProfilePic());

    }

    @Override
    public void onFailure(Call<UserModelResponse> call,
                          Throwable t) {

        Toast.makeText(MainActivity.this,
                       "" + TAG,
                       Toast.LENGTH_LONG)
             .show();

    }
});

我尝试了很多方法,但是都没有得到结果。就像你说的那样,我只是改变了这个("Change MediaType.parse("image/*") to MediaType.parse("image/jpeg")"),现在它可以工作了,非常感谢你。 - Gunnar
希望我能给你更多的投票,谢谢。 - Rohit Maurya
如果您的API使用了@Multipart注释,则@Part注释必须提供名称,或使用MultipartBody.Part参数类型。 - Rohit
好的解决方案!还有一个引用在@Part("profile_pic"; filename="pp.png" "中,应该是@Part("profile_pic\"; filename=\"pp.png " - Ninja

21

完成您的任务非常简单。您只需要按照以下步骤进行:

1. 第一步

public interface APIService {  
    @Multipart
    @POST("upload")
    Call<ResponseBody> upload(
        @Part("item") RequestBody description,
        @Part("imageNumber") RequestBody description,
        @Part MultipartBody.Part imageFile
    );
}

你需要将整个调用设置为@Multipart请求itemimage number只是包含在RequestBody中的字符串体。我们使用MultipartBody.Part类,它允许我们向请求发送实际文件名和二进制文件数据。

2. 第二步

  File file = (File) params[0];
  RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);

  MultipartBody.Part body =MultipartBody.Part.createFormData("Image", file.getName(), requestBody);

  RequestBody ItemId = RequestBody.create(okhttp3.MultipartBody.FORM, "22");
  RequestBody ImageNumber = RequestBody.create(okhttp3.MultipartBody.FORM,"1");
  final Call<UploadImageResponse> request = apiService.uploadItemImage(body, ItemId,ImageNumber);

现在你有一个图像路径,需要将其转换为文件。使用方法RequestBody.create(MediaType.parse("multipart/form-data"), file)文件转换为RequestBody。现在,您需要使用方法MultipartBody.Part.createFormData("Image", file.getName(), requestBody);RequestBody requestFile转换为MultipartBody.Part

ImageNumberItemId是我需要发送到服务器的另一组数据,因此我也将两个值转换为RequestBody

更多信息请参见


15

在@insomniac给出的答案上补充一点。您可以创建一个Map来放置RequestBody的参数,包括图像。

接口代码

public interface ApiInterface {
@Multipart
@POST("/api/Accounts/editaccount")
Call<User> editUser (@Header("Authorization") String authorization, @PartMap Map<String, RequestBody> map);
}

Java类的代码

File file = new File(imageUri.getPath());
RequestBody fbody = RequestBody.create(MediaType.parse("image/*"), file);
RequestBody name = RequestBody.create(MediaType.parse("text/plain"), firstNameField.getText().toString());
RequestBody id = RequestBody.create(MediaType.parse("text/plain"), AZUtils.getUserId(this));

Map<String, RequestBody> map = new HashMap<>();
map.put("file\"; filename=\"pp.png\" ", fbody);
map.put("FirstName", name);
map.put("Id", id);
Call<User> call = client.editUser(AZUtils.getToken(this), map);
call.enqueue(new Callback<User>() {
@Override
public void onResponse(retrofit.Response<User> response, Retrofit retrofit) 
{
    AZUtils.printObject(response.body());
}

@Override
public void onFailure(Throwable t) {
    t.printStackTrace();
 }
});

如何使用2个字符串上传多个文件? - Jay Dangar
你能否回答这个问题:https://stackoverflow.com/questions/60428238/upload-file-using-both-multipart-and-json-key-value-pairs-with-retrofit2 - Ranjit

7
在Kotlin中,使用toMediaTypeasRequestBodytoRequestBody的扩展方法非常容易。以下是一个例子:
我在这里使用multipart上传一个PDF文件和一个图片文件以及一些正常的字段。
这是使用Retrofit声明API:
    @Multipart
    @POST("api/Lesson/AddNewLesson")
    fun createLesson(
        @Part("userId") userId: RequestBody,
        @Part("LessonTitle") lessonTitle: RequestBody,
        @Part pdf: MultipartBody.Part,
        @Part imageFile: MultipartBody.Part
    ): Maybe<BaseResponse<String>>

以下是如何实际调用它的方法:

api.createLesson(
            userId.toRequestBody("text/plain".toMediaType()),
            lessonTitle.toRequestBody("text/plain".toMediaType()),
            startFromRegister.toString().toRequestBody("text/plain".toMediaType()),
            MultipartBody.Part.createFormData(
                "jpeg",
                imageFile.name,
                imageFile.asRequestBody("image/*".toMediaType())
            ),
            MultipartBody.Part.createFormData(
                "pdf",
                pdfFile.name,
                pdfFile.asRequestBody("application/pdf".toMediaType())
            )

但是认证/令牌呢? - Isma Rekathakusuma

4
* Return MultipartBody from file path

 public static MultipartBody.Part generateFileBody(String imagePath)
    {
        File file = new File(imagePath);
        RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        return MultipartBody.Part.createFormData("mediaProfilePic", file.getName(), requestFile);
    }

3
使用Retrofit上传文件非常简单。您需要构建自己的API接口,如下所示:
public interface Api {

    String BASE_URL = "http://192.168.43.124/ImageUploadApi/";


    @Multipart
    @POST("yourapipath")
    Call<MyResponse> uploadImage(@Part("image\"; filename=\"myfile.jpg\" ") RequestBody file, @Part("desc") RequestBody desc);

}

在上面的代码中,image 是关键名称,因此如果您使用的是 PHP,则将编写 $_FILES['image']['tmp_name'] 以获取此内容。而 filename="myfile.jpg" 则是正在发送请求的文件的名称。
现在,要上传文件,您需要一种方法,该方法将从 Uri 中为您提供绝对路径。
private String getRealPathFromURI(Uri contentUri) {
    String[] proj = {MediaStore.Images.Media.DATA};
    CursorLoader loader = new CursorLoader(this, contentUri, proj, null, null, null);
    Cursor cursor = loader.loadInBackground();
    int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
    cursor.moveToFirst();
    String result = cursor.getString(column_index);
    cursor.close();
    return result;
}

现在,您可以使用以下代码来上传文件。
 private void uploadFile(Uri fileUri, String desc) {

        //creating a file
        File file = new File(getRealPathFromURI(fileUri));

        //creating request body for file
        RequestBody requestFile = RequestBody.create(MediaType.parse(getContentResolver().getType(fileUri)), file);
        RequestBody descBody = RequestBody.create(MediaType.parse("text/plain"), desc);

        //The gson builder
        Gson gson = new GsonBuilder()
                .setLenient()
                .create();


        //creating retrofit object
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Api.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

        //creating our api 
        Api api = retrofit.create(Api.class);

        //creating a call and calling the upload image method 
        Call<MyResponse> call = api.uploadImage(requestFile, descBody);

        //finally performing the call 
        call.enqueue(new Callback<MyResponse>() {
            @Override
            public void onResponse(Call<MyResponse> call, Response<MyResponse> response) {
                if (!response.body().error) {
                    Toast.makeText(getApplicationContext(), "File Uploaded Successfully...", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getApplicationContext(), "Some error occurred...", Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void onFailure(Call<MyResponse> call, Throwable t) {
                Toast.makeText(getApplicationContext(), t.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
    }

如需更详细的解释,您可以访问此Retrofit上传文件教程


这是一个hack,已经在Retrofit 2.0中修复了一段时间。请参见下面jimmy0251的答案。 - Matt Wolfe

2

requestBody可以用于上传数据

  val body: RequestBody = Builder().setType(MultipartBody.FORM)
        .addFormDataPart(
            "file", "<image name you wish to give>",
            RequestBody.create(
                MediaType.parse("application/octet-stream"),
                File(path)
            )
        )
        .build()
uploadProfilePhoto(body)

然后像这样调用:
   @POST("/**")
    suspend fun uploadProfilePhoto(
        @Body body: RequestBody,
    ): ResponseBody
}

okhttp3.MultipartBody.Builder() 的构造器。 - Maher Abuthraa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接