使用截击和不使用 HttpEntity 的 POST 多部分请求

这不是一个真正的问题,但是,我想在这里分享我的一些工作代码,供您参考,当您需要的时候。

正如我们所知道的,HttpEntity已经从 API22中废弃,并且从 API23中完全删除。目前,我们不能再访问 Android 开发中的 HttpEntity 引用(404)。下面是我用于 有截击且无 HttpEntity 的 POST 多部分请求的工作示例代码。起作用了,用 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 的回答。

82191 次浏览

Just want to add to the answer. I was trying to figure how to append text fields to the body and created the following function to do it:

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);
}

It is working pretty well.

I rewrite your code @RacZo and @BNK more modular and easy to use like

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);

Check full of code VolleyMultipartRequest at my gist.

For those who are struggling to send utf-8 parameters and still no luck, the problem I had was in the dataOutputStream, and change the code of @RacZo to below code:

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);
}

I found a wrapper of the original volley library which is easier to integrate for multi-part requests. It also supports uploading the multi-part data along with other request parameters. Hence I am sharing my code for the future developers who might run into the problem that I was having (i.e. uploading multi-part data using volley along with some other parameters).

Add the following library in the build.gradle file.

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

Please note that, I removed the original volley library from my build.gradle and used the above library instead which can handle both multi-part and normal requests having similar integration technique.

Then I just had to write the following class which handles the POST request operation.

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);
}
}

Now execute the task like the following.

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

You can upload one file at a time using this library. However, I could manage to upload multiple files, just by initiating multiple tasks.

Hope that helps!

Here is a Kotlin version of a class allowing multipart request with Volley 1.1.1.

It's mostly based on @BNK's solution but slighly simplified. I did not notice any particular performance issue. I uploaded a 5Mb pic in about 3 seconds.

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)
}
}
}

Here is very simplified Kotlin Version for Multipart Volley POST Request

    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)
}