如何在Retrofit2 Android中解析多部分表单数据请求的响应

3

我正在使用 Retrofit 2 创建一个多部分 form-data 请求,它可以正常工作并且服务器响应为 200。但是我在解析响应时遇到了问题。这是我的代码:

@POST("sync/mediaUpload")
@Multipart
Call<ResponseBody> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);


public Response<ResponseBody> uploadMediaFile(String token, String userId, File file, String fileName, String fileId, String hash) {

    MediaService service = retrofit.create(MediaService.class);
    MultipartBody.Part fileBody = prepareFilePart("file", file);
    RequestBody userIdBody = RequestBody.create(MediaType.parse("text/plain"), userId);
    RequestBody fileNameBody = RequestBody.create(MediaType.parse("text/plain"), fileName);
    RequestBody fileIdBody = RequestBody.create(MediaType.parse("text/plain"), fileId);
    RequestBody hashBody = RequestBody.create(MediaType.parse("text/plain"), hash);
    Call<ResponseBody> call = service.uploadMediaFile(token, userIdBody, txIdBody, transIdBody, stepCodeBody,
            fileBody, fileNameBody, fileIdBody, hashBody);
    try {
        return call.execute();
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

 @NonNull
private MultipartBody.Part prepareFilePart(String partName, File file) {
    RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file);
    return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

当文件上传正确时,服务器会返回一个Json对象。例如:

{
    "fileName": "IMG_20190108_183751.jpg",
    "fileId": "0",
    "fileSizeInBytes": 216067
}

但是,在call.execute()中,Retrofit返回:

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileName"
IMG_20190108_183751.jpg

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileId"
0

--MultipartDataMediaFormatterBoundary1q2w3e
Content-Disposition: form-data; name="FileSizeInBytes"
216067

我该如何解析这个响应? 我尝试更改Retrofit服务的签名,使用一个对象而不是ResponseBody:
@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);

我的对象
public class MediaUploadResponse {

public final String fileName;
public final String fileId;
public final long fileSizeInBytes;

    public MediaUploadResponse(String fileName, String fileId, long 
    fileSizeInBytes) {
        this.fileName = fileName;
        this.fileId = fileId;
        this.fileSizeInBytes = fileSizeInBytes;
    }
}

但是Retrofit抛出了MalformedJsonException异常

有人知道如何解决吗?

谢谢。

2个回答

3
我将给你的答案将使用Gson streaming(用于速度)和okhttp3。请记住,这段代码目前没有经过测试。它是为了向您展示该做什么的想法而编写的。我从我的一个正在运行的应用程序中获取了它(这个想法已经实现并且正在工作)。它可能看起来像是一种过度杀伤力。如果由于模糊点而有其他问题,请在下面留言。
1- 使用retrofit设置GSON:
package whatever.package.you.want;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

public class DataService {
    private static Retrofit retrofit = null;
    private static final int CONNECTION_TIMEOUT = 45;//s
    private static final int READ_TIMEOUT = 45;//s
    private static final int WRITE_TIMEOUT = 45;//s
    private static final String MEDIA_TYPE = "application/json";//"multipart/form-data"; //"text/plain";
    private static final DATA_SERVICE_BASE_URL = "https://stackoverflow.com"; // your desired URL
    //I suppose you have your custom declarations here

    public static Retrofit getClient(String yourURL) {

        Gson gson = new GsonBuilder()
                .setLenient()
                .setPrettyPrinting()
                .create();

        //https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
        /*HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);*/

        final OkHttpClient client = new OkHttpClient.Builder()
                /*.addInterceptor(logging)*/
                .connectTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
                .retryOnConnectionFailure(false)
                .build();

        if (retrofit==null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(yourURL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create(gson)) //https://github.com/square/retrofit/tree/master/retrofit-converters/gson
                    .addConverterFactory(ScalarsConverterFactory.create()) //https://github.com/square/retrofit/tree/master/retrofit-converters/scalars
                    .build();
        }
        return retrofit;
    }

    public static DataService getUserDataService() {
        return getClient(DATA_SERVICE_BASE_URL).create(UserDataServiceInterface.class);
    }
}

2- 您的模型 MediaUploadResponse.class

package whatever.package.you.want;

import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;


@JsonAdapter(MediaUploadResponseAdapter.class)
public class MediaUploadResponse {

    @SerializedName("fileName")
    private String fileName = "";

    @SerializedName("fileId")
    private String fileID = "";

    @SerializedName("fileSizeInBytes")
    private long fileSizeInBytes = "";


    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileId() {
        return fileId;
    }

    public void setFileId(String fileId) {
        this.fileId = fileId;
    }

    public String getFileSizeInBytes() {
        return fileSizeInBytes;
    }

    public void setFileSizeInBytes(long fileSizeInBytes) {
        this.fileSizeInBytes = fileSizeInBytes;
    }
}

3- 模型的适配器 MediaUploadResponseAdapter.class,用于序列化和反序列化:
package whatever.package.you.want;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import whatever.MediaUploadResponse;
import whatever.JsonAdapterUtils;

import java.io.IOException;


public class MediaUploadResponseAdapter extends BaseJsonAdapter<MediaUploadResponse> {

    @Override
    public MediaUploadResponse read(JsonReader reader) throws IOException {
        MediaUploadResponse element = new MediaUploadResponse();
        String fieldName = null;

        if(reader.peek() == JsonToken.NULL){
            reader.nextNull();
            return null;
        }

        reader.beginObject();
        while (reader.hasNext()) {
            JsonToken token = reader.peek();
            if(token.equals(JsonToken.NAME))
                fieldName = reader.nextName();

            if (fieldName.equals("fileName") && token != JsonToken.NULL)
                element.setFileName(JsonAdapterUtils.stringFromJsonReader(reader));

            else if (fieldName.equals("fileId") && token != JsonToken.NULL)
                element.setFileID(JsonAdapterUtils.stringFromJsonReader(reader));

            else if (fieldName.equals("fileSizeInBytes") && token != JsonToken.NULL)
                element.SetFileSizeInBytes(JsonAdapterUtils.longFromJsonReader(reader));

            else
                reader.skipValue();
        }
        reader.endObject();

        return element;
    }

    @Override
    public void write(JsonWriter writer, MediaUploadResponse element) throws IOException {
        if(element == null){
            writer.nullValue();
            return;
        }
        writer.beginObject();
        writer.name("fileName").value(element.getFileName());
        writer.name("fileId").value(element.getFileId());
        writer.name("fileSizeInBytes").value(element.getFileSizeInBytes());
        writer.endObject();
    }
}

4- 使用这个调用(你发布的第二个)(编辑:这是解决主要问题的答案):
@Headers({
        "Accept: application/json"
})
@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);

