凌峰创科服务平台

Android文件上传到服务器,如何实现高效稳定?

  • Android 端: Retrofit + OkHttp (网络请求库) + MultipartBody (用于构建文件上传请求体)。
  • 服务器端: Java Spring Boot (因为它是 Java 开发中最流行的后端框架,示例代码清晰易懂),如果你使用其他语言(如 Node.js, Python, PHP),核心逻辑是相通的。

目录

  1. 核心概念: 了解文件上传的原理。
  2. 服务器端准备: 用 Spring Boot 搭建一个简单的接收文件接口。
  3. Android 端实现: 详细步骤,从添加依赖到编写上传代码。
  4. 进阶与最佳实践: 错误处理、进度显示、多文件上传等。
  5. 常见问题与解决方案

核心概念:HTTP 文件上传

在 Web 开发中,文件上传通常使用 POST 请求,并且请求头的 Content-Type 设置为 multipart/form-data

Android文件上传到服务器,如何实现高效稳定?-图1
(图片来源网络,侵删)
  • POST: 使用 POST 方法可以将数据作为请求体发送,适合传输较大的文件。
  • multipart/form-data: 这种内容类型允许你在一个请求中发送多个部分的数据(文件和文本字段),每个部分之间由一个特殊的边界字符串分隔。
  • MultipartBody: 在 Android 中,我们使用 OkHttpMultipartBody 来构建这种复杂的请求体,它会自动处理边界字符串和数据的编码。

服务器端准备 (Spring Boot 示例)

我们需要一个服务器来接收文件,这里是一个简单的 Spring Boot 控制器示例。

1. 添加 Maven 依赖

在你的 pom.xml 文件中确保有 spring-boot-starter-web 依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 创建文件上传控制器

创建一个 Java 类来处理文件上传请求。

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
    // 定义服务器上存储文件的目录
    private static final String UPLOADED_FOLDER = "/path/to/your/server/uploads/"; // 请务必修改为你的实际路径
    @PostMapping("/single")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return "Please select a file to upload";
        }
        try {
            // 获取原始文件名
            String originalFilename = file.getOriginalFilename();
            // 创建目标文件路径
            Path destinationPath = Paths.get(UPLOADED_FOLDER + originalFilename);
            // 将文件保存到服务器
            Files.copy(file.getInputStream(), destinationPath);
            return "File uploaded successfully: " + originalFilename;
        } catch (IOException e) {
            e.printStackTrace();
            return "File upload failed: " + e.getMessage();
        }
    }
    // 多文件上传示例
    @PostMapping("/multi")
    public String uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        StringBuilder result = new StringBuilder();
        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                result.append("Skipped empty file. ");
                continue;
            }
            try {
                String originalFilename = file.getOriginalFilename();
                Path destinationPath = Paths.get(UPLOADED_FOLDER + originalFilename);
                Files.copy(file.getInputStream(), destinationPath);
                result.append("Successfully uploaded: ").append(originalFilename).append(". ");
            } catch (IOException e) {
                result.append("Failed to upload: ").append(file.getOriginalFilename()).append(". ");
                e.printStackTrace();
            }
        }
        return result.toString();
    }
}

重要提示:

Android文件上传到服务器,如何实现高效稳定?-图2
(图片来源网络,侵删)
  • /path/to/your/server/uploads/ 替换为你服务器上一个有写入权限的真实目录。
  • Spring Boot 默认对上传文件的大小有限制(通常是 1MB),你需要在 application.propertiesapplication.yml 中增加限制:
    # application.properties
    spring.servlet.multipart.max-file-size=10MB
    spring.servlet.multipart.max-request-size=10MB

Android 端实现 (使用 Retrofit)

1. 添加依赖

在你的 app/build.gradle 文件的 dependencies 代码块中添加以下依赖:

// Retrofit for networking
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // 如果你还需要解析JSON
// OkHttp for logging and other features
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'

2. 添加网络权限

app/src/main/AndroidManifest.xml 中添加 INTERNET 权限:

<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>
</manifest>

3. 创建 Retrofit 接口

定义一个接口来描述我们的 API。

