Рабочий многочастный запрос POST с залпом и без HttpEntity

Это на самом деле не вопрос, однако я хотел бы поделиться некоторыми из моего рабочего кода здесь для справки, когда вам нужно.

Как мы знаем, что HttpEntity устарела с API22 и полностью удалена с API23. В данный момент мы больше не можем получить доступ к HttpEntity Reference на Android Developer (404). Итак, вот мой рабочий пример кода для POST Multipart Request с Volley и без HttpEntity. Работает, проверено с Asp.Net Web API, Конечно, код, возможно, является просто базовым примером, в котором публикуются два существующих отрисовываемых файла, а также не является лучшим решением для всех случаев и не является хорошей настройкой.

MultipartActivity.java:

package com.example.multipartvolley;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.VolleyError;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;


public class MultipartActivity extends Activity {

    private final Context context = this;
    private final String twoHyphens = "--";
    private final String lineEnd = "\r\n";
    private final String boundary = "apiclient-" + System.currentTimeMillis();
    private final String mimeType = "multipart/form-data;boundary=" + boundary;
    private byte[] multipartBody;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multipart);

        byte[] fileData1 = getFileDataFromDrawable(context, R.drawable.ic_action_android);
        byte[] fileData2 = getFileDataFromDrawable(context, R.drawable.ic_action_book);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            // the first file
            buildPart(dos, fileData1, "ic_action_android.png");
            // the second file
            buildPart(dos, fileData2, "ic_action_book.png");
            // send multipart form data necesssary after file data
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // pass to multipart body
            multipartBody = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String url = "http://192.168.1.100/api/postfile";
        MultipartRequest multipartRequest = new MultipartRequest(url, null, mimeType, multipartBody, new Response.Listener<NetworkResponse>() {
            @Override
            public void onResponse(NetworkResponse response) {
                Toast.makeText(context, "Upload successfully!", Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(context, "Upload failed!\r\n" + error.toString(), Toast.LENGTH_SHORT).show();
            }
        });

        VolleySingleton.getInstance(context).addToRequestQueue(multipartRequest);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_multipart, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void buildPart(DataOutputStream dataOutputStream, byte[] fileData, String fileName) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""
                + fileName + "\"" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);

        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(fileData);
        int bytesAvailable = fileInputStream.available();

        int maxBufferSize = 1024 * 1024;
        int bufferSize = Math.min(bytesAvailable, maxBufferSize);
        byte[] buffer = new byte[bufferSize];

        // read file and write it into form...
        int bytesRead = fileInputStream.read(buffer, 0, bufferSize);

        while (bytesRead > 0) {
            dataOutputStream.write(buffer, 0, bufferSize);
            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
        }

        dataOutputStream.writeBytes(lineEnd);
    }

    private byte[] getFileDataFromDrawable(Context context, int id) {
        Drawable drawable = ContextCompat.getDrawable(context, id);
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }
}

MultipartRequest.java:

package com.example.multipartvolley;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;

import java.util.Map;

class MultipartRequest extends Request<NetworkResponse> {
    private final Response.Listener<NetworkResponse> mListener;
    private final Response.ErrorListener mErrorListener;
    private final Map<String, String> mHeaders;
    private final String mMimeType;
    private final byte[] mMultipartBody;