5- 一些奖励,以便您不会错过某些依赖项:
a- BaseJsonAdapter.class 类(将帮助解析列表):
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by mamboa on 3/6/2018.
 */

public class BaseJsonAdapter<T> extends TypeAdapter<T>{

    public ArrayList<T> readArray(JsonReader reader) throws IOException {
        if(reader.peek() == JsonToken.NULL){
            reader.nextNull();
            return null;
        }

        ArrayList<T> elements = new ArrayList<T>();

        reader.beginArray();
        while (reader.hasNext()) {
            T value = read(reader);
            if(value != null)
                elements.add(value);
            else {
             break;
            }
        }
        reader.endArray();
        return elements;
    }

    public void writeArray(JsonWriter writer, List<T> messages) throws IOException {
        writer.beginArray();
        for (T message : messages) {
            write(writer, message);
        }
        writer.endArray();
    }

    public T read(JsonReader reader) throws IOException {
        return null;
    }

    public void write(JsonWriter writer, T t) throws IOException {
    }
}

最后是 JsonAdapterUtils:
package whatever.Utils;


import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

import java.io.IOException;

/**
 * Created by mamboa on 3/7/2018.
 */

public class JsonAdapterUtils {

    public static final int INTEGER_DEFAULT = -1;
    public static final String STRING_DEFAULT = "";
    public static final boolean BOOLEAN_DEFAULT = false;