import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
public interface FileUploadApi {
    // 单文件上传
    // @Multipart 表示这是一个 multipart/form-data 请求
    // @Part("file") 指定了表单中字段的名称,必须与服务器端 @RequestParam("file") 的 "file" 对应
    @Multipart
    @POST("/api/upload/single")
    Call<String> uploadFile(@Part MultipartBody.Part file);
    // 多文件上传
    @Multipart
    @POST("/api/upload/multi")
    Call<String> uploadMultipleFiles(@Part List<MultipartBody.Part> files);
    // 带其他参数的文件上传
    @Multipart
    @POST("/api/upload/with-info")
    Call<String> uploadFileWithInfo(
            @Part("file") MultipartBody.Part file,
            @Part("description") RequestBody description
    );
}

4. 创建 Retrofit 实例并进行上传

这是上传逻辑的核心,通常在 ActivityViewModel 中调用。

Android文件上传到服务器,如何实现高效稳定?-图3
(图片来源网络,侵删)
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "FileUpload";
    private static final String BASE_URL = "http://your-server-ip:8080/"; // 请替换为你的服务器地址
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button uploadButton = findViewById(R.id.btn_upload);
        uploadButton.setOnClickListener(v -> uploadSingleFile());
    }
    private void uploadSingleFile() {
        // 1. 选择要上传的文件 (这里以选择手机上的一个图片为例)
        // 实际项目中,你可能通过 Intent.ACTION_GET_CONTENT 让用户选择文件
        File file = new File(getExternalFilesDir(null), "test_image.jpg");
        if (!file.exists()) {
            Toast.makeText(this, "File not found!", Toast.LENGTH_SHORT).show();
            return;
        }
        // 2. 创建 RequestBody
        // "image/*" 是文件的 MIME 类型,可以根据文件类型修改
        RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file);
        // 3. 创建 MultipartBody.Part
        MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
        // 4. 创建 Retrofit 实例
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        // 5. 创建 API 接口的实例
        FileUploadApi uploadApi = retrofit.create(FileUploadApi.class);
        // 6. 发起异步上传请求
        Call<String> call = uploadApi.uploadFile(body);
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {
                if (response.isSuccessful()) {
                    // 上传成功
                    Log.d(TAG, "Upload successful: " + response.body());
                    Toast.makeText(MainActivity.this, "Upload Success: " + response.body(), Toast.LENGTH_LONG).show();
                } else {
                    // 服务器返回了错误状态码
                    Log.e(TAG, "Upload failed with error code: " + response.code());
                    Toast.makeText(MainActivity.this, "Upload Failed: Server Error", Toast.LENGTH_SHORT).show();
                }
            }
            @Override
            public void onFailure(Call<String> call, Throwable t) {
                // 请求失败,如网络问题、解析错误等
                Log.e(TAG, "Upload failed: ", t);
                Toast.makeText(MainActivity.this, "Upload Failed: " + t.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

进阶与最佳实践

1. 显示上传进度

Retrofit 默认不提供进度回调,但我们可以通过 OkHttpInterceptor 来实现。

  1. 创建进度监听接口

    public interface ProgressListener {
        void onProgress(long bytesWritten, long contentLength);
    }
  2. 创建 ProgressRequestBody

    import okhttp3.MediaType;
    import okhttp3.RequestBody;
    import okio.Buffer;
    import okio.BufferedSink;
    import okio.ForwardingSink;
    import okio.Okio;
    import okio.Sink;
    import java.io.IOException;
    public class ProgressRequestBody extends RequestBody {
        private final RequestBody requestBody;
        private final ProgressListener progressListener;
        public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
            this.requestBody = requestBody;
            this.progressListener = progressListener;
        }
        @Override
        public MediaType contentType() {
            return requestBody.contentType();
        }
        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            BufferedSink bufferedSink = Okio.buffer(new ForwardingSink(sink) {
                private long bytesWritten = 0L;
                private long contentLength = 0L;
                @Override
                public void write(Buffer source, long byteCount) throws IOException {
                    super.write(source, byteCount);
                    if (contentLength == 0) {
                        contentLength = contentLength();
                    }
                    bytesWritten += byteCount;
                    progressListener.onProgress(bytesWritten, contentLength);
                }
            });
            requestBody.writeTo(bufferedSink);
            bufferedSink.flush();
        }
    }
  3. 在上传时使用

    // 在 MainActivity 中
    private void uploadFileWithProgress() {
        File file = new File(getExternalFilesDir(null), "test_image.jpg");
        RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file);
        // 使用 ProgressRequestBody 包装
        ProgressRequestBody progressRequestBody = new ProgressRequestBody(requestFile, new ProgressListener() {
            @Override
            public void onProgress(long bytesWritten, long contentLength) {
                // 在这里更新 UI 显示进度
                // 注意:不能在子线程更新 UI,需要 runOnUiThread
                int progress = (int) ((bytesWritten * 100) / contentLength);
                runOnUiThread(() -> {
                    // progressBar.setProgress(progress);
                    Log.d(TAG, "Upload Progress: " + progress + "%");
                });
            }
        });
        MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), progressRequestBody);
        // ... 后续的 Retrofit 调用和 enqueue 与之前相同
    }

