Android 中的 Multipart/Form-Data 请求:HttpURLConnection 与 OkHttp
在 Android 开发中,向服务器上传文件及表单数据是常见的需求,通常使用
multipart/form-data编码格式。本文将对比使用原生HttpURLConnection和现代库OkHttp4的实现方式,指出原始代码中的问题,并提供现代化改进建议。
博主博客
第一部分:使用 HttpURLConnection 实现
以下是一个使用 HttpURLConnection 发送 Multipart 请求的示例,该示例将图片和表单数据一并上传。
public class HandleAccountData extends AsyncTask<String, Void, Void> {
protected Void doInBackground(String... params) {
HttpURLConnection conn = null;
DataOutputStream dos = null;
String lineEnd = "\r\n";
String twoHyphens = "--";
String boundary = "*****";
int bytesRead, bytesAvailable, bufferSize;
byte[] buffer;
int maxBufferSize = 1 * 1024 * 1024;
int serverResponseCode = 0;
// 参数
String urldisplay = params[0];
String email = params[1];
String personName = params[2];
String loginMethod = params[3];
Bitmap mIcon11 = null;
try {
System.setProperty("http.keepAlive", "false");
/////////////////////////////////////////////
//// 上传图片和数据
// 从网络下载图片
InputStream in = new URL(urldisplay).openStream();
mIcon11 = BitmapFactory.decodeStream(in);
// 将图片编码为字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mIcon11.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] imageBytes = baos.toByteArray();
// 打开到 Servlet 的 URL 连接
ByteArrayInputStream fileInputStream = new ByteArrayInputStream(imageBytes);
URL url = new URL(url_upload_profile_picture);
// 打开 HTTP 连接
conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true); // 允许输入
conn.setDoOutput(true); // 允许输出
conn.setUseCaches(false); // 不使用缓存
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept-Encoding", "");
// conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("ENCTYPE", "multipart/form-data");
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
conn.setRequestProperty("uploaded_file", "profile_picture");
conn.setRequestProperty("email", email);
conn.setRequestProperty("full_name", personName);
conn.setRequestProperty("password", loginMethod);
dos = new DataOutputStream(conn.getOutputStream());
// 第一个参数 - email
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"email\"" + lineEnd + lineEnd
+ email + lineEnd);
// 第二个参数 - userName
// 修正:此处原始代码将字节数组直接 toString,是错误的,应直接写入字符串
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"full_name\"" + lineEnd + lineEnd
+ personName + lineEnd);
// 第三个参数 - password
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"password\"" + lineEnd + lineEnd
+ loginMethod + lineEnd);
// 第四个参数 - 文件名
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\";filename=\""
+ "profile_picture" + "\"" + lineEnd);
dos.writeBytes(lineEnd);
// 创建最大大小的缓冲区
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
buffer = new byte[bufferSize];
// 读取文件并写入表单
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
while (bytesRead > 0) {
dos.write(buffer, 0, bufferSize);
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
}
// 发送文件数据后的必要分隔符
dos.writeBytes(lineEnd);
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
// 获取服务器响应(状态码和信息)
serverResponseCode = conn.getResponseCode();
String serverResponseMessage = conn.getResponseMessage();
Log.i("uploadFile", "HTTP Response is : "
+ serverResponseMessage + ": " + serverResponseCode);
// 关闭流
fileInputStream.close();
dos.flush();
dos.close();
conn.disconnect();
///////////////////////////////////////////////////////////
/////////// 将数据存储到 SharedPreferences
// 创建小尺寸图片链接(假设原链接以特定格式结尾)
urldisplay = urldisplay.substring(0, urldisplay.length() - 3) + "70";
// 创建 SharedPreferences 变量来存储用户信息
// 'mysettings' 是文件名
SharedPreferences mSettings = getSharedPreferences("mysetting", Context.MODE_PRIVATE);
Editor editor = mSettings.edit();
editor.putString("login_method", loginMethod);
editor.putString("email", email);
editor.putString("name", personName);
editor.putString("pic_uri", urldisplay);
editor.apply();
////////////////////////////////////////////////////////////
////// 启动主界面
Intent i = new Intent(getApplicationContext(), MainPage.class);
startActivity(i);
finish();
} catch (MalformedURLException ex) {
Log.e("Upload file to server", "error: " + ex.getMessage(), ex);
} catch (Exception e) {
String err = (e.getMessage() == null) ? "SD Card failed" : e.getMessage();
Log.e("The caught exception is: ", err);
}
return null;
}
}
HttpURLConnection 实现的问题与说明
- 字符编码问题:原始代码中
personName.getBytes("UTF-8").toString()是错误的,它不会返回原始字符串,而是字节数组的对象地址。已修正为直接使用字符串。 - 硬编码边界符:边界符
boundary应为随机生成,以避免与内容冲突。 - 主线程操作:在
doInBackground中直接启动 Activity 和操作 UI 是不安全的,应通过onPostExecute或在主线程中执行。 - 已过时 API:
AsyncTask在较新版本的 Android 中已弃用,建议使用Kotlin 协程、RxJava或WorkManager等现代异步处理方式。 - 资源管理:需要确保所有流都被正确关闭,即使在发生异常的情况下。
第二部分:使用 OkHttp4 实现(现代方案)
以下是使用 OkHttp4 发送相同请求的示例。OkHttp4 提供了更简洁、更安全的 API,并支持 Kotlin 协程。
Kotlin + 协程实现
class ProfileUploader(private val context: Context) {
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
suspend fun uploadProfileData(
imageUrl: String,
email: String,
name: String,
password: String,
uploadUrl: String
): Result<Unit> = withContext(Dispatchers.IO) {
return@withContext try {
// 1. 下载图片
val bitmap = downloadImage(imageUrl)
// 2. 上传数据
val response = uploadToServer(bitmap, email, name, password, uploadUrl)
if (response.isSuccessful) {
// 3. 保存到 SharedPreferences
saveToPreferences(imageUrl, email, name, password)
// 4. 返回成功(UI操作应在调用处处理)
Result.success(Unit)
} else {
Result.failure(Exception("上传失败: ${response.code} ${response.message}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
private suspend fun downloadImage(imageUrl: String): Bitmap = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(imageUrl)
.build()
okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("下载图片失败: ${response.code}")
val body = response.body ?: throw IOException("响应体为空")
val inputStream = body.byteStream()
BitmapFactory.decodeStream(inputStream) ?: throw IOException("图片解码失败")
}
}
private suspend fun uploadToServer(
bitmap: Bitmap,
email: String,
name: String,
password: String,
uploadUrl: String
): Response = withContext(Dispatchers.IO) {
// 将 Bitmap 转换为字节数组
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, byteArrayOutputStream)
val imageBytes = byteArrayOutputStream.toByteArray()
// 构建 Multipart 请求体
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("email", email)
.addFormDataPart("full_name", name)
.addFormDataPart("password", password)
.addFormDataPart(
"uploaded_file",
"profile_picture.jpg",
imageBytes.toRequestBody("image/jpeg".toMediaType())
)
.build()
val request = Request.Builder()
.url(uploadUrl)
.post(requestBody)
.build()
return@withContext okHttpClient.newCall(request).execute()
}
private fun saveToPreferences(
imageUrl: String,
email: String,
name: String,
password: String
) {
// 生成缩略图 URL(假设特定格式)
val thumbnailUrl = imageUrl.substring(0, imageUrl.length - 3) + "70"
context.getSharedPreferences("mysetting", Context.MODE_PRIVATE).edit {
putString("login_method", password)
putString("email", email)
putString("name", name)
putString("pic_uri", thumbnailUrl)
}
}
}
// 使用示例(在 ViewModel 或 Activity/Fragment 中)
class ProfileViewModel : ViewModel() {
private val profileUploader = ProfileUploader(applicationContext)
private val _uploadState = MutableStateFlow<UploadState>(UploadState.Idle)
val uploadState: StateFlow<UploadState> = _uploadState
fun uploadProfile(
imageUrl: String,
email: String,
name: String,
password: String,
uploadUrl: String
) {
viewModelScope.launch {
_uploadState.value = UploadState.Loading
val result = profileUploader.uploadProfileData(
imageUrl, email, name, password, uploadUrl
)
_uploadState.value = when (result) {
is Result.Success -> UploadState.Success
is Result.Failure -> UploadState.Error(result.exception.message ?: "未知错误")
}
}
}
sealed class UploadState {
object Idle : UploadState()
object Loading : UploadState()
object Success : UploadState()
data class Error(val message: String) : UploadState()
}
}
Java 兼容版本(OkHttp4)
public class OkHttp4Uploader {
private final OkHttpClient client;
private final Context context;
public OkHttp4Uploader(Context context) {
this.context = context.getApplicationContext();
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public interface UploadCallback {
void onSuccess();
void onError(String error);
}
public void uploadProfile(
String imageUrl,
String email,
String name,
String password,
String uploadUrl,
UploadCallback callback
) {
new Thread(() -> {
try {
// 1. 下载图片
Bitmap bitmap = downloadImage(imageUrl);
// 2. 上传到服务器
boolean success = uploadToServer(bitmap, email, name, password, uploadUrl);
if (success) {
// 3. 保存到 SharedPreferences
saveToPreferences(imageUrl, email, name, password);
// 4. 回调成功(切换到主线程)
new Handler(Looper.getMainLooper()).post(callback::onSuccess);
} else {
new Handler(Looper.getMainLooper()).post(() ->
callback.onError("上传失败"));
}
} catch (Exception e) {
new Handler(Looper.getMainLooper()).post(() ->
callback.onError(e.getMessage()));
}
}).start();
}
private Bitmap downloadImage(String imageUrl) throws IOException {
Request request = new Request.Builder()
.url(imageUrl)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("下载失败: " + response.code());
}
ResponseBody body = response.body();
if (body == null) {
throw new IOException("响应体为空");
}
InputStream inputStream = body.byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap == null) {
throw new IOException("图片解码失败");
}
return bitmap;
}
}
private boolean uploadToServer(
Bitmap bitmap,
String email,
String name,
String password,
String uploadUrl
) throws IOException {
// 压缩图片
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);
byte[] imageBytes = baos.toByteArray();
// 构建 Multipart 请求体
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("email", email)
.addFormDataPart("full_name", name)
.addFormDataPart("password", password)
.addFormDataPart(
"uploaded_file",
"profile_picture.jpg",
RequestBody.create(imageBytes, MediaType.parse("image/jpeg"))
)
.build();
Request request = new Request.Builder()
.url(uploadUrl)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
}
}
private void saveToPreferences(
String imageUrl,
String email,
String name,
String password
) {
String thumbnailUrl = imageUrl.substring(0, imageUrl.length() - 3) + "70";
SharedPreferences preferences = context.getSharedPreferences(
"mysetting", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("login_method", password);
editor.putString("email", email);
editor.putString("name", name);
editor.putString("pic_uri", thumbnailUrl);
editor.apply();
}
}
OkHttp4 实现的优势
- 简洁的 API:MultipartBody.Builder 提供了直观的构建方式
- 自动管理:支持 try-with-resources(Java)或 use 函数(Kotlin)自动关闭资源
- 超时配置:可以方便地设置连接、读取和写入超时
- 拦截器支持:便于添加日志、认证等通用功能
- 协程友好:在 Kotlin 中与协程完美结合
现代化最佳实践
1. 使用 Kotlin 协程处理异步
// 在 ViewModel 中使用
viewModelScope.launch {
try {
val result = uploader.uploadData(...)
// 更新 UI 状态
} catch (e: Exception) {
// 处理错误
}
}
2. 添加进度监听
// 自定义 RequestBody 以跟踪上传进度
class ProgressRequestBody(
private val requestBody: RequestBody,
private val onProgress: (percentage: Int) -> Unit
) : RequestBody() {
override fun contentType(): MediaType? = requestBody.contentType()
override fun contentLength(): Long = requestBody.contentLength()
override fun writeTo(sink: BufferedSink) {
val countingSink = countingSink(sink) { bytesWritten ->
val percentage = (bytesWritten * 100 / contentLength()).toInt()
onProgress(percentage)
}
val bufferedSink = countingSink.buffer()
requestBody.writeTo(bufferedSink)
bufferedSink.flush()
}
private fun countingSink(
sink: Sink,
onProgress: (bytesWritten: Long) -> Unit
): ForwardingSink {
return object : ForwardingSink(sink) {
private var bytesWritten = 0L
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
onProgress(bytesWritten)
}
}
}
}
3. 使用 Hilt/Dagger 依赖注入
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("User-Agent", "MyApp/1.0")
.build()
chain.proceed(request)
}
.build()
}
@Provides
fun provideUploader(client: OkHttpClient): ProfileUploader {
return ProfileUploader(client)
}
}
4. 错误处理与重试机制
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 1000,
maxDelay: Long = 10000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == times - 1) throw e
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
}
return block() // 最后一次尝试
}
对比总结
| 特性 | HttpURLConnection | OkHttp4 |
|---|---|---|
| API 简洁性 | 冗长,需要手动构造 multipart 格式 | 简洁,Builder 模式 |
| 性能 | 基础实现,性能一般 | 连接池、缓存优化,性能更好 |
| 功能特性 | 基础功能 | 拦截器、自动重试、HTTP/2 支持等 |
| 异步支持 | 需自行封装 | 原生支持 Callback,协程友好 |
| 社区维护 | Android 标准库,更新缓慢 | 活跃社区,持续更新 |
| 适用场景 | 简单请求,避免额外依赖 | 生产环境,需要高级功能 |
迁移建议
- 新项目:直接使用 OkHttp4 + Kotlin 协程组合
- 老项目迁移:逐步替换,优先在新增功能中使用 OkHttp
- 保持兼容:对于简单请求,仍可使用 HttpURLConnection,但建议统一技术栈
- 测试保障:迁移过程中确保充分的单元测试和集成测试
结论
在 Android 开发中,虽然 HttpURLConnection 仍然可用,但 OkHttp4 提供了更现代、更强大的网络请求能力。结合 Kotlin 协程,可以编写出更简洁、更安全的异步代码。对于 multipart/form-data 请求,OkHttp 的 MultipartBody.Builder 极大地简化了构建过程,避免了手动拼接格式的错误。
建议开发者根据项目需求选择技术方案:对于简单应用或学习目的,HttpURLConnection 足够使用;对于生产环境或复杂应用,OkHttp 是更优选择。无论选择哪种方式,都要注意正确处理异步操作、资源管理和错误处理,以确保应用的稳定性和用户体验。
Android 中的 Multipart/Form-Data 请求:HttpURLConnection 与 OkHttp
https://blog.uso6.com/archives/android-zhong-de-multipart-form-data-qing-qiu-httpurlconnection-yu-okhttp
评论