    public static int intFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Integer.parseInt(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static long longFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Long.parseLong(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static float floatFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Float.parseFloat(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static double doubleFromJsonReader(JsonReader reader) throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  fromBooleanToInt(reader.nextBoolean());

            String resultValue = reader.nextString();
            if("".equals(resultValue))
                return INTEGER_DEFAULT;

            return Double.parseDouble(resultValue);
        }
        catch (IOException ex){
            return returnDefaultAfterException(reader);
        }
        catch (IllegalStateException ex){
            return returnDefaultAfterException(reader);
        }
        catch (NumberFormatException ex){
            return returnDefaultAfterException(reader);
        }
    }

    public static String stringFromJsonReader(JsonReader reader) throws IOException{
        String resultValue = "";
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  boolFromJsonReader(reader)? "true" : "false";

            resultValue = reader.nextString();
            return  !resultValue.equals("") ? resultValue : STRING_DEFAULT;
        }
        catch (IOException ex){
            reader.skipValue();
            return STRING_DEFAULT;
        }
        catch (IllegalStateException ex){
            reader.skipValue();
            return STRING_DEFAULT;
        }
    }

    public static boolean boolFromJsonReader(JsonReader reader)throws IOException{
        try {
            if(reader.peek() == JsonToken.BOOLEAN)
                return  reader.peek() == JsonToken.BOOLEAN ? reader.nextBoolean() : BOOLEAN_DEFAULT;
        }
        catch (IOException ex){
            reader.skipValue();
        }
        return BOOLEAN_DEFAULT;
    }

    private static int returnDefaultAfterException(JsonReader reader) throws IOException {
        if(reader != null) reader.skipValue();
        return INTEGER_DEFAULT;
    }

    private static int fromBooleanToInt(boolean value){
        return value ? 1 : 0;
    }

    public static String serializeObject(Object object){
        if(object != null) {
            Gson gson = new Gson();
            return gson.toJson(object);
        }
        return "";
    }
}

编辑:问题:
问题在于,在HTTP请求的参数中,服务器必须知道调用者希望以JSON格式获得响应。 因此,使用Retrofit 2时,当使用Multipart时,解决方案是在请求的顶部添加以下内容:
@Headers({
        "Accept: application/json"
})

我修改了我的代码,使用了你的修改,但是还是不起作用。在MediaUploadResponseAdapter类的"read"方法中,我得到了一个异常:java.lang.IllegalStateException: 在第1行第1列处预期为BEGIN_OBJECT,但实际上是STRING。路径为$。 - Javier Hinmel
如果你遇到这个错误,可能是因为你的Json没有以{开头。请检查一下。另外,在适配器类中,在read()方法中,对于每一行以element.set...开头的代码,可以设置一个断点来精确定位问题所在。 - Maxime Claude
非常感谢您的合作,Maxime!是的,我在我的GsonBuilder中使用了setLenient()。您可以使用Postman测试我的WebService,发送一个POST请求到:link ,选择form-data作为内容类型,并在请求体中设置以下参数:file(选择任意文件.jpg),filename =test.jpg,userId =TEST,photoId =0。 - Javier Hinmel
我按照你的要求尝试了 Postman,但出现了500的HTTP错误。这是一个截图。我有遗漏了什么吗? - Maxime Claude
1
感谢您详细的回答。对我很有帮助。然而,我意识到也许我可以直接要求服务器首先以JSON格式响应(@Headers({ "Accept: application/json" }))。是的,这解决了我在多部分响应方面的问题,而且我不需要再实现其他任何东西。 - Erik Medina
显示剩余10条评论

1

我曾经遇到过同样的问题,后来发现可以通过向服务器发送 JSON 请求来解决问题(@Headers):

@Headers({
        "Accept: application/json"
})
@POST("sync/mediaUpload")
@Multipart
Call<MediaUploadResponse> uploadMediaFile(@Header("Authorization") String token,
                                          @Part("userId") RequestBody userId,
                                          @Part MultipartBody.Part file,
                                          @Part("fileId") RequestBody photoId,
                                          @Part("hash") RequestBody hash);

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