核心概念
-
HTTP 协议: 文件上传本质上是使用 HTTP 协议的
POST请求,有两种主要方式:
(图片来源网络,侵删)multipart/form-data: 这是最常用、最标准的方式,它允许你在一个请求中发送多种类型的数据(比如文本字段和文件),文件内容会被编码后放在请求体中。application/octet-stream: 这种方式相对简单,直接将文件二进制流作为请求体发送,但它通常只能上传一个文件,并且不能同时发送额外的文本参数。
-
权限: Android 应用需要声明权限才能访问文件系统。
android.permission.READ_EXTERNAL_STORAGE: 读取外部存储(如手机相册、下载文件夹)中的文件。android.permission.WRITE_EXTERNAL_STORAGE: 写入外部存储,如果需要将文件保存到外部存储,则需要此权限。- 注意: 从 Android 6.0 (API 23) 开始,
READ_EXTERNAL_STORAGE是运行时权限,必须在代码中动态请求,从 Android 11 (API 30) 开始,管理文件变得更加严格,推荐使用MediaStoreAPI 来访问媒体文件。
使用 HttpURLConnection (原生 Java API)
这是 Android SDK 自带的方式,无需第三方库,适合简单的上传需求。
Android 客户端实现
步骤:
-
添加网络权限 (
AndroidManifest.xml):
(图片来源网络,侵删)<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
处理运行时权限 (在 Activity 或 Fragment 中):
// 在需要上传文件的地方,例如按钮点击事件中 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_READ_STORAGE); } else { // 权限已授予,开始上传 uploadFile(); } -
编写上传方法:
public void uploadFile() { // 1. 选择文件 (这里以一个示例文件路径为例) // 实际应用中,你应该通过 Intent 调用系统文件选择器 String filePath = Environment.getExternalStorageDirectory().getPath() + "/my_image.jpg"; File file = new File(filePath); if (!file.exists()) { Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show(); return; } // 2. 创建请求体 RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", file.getName(), // "file" 是服务器端接收的字段名 RequestBody.create(MediaType.parse("image/jpeg"), file)) .addFormDataPart("description", "This is a test upload") // 可以添加其他文本参数 .build(); // 3. 创建请求 Request request = new Request.Builder() .url("https://your.server.com/upload") // 替换为你的服务器地址 .post(requestBody) .build(); // 4. 发起异步请求 OkHttpClient client = new OkHttpClient(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 请求失败 runOnUiThread(() -> Toast.makeText(MainActivity.this, "上传失败: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { // 请求成功 String responseData = response.body().string(); runOnUiThread(() -> { Toast.makeText(MainActivity.this, "上传成功: " + responseData, Toast.LENGTH_SHORT).show(); }); } }); }
服务器端接收 (以 Java Spring Boot 为例)
// Spring Boot Controller
@RestController
@RequestMapping("/api")
public class FileUploadController {
@PostMapping("/upload")
public ResponseEntity<String> handleFileUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("description") String description) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择一个文件");
}
try {
// 保存文件到服务器指定目录
String serverPath = "/var/www/uploads/" + file.getOriginalFilename();
Files.copy(file.getInputStream(), Paths.get(serverPath), StandardCopyOption.REPLACE_EXISTING);
// 返回成功响应
return ResponseEntity.ok("文件上传成功: " + file.getOriginalFilename() + ", 描述: " + description);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件上传失败");
}
}
}
使用 OkHttp + Retrofit (强烈推荐)
对于生产环境,使用 Retrofit 是更好的选择,它将网络请求接口化,代码更简洁、易维护。
添加依赖 (app/build.gradle)
// OkHttp 和 Retrofit implementation 'com.squareup.okhttp3:okhttp:4.9.3' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // 用于解析 JSON 响应 // 图片加载库 (可选,但强烈推荐) implementation 'com.github.bumptech.glide:glide:4.12.0'
定义 API 接口
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.Part;
public interface ApiService {
// @Multipart 表示这是一个 multipart/form-data 请求
// @POST 指定请求方法为 POST
// @Part 用于标记要上传的每个部分
@Multipart
@POST("upload") // 你的 API 路径
Call<UploadResponse> uploadFile(
@Part("file") RequestBody file, // "file" 是服务器字段名
@Part("description") RequestBody description // 其他文本参数
);
}
创建 Retrofit 实例
public class RetrofitClient {
private static final String BASE_URL = "https://your.server.com/api/"; // 基础 URL
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) // 添加 JSON 转换器
.build();
}
return retrofit;
}
}
在 Activity/ViewModel 中调用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button uploadButton = findViewById(R.id.upload_button);
uploadButton.setOnClickListener(v -> startUpload());
}
private void startUpload() {
// 1. 获取文件
String filePath = Environment.getExternalStorageDirectory().getPath() + "/my_image.jpg";
File file = new File(filePath);
// 2. 创建 RequestBody
RequestBody fileBody = RequestBody.create(MediaType.parse("image/*"), file);
RequestBody descriptionBody = RequestBody.create(MediaType.parse("text/plain"), "This is a test");
// 3. 创建 API Service 实例
ApiService apiService = RetrofitClient.getClient().create(ApiService.class);
// 4. 发起请求
Call<UploadResponse> call = apiService.uploadFile(fileBody, descriptionBody);
call.enqueue(new Callback<UploadResponse>() {
@Override
public void onResponse(Call<UploadResponse> call, Response<UploadResponse> response) {
if (response.isSuccessful()) {
UploadResponse uploadResponse = response.body();
Log.d("Upload", "Success: " + uploadResponse.getMessage());
Toast.makeText(MainActivity.this, "上传成功", Toast.LENGTH_SHORT).show();
} else {
Log.e("Upload", "Error: " + response.code());
Toast.makeText(MainActivity.this, "上传失败: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<UploadResponse> call, Throwable t) {
Log.e("Upload", "Failure: " + t.getMessage());
Toast.makeText(MainActivity.this, "上传失败: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
最佳实践与注意事项
-
后台任务: 文件上传可能很耗时,绝对不能在主线程(UI线程)中执行,必须使用异步任务、线程池或协程,上面的例子中,Retrofit 的
enqueue方法已经帮我们处理了这一点。 -
进度显示: 良好的用户体验需要显示上传进度,OkHttp 3.0+ 提供了
ProgressResponseBody来实现这个功能。- 你需要创建一个继承自
ResponseBody的类,在其中拦截source()方法,计算已读取的字节数。 - 然后创建一个
OkHttpClient,并设置一个Interceptor来包装你的原始Response为ProgressResponseBody。 - 在回调中,你可以通过一个接口(如
UploadCallbacks)将进度更新到 UI。
- 你需要创建一个继承自
-
文件选择: 不要硬编码文件路径,使用
Intent调用系统文件选择器或图片选择器,让用户自己选择要上传的文件。Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(intent, PICK_IMAGE_REQUEST); -
安全性:
- HTTPS: 必须使用 HTTPS 协议,防止数据在传输过程中被窃听或篡改。
- 认证: 服务器需要对上传请求进行身份验证,例如使用 Token 或 OAuth2。
- 文件类型/大小限制: 在服务器端对上传的文件类型(通过扩展名或 Content-Type)、大小进行严格校验,防止恶意用户上传超大文件或病毒文件。
-
错误处理: 处理各种可能的错误,如网络中断、服务器返回 500 错误、文件读取失败等,并向用户友好的提示。
-
取消上传: 提供一个取消按钮,允许用户中断上传,Retrofit 的
Call对象有一个cancel()方法,可以取消正在进行的请求。 -
Android 10+ 作用域存储: 对于 Android 10 (API 29) 及更高版本,推荐使用
MediaStoreAPI 或Storage Access Framework (SAF)来访问和共享文件,而不是直接访问Environment.getExternalStorageDirectory(),这是未来的趋势,可以更好地保护用户隐私。
| 特性 | HttpURLConnection |
Retrofit + OkHttp |
|---|---|---|
| 依赖 | 无需第三方库 | 需要 OkHttp 和 Retrofit |
| 易用性 | 较低,代码繁琐 | 高,接口化,代码简洁 |
| 功能 | 基础功能 | 强大,支持拦截器、适配器、进度等 |
| 维护性 | 差 | 好,易于扩展和测试 |
| 适用场景 | 简单 demo、小型项目 | 所有生产环境项目 |
强烈建议在新的 Android 项目中使用 Retrofit + OkHttp 的方案,它能让你更专注于业务逻辑,而不是网络通信的底层细节。