    public MultipartRequest(String url, Map<String, String> headers, String mimeType, byte[] multipartBody, Response.Listener<NetworkResponse> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, errorListener);
        this.mListener = listener;
        this.mErrorListener = errorListener;
        this.mHeaders = headers;
        this.mMimeType = mimeType;
        this.mMultipartBody = multipartBody;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return (mHeaders != null) ? mHeaders : super.getHeaders();
    }

    @Override
    public String getBodyContentType() {
        return mMimeType;
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        return mMultipartBody;
    }

    @Override
    protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) {
        try {
            return Response.success(
                    response,
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (Exception e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(NetworkResponse response) {
        mListener.onResponse(response);
    }

    @Override
    public void deliverError(VolleyError error) {
        mErrorListener.onErrorResponse(error);
    }
}

ОБНОВИТЬ:

Для текстовой части, пожалуйста, обратитесь к ответу @Oscar ниже.

6 ответов

Я переписываю ваш код @RacZo и @BNK более модульным и простым в использовании, как

VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.POST, url, new Response.Listener<NetworkResponse>() {
        @Override
        public void onResponse(NetworkResponse response) {
            String resultResponse = new String(response.data);
            // parse success output
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {                
            error.printStackTrace();
        }
    }) {
        @Override
        protected Map<String, String> getParams() {
            Map<String, String> params = new HashMap<>();
            params.put("api_token", "gh659gjhvdyudo973823tt9gvjf7i6ric75r76");
            params.put("name", "Angga");
            params.put("location", "Indonesia");
            params.put("about", "UI/UX Designer");
            params.put("contact", "angga@email.com");
            return params;
        }

        @Override
        protected Map<String, DataPart> getByteData() {
            Map<String, DataPart> params = new HashMap<>();
            // file name could found file base or direct access from real path
            // for now just get bitmap data from ImageView
            params.put("avatar", new DataPart("file_avatar.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mAvatarImage.getDrawable()), "image/jpeg"));
            params.put("cover", new DataPart("file_cover.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mCoverImage.getDrawable()), "image/jpeg"));

            return params;
        }
    };

    VolleySingleton.getInstance(getBaseContext()).addToRequestQueue(multipartRequest);

Проверьте полный код VolleyMultipartRequest в моем смысле.

Просто хочу добавить в ответ. Я пытался понять, как добавить текстовые поля к телу, и создал для этого следующую функцию:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
    dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
    dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
    dataOutputStream.writeBytes(lineEnd);
    dataOutputStream.writeBytes(parameterValue + lineEnd);
}

Это работает довольно хорошо.

Для тех, кто изо всех сил пытается отправить параметры utf-8 и все еще не везет, проблема была в dataOutputStream, и измените код @RacZo на код ниже:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"");
        dataOutputStream.write(parameterName.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.write(parameterValue.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
    } 

Вот Kotlin-версия класса, позволяющая выполнять составные запросы с помощью Volley 1.1.1.

В основном он основан на решении @BNK, но немного упрощен. Особых проблем с производительностью не заметил. Я загрузил 5Мб фото примерно за 3 секунды.

class MultipartWebservice(context: Context) {

    private var queue: RequestQueue? = null

    private val boundary = "apiclient-" + System.currentTimeMillis()
    private val mimeType = "multipart/form-data;boundary=$boundary"

    init {
        queue = Volley.newRequestQueue(context)
    }

    fun sendMultipartRequest(
        method: Int,
        url: String,
        fileData: ByteArray,
        fileName: String,
        listener: Response.Listener<NetworkResponse>,
        errorListener: Response.ErrorListener
    ) {

        // Create multi part byte array
        val bos = ByteArrayOutputStream()
        val dos = DataOutputStream(bos)
        buildMultipartContent(dos, fileData, fileName)
        val multipartBody = bos.toByteArray()

        // Request header, if needed
        val headers = HashMap<String, String>()
        headers["API-TOKEN"] = "458e126682d577c97d225bbd73a75b5989f65e977b6d8d4b2267537019ad9d20"

        val request = MultipartRequest(
            method,
            url,
            errorListener,
            listener,
            headers,
            mimeType,
            multipartBody
        )

        queue?.add(request)

    }

    @Throws(IOException::class)
    private fun buildMultipartContent(dos: DataOutputStream, fileData: ByteArray, fileName: String) {

        val twoHyphens = "--"
        val lineEnd = "\r\n"

        dos.writeBytes(twoHyphens + boundary + lineEnd)
        dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"$lineEnd")
        dos.writeBytes(lineEnd)
        dos.write(fileData)
        dos.writeBytes(lineEnd)
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
    }

    class MultipartRequest(
        method: Int,
        url: String,
        errorListener: Response.ErrorListener?,
        private var listener: Response.Listener<NetworkResponse>,
        private var headers: MutableMap<String, String>,
        private var mimeType: String,
        private var multipartBody: ByteArray
    ) : Request<NetworkResponse>(method, url, errorListener) {

        override fun getHeaders(): MutableMap<String, String> {
            return if (headers.isEmpty()) super.getHeaders() else headers
        }

        override fun getBodyContentType(): String {
            return mimeType
        }

        override fun getBody(): ByteArray {
            return multipartBody
        }

        override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse> {
            return try {
                Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
            } catch (e: Exception) {
                Response.error(ParseError(e))
            }
        }

        override fun deliverResponse(response: NetworkResponse?) {
            listener.onResponse(response)
        }
    }
}

Я нашел обертку исходной библиотеки volley, которую легче интегрировать для многостраничных запросов. Он также поддерживает загрузку данных, состоящих из нескольких частей, вместе с другими параметрами запроса. Поэтому я делюсь своим кодом для будущих разработчиков, которые могут столкнуться с проблемой, с которой я столкнулся (например, загрузка составных данных с помощью залпа вместе с некоторыми другими параметрами).

Добавьте следующую библиотеку в build.gradle файл.

dependencies {
    compile 'dev.dworks.libs:volleyplus:+'
}

Обратите внимание, что я удалил исходную библиотеку волейбола из своего build.gradle и вместо этого использовал указанную выше библиотеку, которая может обрабатывать как составные, так и обычные запросы с аналогичной техникой интеграции.

Затем мне просто нужно было написать следующий класс, который обрабатывает операцию запроса POST.

public class POSTMediasTask {
    public void uploadMedia(final Context context, String filePath) {

        String url = getUrlForPOSTMedia(); // This is a dummy function which returns the POST url for you
        SimpleMultiPartRequest multiPartRequestWithParams = new SimpleMultiPartRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        Log.d("Response", response);
                        // TODO: Do something on success
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // TODO: Handle your error here
            }
        });

        // Add the file here
        multiPartRequestWithParams.addFile("file", filePath);

        // Add the params here
        multiPartRequestWithParams.addStringParam("param1", "SomeParamValue1");
        multiPartRequestWithParams.addStringParam("param2", "SomeParamValue2");

        RequestQueue queue = Volley.newRequestQueue(context);
        queue.add(multiPartRequestWithParams);
    }
}

Теперь выполните следующую задачу.

new POSTMediasTask().uploadMedia(context, mediaPath);

С помощью этой библиотеки вы можете загружать по одному файлу за раз. Однако мне удалось загрузить несколько файлов, просто запустив несколько задач.

Надеюсь, это поможет!

Вот очень упрощенная версия Kotlin для запроса POST Multipart Volley

          private fun saveProfileAccount(concern: String, selectedDate: String, WDH: String, details: String, fileExtention: String) {

    val multipartRequest: VolleyMultipartRequest =
        object : VolleyMultipartRequest(
            Request.Method.POST, APIURLS.WhistleBlower,
            Response.Listener<JSONObject> { response ->

            },
            Response.ErrorListener { error ->

                error.printStackTrace()
            }) {

            @Throws(AuthFailureError::class)
            override fun getHeaders(): MutableMap<String, String>{
                val params: MutableMap<String, String> = HashMap()
                params[ConstantValues.XAppApiKey] = APIURLS.XAppApiValue
                return params
            }

            override fun getParams(): Map<String, String>? {
                val params: MutableMap<String, String> = HashMap()
                val sharedPreferences: SharedPreferences = requireActivity().getSharedPreferences(Prefs.PREF_NAME, 0)
                val userId = Prefs.getStringPref(sharedPreferences, Prefs.USER_ID)
                val userName = Prefs.getStringPref(sharedPreferences, Prefs.FULL_NAME)

                params["PersonId"] = userId.toString()
                params["PersonName"] = userName.toString()
                params["Concern"] = concern
                params["DateOfHappen"] = selectedDate
                params["WhereHappened"] = WDH
                params["Ext"] = fileExtention
                

                return params
            }


            override fun getByteData(): Map<String, DataPart>? {
                val params: MutableMap<String, DataPart> = HashMap()
                // file name could found file base or direct access from real path
                // for now just get bitmap data from ImageView

                params["cover"] = DataPart(
                    "sd.pdf",
                    byteArray,
                    "doc/pdf"
                )
                return params
            }
        }

    AppSingleton.getInstance(requireContext())?.addToRequestQueue(multipartRequest)
}
Другие вопросы по тегам