2. 上传多个文件

只需将多个 MultipartBody.Part 放入一个 List 中,然后调用接口中的多文件上传方法即可。

private void uploadMultipleFiles() {
    List<File> filesToUpload = new ArrayList<>();
    filesToUpload.add(new File(getExternalFilesDir(null), "file1.jpg"));
    filesToUpload.add(new File(getExternalFilesDir(null), "file2.png"));
    List<MultipartBody.Part> parts = new ArrayList<>();
    for (File file : filesToUpload) {
        RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), file);
        MultipartBody.Part part = MultipartBody.Part.createFormData("files", file.getName(), requestFile);
        parts.add(part);
    }
    Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL).build();
    FileUploadApi api = retrofit.create(FileUploadApi.class);
    Call<String> call = api.uploadMultipleFiles(parts);
    call.enqueue(...); // 与之前相同的 enqueue 回调
}

3. 添加其他表单字段

有时上传文件时还需要附带其他信息(如用户ID、描述等),这可以通过 @Part 注解添加 RequestBody 来实现。

// 在接口中定义
@Multipart
@POST("/api/upload/with-info")
Call<String> uploadFileWithInfo(
    @Part("file") MultipartBody.Part file,
    @Part("userId") RequestBody userId,
    @Part("description") RequestBody description
);
// 在调用时构建 RequestBody
String userId = "12345";
String description = "This is a test image";
RequestBody userIdBody = RequestBody.create(MediaType.parse("text/plain"), userId);
RequestBody descBody = RequestBody.create(MediaType.parse("text/plain"), description);
// ... 创建 file body ...
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
Call<String> call = api.uploadFileWithInfo(filePart, userIdBody, descBody);

常见问题与解决方案

  1. java.net.UnknownHostException:

    • 原因: 无法连接到服务器。
    • 解决:
      • 检查 BASE_URL 是否正确,包括 IP 地址和端口号。
      • 确保手机和服务器在同一个局域网内(如果是在本地开发测试)。
      • 确认服务器正在运行,并且防火墙没有阻止你的端口。
  2. java.net.SocketTimeoutException:

    • 原因: 读取服务器响应超时。

    • 解决:

      • 文件太大或网络太慢,导致默认的超时时间不够。

      • 可以在 OkHttpClient 中设置超时时间:

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS) // 连接超时
                .writeTimeout(2, TimeUnit.MINUTES)     // 写入超时
                .readTimeout(2, TimeUnit.MINUTES)     // 读取超时
                .build();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient)
                .build();
  3. android.os.NetworkOnMainThreadException:

    • 原因: 在主线程(UI线程)中执行了网络操作。
    • 解决: 网络请求(如 call.enqueue())必须在子线程中执行。enqueue 方法本身是异步的,但如果你在主线程调用它,Retrofit 会抛出这个异常,通常我们直接在主线程的 onClick 中调用 enqueue,因为它内部会处理线程切换,所以这个异常不常出现,但如果同步调用 call.execute(),则必须在子线程。
  4. 服务器返回 413 Request Entity Too Large:

    • 原因: 上传的文件大小超过了服务器配置的限制。
    • 解决: 在 Spring Boot 的 application.properties 中增加 spring.servlet.multipart.max-file-sizespring.servlet.multipart.max-request-size 的值。
  5. Content-Type 不匹配:

    • 原因: Android 端设置的 MediaType 与服务器端期望的不一致。
    • 解决: 确保你为文件设置的 MediaType 是正确的(如 image/jpeg 用于 .jpg 文件,application/pdf 用于 .pdf 文件),如果不确定,可以使用 application/octet-stream 作为通用类型。

希望这份详细的指南能帮助你顺利实现 Android 文件上传功能!

分享:
扫描分享到社交APP
上一篇
下一篇